|
|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| This handy utility class will save time and keep your code
cleaner
Scott
Clee (mailto:Scott_Clee@uk.ibm.com?cc=&subject=Use
a consistent trace system for easier debugging) CICS Tester,
IBM 4 November 2003
When faced with a thorny bug, many
developers use System.out.println statements to send status
messages to the console so that they can more easily pin down the moment
at which their program goes awry. But those statements slow down program
execution and can be difficult to clean up once the code is ready for
production; more to the point, they are more of a stop-gap measure than
a truly consistent debugging system. In this article, Scott Clee
introduces a tracing utility class that improves upon this debugging
method. He explains how the class works and how you can configure it for
your particular needs, and offers examples to show how you can integrate
it into your own projects.
We've all been there: just as your program is nearing completion, you
run it once more to bask in its glory and ... it's broken. You can't
remember what you added to break it, and have no idea what's gone wrong.
Like many developers, you decide to bring out the trusty old
System.out.println command to help you get in there and fix
it. By inserting these tracing statements into your code, you can see the
progress of your program execution at the console and figure out where it
all goes wrong. You add a few trace statements, then a few more, and a few
more. And once you've cracked the problem, you have to remove all those
extraneous trace statements, because they'll only get in the way in a
production environment. "That wasn't so bad after all," you think once
you're done. But then you find another bug, and the trace statements that
helped you last time are, of course, gone -- if only you'd kept them!
Well, there's no reason you can't leave debugging code in your
applications to help you troubleshoot throughout the development
process.
The assertion functionality built into version 1.4 of the Java language
shows us how it can be done. On its Web site, Sun claims that assertions
have zero performance impact if the language's assertion facility is
switched off:
To
ensure that assertions are not a performance liability in deployed
applications, assertions can be enabled or disabled when the program is
started, and are disabled by default. Disabling assertions eliminates
their performance penalty entirely.
(See Resources for
more on assertions and other useful information.)
What this means is that assertion statements in compiled code are
somehow not actually executed, and hence incur no performance penalty.
Now, if Sun could take the same approach with a built-in Java trace
system, the world would be a much better place. But until then, we have to
do it ourselves.
What we need is a single Java class that will contain all the tracing
functionality we could ever want. We should be able to reuse this class in
all of our Java projects. This will give us confidence that our code can
be debugged easily with a consistent look and feel.
Luckily for you, I'll discuss just such a class in this article.
Examining the Trace
class The class we'll introduce here is called
Trace ; it exists in a package called T . We've
placed this class in a package so that it can be called from any Java
class that may also exist in a package; there is a Java language
restriction that prevents classes in packages from calling classes that
exist in the default package (that is, in no package). We've called the
package for this class T solely to save on typing when using
it. You'll find it helpful to download the
class and examine it as we discuss it in this section; the code is
fully commented.
On initialization of the class, a static initialize()
method is called that reads in any trace configuration parameters passed
to the JVM and stores them in static variables. This only happens once,
during the static initialization of the class, and hence causes a minimal
performance hit. See the section below entitled "Is it
configurable?" for more details on this.
To minimize the footprint of the class, a lot of functionality is
reused. Essentially, there are three variations of the out()
method for stdout , and three variations of the very similar
err() method for stderr . All of these methods
invoke a method called outputMessage() , where all the hard
work is done. The outputMessage() method is defined with the
Java modifier synchronized to aid safe usage in a
multi-threaded system. First, let's look at the methods we'll be calling
directly:
The
out() method
public static void out(String txt)
public static void out(String txt, int level)
public static void out(String txt, int level, boolean
forceTime)
The
err() method
public static void err(String txt)
public static void err(String txt, int level)
public static void err(String txt, int level, boolean
forceTime)
For each of the methods above, there is also an equivalent
outln() or errln() method, which simply appends
a new line character \n to the end of txt . This
means that our class follows a convention similar to the
System.out.print() and System.out.println()
methods.
Working backwards, let's first take a look at methods numbered 3 in the
lists above. The parameters are shown in Table 1.
Table 1.
Parameters for methods numbered 3
String txt |
The trace message to output |
int level |
The importance of the trace statement, with one 1 being the
least important and Integer.MAX_VALUE being the most
important |
boolean forceTime |
If true, forces a time stamp to be output even if one is not
expected (more on this shortly) |
Redirecting trace If your tracing
statements are sending a lot of data to the console, here's a little
tip that will make that data easier to read. You can pipe
stdout and stderr from a Java program (or
indeed from any program) executed on the command line to files by
using the following syntax:
java MyProg 1>stdout.txt 2>stderr.txt
|
This technique is helpful for capturing any output from a
program.
Want to pipe the output to a file but still see that output as
it's sent to the console? Under Linux and UNIX, you can view the
output file created in this way using the tail -f
<filename> command, which will show the data from a
file as it grows dynamically. So, to view the
stdout.txt file we created above as it grows, you would
use the command:
Unfortunately, there is no default command similar to
tail for Windows. |
A method numbered 2 in the lists above calls the corresponding method
numbered 3 with a default forceTime value of
false . Similarly, a method numbered 1 calls a method numbered
2 with a default trace message level of 5 (meaning that the
message is not very important, but isn't the least important either --
remember, the value 1 has the least importance).
The reason for having 1 as the least important trace value and
Integer.MAX_VALUE as the most important is to make the
importance value unbounded, in theory. It's easy to add newer and more
important trace statements without having to do too much forward planning.
Think of it this way: if the importance levels worked the other way round
(with 1 being the most important), then you might add many statements with
trace level 1, but at some later point decide to add further statements
that were even more important than the previous batch. In such a case, you
would have to find all the existing trace statements and reduce their
trace level in order to make room for these new trace statements. That
would be a lot of unnecessary work, and the current design for our class
avoids it.
Now, let's go back to the outputMessage() method. This is
where all the trace formatting functionality is contained; it's also the
method that you will be most likely to modify should you have any personal
features to add to this class. Note that a time stamp is output for each
trace call unless implicitly suppressed (we'll discuss this in more detail
further on). If a statement happens to cover multiple lines, then margins
are set at the length of the time stamp for each subsequent line. This
makes the output text easier on the eyes.
The outputMessage() method takes the string, and, if it
will fit on the current line, simply outputs it. If that's not possible,
then the string is separated into chunks and each is output to a new line.
The size of a chunk depends on the width of the output and on any new line
characters embedded in the trace message. We'll see examples of this in
the next section.
Using Trace All methods in
the class are static, which means that we can call them directly on the
class without having to create an instance of it first. In other words,
you can use this call:
T.Trace.out("a message\n");
|
rather than this one:
T.Trace traceObject = new T.Trace();
traceObject.out("a message\n");
|
Note also that, if you'd like, you could include the statement
import T.* at the top of your class and then only call
Trace.out("a message\n"); throughout your code. This is a
design decision left up to you.
Listing 1 shows some examples of the Trace class in
use: Listing 1. Tracing errors in production
code
public double deposit(double amount, int accountNum)
{
double oldBalance, newBalance;
Context context = null;
Properties env = new Properties();
env.put(javax.naming.Context.PROVIDER_URL,
"iiop://jndiserver.ibm.com:900" );
env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.cosnaming.CNCtxFactory" );
try
{
// ***** Example 1 - start *****
T.Trace.outln("Getting initial context", 5);
context = new InitialContext(env);
T.Trace.outln("Looking up Account bean", 5);
Object ref = context.lookup("Account");
T.Trace.outln("Narrowing Object", 5);
AccountHome home = (AccountHome)
PortableRemoteObject.narrow(ref, AccountHome.class);
T.Trace.outln("Creating Account object", 5);
Account accountBean = home.create();
// ***** Example 1 - end *****
// ***** Example 2 - start *****
T.Trace.out("Getting old balance", 5);
oldBalance = accountBean.getBalance(accountNum);
T.Trace.outln(" - done", 5);
// ***** Example 2 - end *****
T.Trace.outln("Depositing money into account", 5);
accountBean.deposit(amount, accountNum);
T.Trace.outln("Getting new balance", 5);
newBalance = accountBean.getBalance(accountNum);
// ***** Example 3 - start *****
T.Trace.outln("This is a really long line of trace that " +
"serves no purpose to the code in this " +
"example but does show trace text split " +
"over multiple lines", 5);
// ***** Example 3 - end *****
// ***** Example 4 - start *****
T.Trace.outln("Account details:\n" +
"account number = " + accountNum + "\n" +
"deposit amount = " + amount + "\n" +
"old balance = " + oldBalance + "\n" +
"new balance = " + newBalance, 6);
// ***** Example 4 - end *****
}
catch (NamingException e1) {
T.Trace.errln("NamingException: " + e1); }
catch (Exception e2) {
T.Trace.errln("Exception: " + e2); }
return newBalance;
}
|
You might think that I've gone over the top with the trace statements
here, but this does give you a feel for how they would work in production
code. The tracing statements essentially give you updates on everything
that's happening in the class as it executes.
So, what does our tracer class output? Nothing -- well, nothing until
an appropriate trace level is set. By default, the trace level is 0, which
means that nothing is traced. The trace level is set with a
-D switch to the JVM, as follows:
java -DTRACE_OUT=5 -DTRACE_ERROR=5 MyClass
|
If TRACE_OUT is set to 5, then any Trace.out
statements of importance 5 or greater will be output to the console. You
can set a level for TRACE_ERROR in the same way.
With the trace level set appropriately, Example 1 in Listing 1 should
send the contents of Listing 2 to stdout . (In general,
stdout will be the command line window where the program is
running). Listing 2. stdout output from Example
1
(17:46:28) Getting initial context
(17:46:29) Looking up Account bean
(17:46:30) Narrowing Object
(17:46:31) Creating Account object
|
Obviously, the data in the parentheses is the time stamp, and the data
after it is the trace message. A time stamp will be output for every line
of trace unless the trace message does not end in a new line character, in
which case a time stamp will not be output on the next call to the method.
The class is written this way to allow the sort of logic we wrote for
Example 2 in Listing 1. First, (17:46:32) Getting old balance
will be output; once the action has completed, it will append -
done to the end of the statement, giving the output shown in
Listing 3. Listing 3. stdout output from Example
2
(17:46:32) Getting old balance - done
|
Another feature of this class that helps make the trace information
more readable is line wrapping. A particularly long line, like the one in
Example 3, will wrap automatically, as shown in Listing 4: Listing 4. stdout output from Example 3
(17:46:33) This is a really long line of trace that ser
ves no purpose to the code in this example b
ut does show trace text split over multiple
lines
|
Finally, new line characters embedded in trace statements, like the
ones in Example 4, can be used to format the output, as illustrated in
Listing 5: Listing 5. stdout output from Example
4
(17:46:34) Account details:
account number = 12345
deposit amount = 25.0
old balance = 150.0
new balance = 175.0
|
Note that the trace level for Example 4 in Listing 1 is set to 6. If we
used the command-line option -DTRACE_OUT=6 , then only the
trace statement in Listing 5 would be output, as the others all have their
trace level set to 5.
Configuring Trace You've
already seen that we can configure our class by passing in
-D -style command-line JVM options. Table 2 outlines the main
options available and explains their use.
Table 2.
Command-line JVM options for T.Trace
TRACE_OUT |
Sets the debug level for all Trace.out()
calls |
TRACE_ERROR |
Sets the debug level for all Trace.err()
calls |
TRACE_WIDTH |
Sets the trace output's width, in characters, which is very
useful for customizing the output for various window
terminals |
Performance
considerations The truth is that T.Trace
statements in your code do affect performance, even if
TRACE_OUT and TRACE_ERROR are set to 0. Every
time a trace call is made, a conditional statement is executed. If the
trace level is set to 0 (the default value), then no further processing is
done, and no trace is output. Nevertheless, the JVM still executes each
extra conditional statement, and in the end those statements add up. One
way to get around this when deploying code to a production system is to
simply comment out all the trace statements by globally searching for
T.Trace and replacing each instance with
;//T.Trace . The semicolon in the replace string will take
care of any trace statements that may be in a loop or conditional
statement. This line:
if (X == true) T.Trace.outln("X is true!");
|
becomes:
if (X == true) ;//T.Trace.outln("X is true!");
|
This code will not execute the trace call; more importantly, it will
not affect any code thereafter.
Versioning If at any point
you alter your tracing class or add some cool new functionality to it, the
best way to keep track of your changes is with versioning. This can be
essential if you come back to a project and can't remember what version of
your trace class you're working with. The tracing class in this article is
version 1.0, so if at some point you make a change or add some new
functionality, then you'll probably want to increment the version to 1.1
-- 2.0 if it's a complete rewrite.
The version number is always printed at the start of a run if tracing
is turned on, so you always know what you're working with. You can
suppress this by setting the following -D JVM option:
Wrap up Well, that brings
our tour of this handy class to a close. Put this class in all your Java
projects and you'll have a consistent, dynamic trace system -- one that is
far better than simply scattering System.out.println
statements all over your code. I have more ideas on filtering trace output
using class, package, and even method names, but that's for another day.
I'll save the exploration for you -- and please feel free to e-mail me with
your ideas. Enjoy, good luck, and good debugging.
Resources
About the
author Scott Clee
currently works as a tester for IBM's CICS product. He's been a Java
programmer for five years and has more recently focused on Java
testing in CICS, including testing the new Java ORB. He'd like to
say hello to Becky, Dave P., Barry S., Kay J., Colin B., Kathy T.,
Sharon S., Dan W., Ian D., Adam C., and everyone else on his team.
Contact Scott at Scott_Clee@uk.ibm.com. |
|
|