
Peeking Inside the Box: Attribute-Oriented Programming with Java 1.5, Part 2
by Don Schwarz07/21/2004
In the previous article in this series, "Peeking Inside the Box, Part 1," I introduced the concepts of Attribute-Oriented Programming, Java 1.5 annotations, and bytecode instrumentation. I used these concepts to provide a JStatusBar
GUI component that
reports on the progress of an application without any explicit code.
In this article I will introduce a completely different implementation
of the same JStatusBar
that uses thread sampling rather
than bytecode instrumentation. I will then combine the two practices
to develop a solution that has the best features of each.
In the previous article, I also defined a new annotation, @Status
, which I used throughout my code to associate methods with user-readable status messages. For example:
@Status("Connecting to database")
public void connectToDB (String url) {
...
}
Exception Handling
As discussed in the previous article, I may want to write additional code which uses the @Status
annotations for a different purpose. Let's consider the additional requirement that our application must catch all unhandled exceptions and display them to the user. Rather than providing a Java stack trace, however, it should only display methods that have a @Status
annotation, and it should not show any code artifacts (class or method names, line numbers, etc.).
For example, consider the following stack trace:
java.lang.RuntimeException: Could not load data for symbol IBM
at boxpeeking.code.YourCode.loadData(Unknown Source)
at boxpeeking.code.YourCode.go(Unknown Source)
at boxpeeking.yourcode.ui.Main+2.run(Unknown Source)
at java.lang.Thread.run(Thread.java:566)
Caused by: java.lang.RuntimeException: Timed out
at boxpeeking.code.YourCode.connectToDB(Unknown Source)
... 4 more
This should result in the GUI pop-up box in Figure 1, assuming that YourCode.loadData()
,
YourCode.go()
, and YourCode.connectToDB()
all have @Status
annotations. Note that the order of the exceptions is reversed so that the user is given the most detailed information first.
Figure 1. Stack trace displayed in an error dialog
Implementing this will require a few modifications to my
existing code. First, to ensure that the @Status
annotations can be seen at run time, I'll need to upgrade my @Retention
again, to
@Retention(RetentionPolicy.RUNTIME)
. Remember,
@Retention
controls when the JVM is free to discard
annotation information. This setting means that annotations will
not only be inserted into the bytecode by the compiler, but will
also be accessible through reflection with the new
Method.getAnnotation(Class)
method.
Now, I'll need to arrange to receive notification of any
exceptions that our code does not explicitly handle.
As of Java 1.4, the best way to handle uncaught exceptions for a
particular thread was to subclass ThreadGroup
and add
your new thread to a ThreadGroup
of that type. However,
Java 1.5 provides additional functionality here. I can define an
instance of the UncaughtExceptionHandler
interface and
register it for either a specific thread, or for all threads.
Note: Registering for a specific exception would probably be preferable in this case, but there was a bug in Java 1.5.0beta1 (#4986764) that prevented this from working. Setting a handler for all threads, however, does work, so I've done this as a workaround.
Now that we have a way to intercept the uncaught exceptions, they need
to be reported to the user. In a GUI application, this is typically
done by popping up a modal dialog box containing the entire stack
trace, or perhaps simply the message. In this case, I want to display
the message if any exception is thrown, but I want to provide a stack
of @Status
descriptions instead of class and method names.
To do this, I simply iterate through the
Thread
's array of StackTraceElement
s, find
the associated java.lang.reflect.Method
object for each
frame, and query it for a list of stack annotations. Unfortunately,
only method names are provided, not method signatures, so this
technique will not support overloaded methods with the same name (and
different @Status
annotations).
Example code for this approach can be found in the /code/04_exceptions directory in the peekinginside-pt2.tar.gz file (see References below).
Sampling
I now have a way to turn an array of StackTraceElement
s
into a stack of @Status
annotations. This is actually more useful than it may seem. Another new feature in Java 1.5, thread introspection, gives us a way to get extract array of StackTraceElement
s from a currently running thread. With these two pieces of the puzzle, I can construct an alternate implementation of
the JStatusBar
from Part 1. Instead of receiving notifications
when each method call takes place, the StatusManager
can simply start up an additional
thread responsible for grabbing a stack trace at regular intervals and its state at each step. As long as this interval is short enough that the user does not perceive the update delay, this is just as good.
Here's the code behind our "sampler" thread, which tracks the progress of another thread:
class StatusSampler implements Runnable
{
private Thread watchThread;
public StatusSampler (Thread watchThread)
{
this.watchThread = watchThread;
}
public void run ()
{
while (watchThread.isAlive()) {
// Get stack trace from the Thread.
StackTraceElement[] stackTrace =
watchThread.getStackTrace();
// Extract Status msgs from stack trace.
List<Status> statusList =
StatusFinder.getStatus(stackTrace);
Collections.reverse(statusList);
// Build a state from Status msgs.
StatusState state = new StatusState();
for (Status s : statusList) {
String message = s.value();
state.push(message);
}
// Update the current state.
StatusManager.setState(watchThread,
state);
// Sleep until the next cycle.
try {
Thread.sleep(SAMPLING_DELAY);
} catch (InterruptedException ex) {}
}
// reset state
StatusManager.setState(watchThread,
new StatusState());
}
}
Compared to adding method calls, either manually or via
instrumentation, sampling is much less invasive. I didn't need to
change any build processes or command-line arguments, or modify my
start-up procedure at all. It also allowed me to control how much
overhead is incurred simply by adjusting the SAMPLING_DELAY
.
Unfortunately, there is no explicit callback when a method call
begins or ends. Other than the latency of the status updates, there's
no reason that this code would need to receive callbacks at the moment.
However, in the future, I could add additional code to track the exact
run time of each method. It would be impossible to do this accurately
by examining StackTraceElement
s.
Example code to implement JStatusBar
by means of thread
sampling can be found in the /code/05_sampling directory of
the peekinginside-pt2.tar.gz file (see
References).
Instrumenting Bytecode During Execution
By combining the sampling approach with instrumentation, I was able to
come up with a final implementation that provides the best features of each.
Sampling can be used by default, but methods that the application
appears to be spending the most time in can be instrumented individually.
This implementation will not install
a ClassTransformer
at all, but instead will instrument
methods one at a time in response to the data collected during sampling.
To accomplish this, I'll create a new class,
InstrumentationManager
, which can be used to instrument
and un-instrument individual methods. This will use the new
Instrumentation.redefineClasses
method to modify classes
on the fly, while the code is executing. The StatusSampler
thread, added in the previous section, will now have the additional
responsibility of adding any @Status
methods that it "sees"
to a collection. It will periodically pop off the worst offenders and
feed them to the InstrumentationManager
to be instrumented.
This allows the application to track each method's start and end times more precisely.
One of the problems with the sampling approach discussed earlier is that it can't
distinguish between methods that take a long time to run and methods
that are called many times in a tight loop. Since instrumentation
is adding a fixed amount of overhead to every method call, it would be
useful to skip over methods that are called very frequently.
Luckily, we can resolve this with instrumentation. In addition to
simply updating the StatusManager
, we will also maintain
a running count of the number of times each instrumented method has been
called. If this count exceeds some threshold (meaning that it is too
expensive to maintain information for that method), the sampling
thread can permanently undo the instrumentation of that method.
Ideally, I would have liked to store the call count of each method in
new fields added to the class during instrumentation. Unfortunately,
the class transformation mechanism added in Java 1.5 does not allow
this; no fields can be added or removed. Instead, I've stored this
information in a static map of Method
objects in the new
CallCounter
class.
The code for this hybrid approach can be found in the /code/06_dynamic directory of the example code.
Summary
Figure 2 presents a matrix showing the features and costs associated with each of the examples that I've provided.
Figure 2. Analysis of instrumentation approaches
As you can see, the Dynamic version is a good compromise between the other solutions. Like all of the examples that used instrumentation, it provides explicit callbacks when a method starts or stops, so that your application can track exact run times and provide immediate feedback to the user. However, it is also able to un-instrument methods that are called too often, so it does not suffer from the same performance problems that the other instrumentation solutions do. There are no compile-time steps involved, and it does not add any additional work to the class-loading process.
Future Directions
There are a number of additional features that could be added to make
this project more practical. Perhaps the most useful feature would be
dynamic status messages. The new java.util.Formatter
class could be used to provide printf
-like pattern
substitution into the @Status
messages. For example, an
annotation of @Status("Connecting to %s")
in our
connectToDB(String url)
method could actually report the
URL as part of the message.
With source-code instrumentation, this would be trivial, as the
Formatter.format
method I would like to call uses
variable arguments (more black magic added in Java 1.5). The
instrumented version would look something like this:
public void connectToDB (String url) {
Formatter f = new Formatter();
String message = f.format("Connecting to %s", url);
StatusManager.push(message);
try {
...
} finally {
StatusManager.pop();
}
}
Unfortunately, this black magic is implemented entirely in the
compiler. In bytecode, Formatter.format
takes an
Object[]
, and the compiler explicitly adds code to box
each of the primitive types and assemble the array. Until BCEL
catches up, I would've had to re-implement this logic if I wanted to
use bytecode instrumentation for this task.
Since this would only work for instrumentation (where the method arguments are available) and not for sampling, you may want to instrument these methods at startup, or at least have the dynamic implementation be biased towards instrumentation for any methods with substitution patterns in the message.
You could also track the start times of each instrumented method call so that you could more accurately report the running times of each. You could even keep historical statistics on these times and use them to seed a real progress bar (instead of the indeterminate version I used). This capability would give you a good reason to instrument methods one at a time, since the overhead involved in tracking any individual method would be much more significant.
You could add a "debug" mode for the progress bar, which reports on all
method calls that show up in sampling, regardless of whether they have a
@Status
annotation. This would prove invaluable for any
developers who need to debug a deadlock or performance issue "in the field." In fact, Java 1.5 also provides a programmatic API to its
deadlock detection, and this could be used to make the progress bar
turn red if the application locks up.
There's probably also a market for a "fully baked" version of the
annotation-based instrumentation framework that I've built in this
article. A single tool that allowed bytecode instrumentation at
compile time (via an Ant task), startup time (with a
ClassTransformer
), and during execution (using
Instrumentation
) would no doubt be invaluable for a few
other projects.
Conclusions
As you can see by these few examples, meta-programming can be a very
powerful technique. Reporting on the progress of long-running
operations is just one possible application of this technique, and our
JStatusBar
is just one medium for communicating this
information. As we've seen, many of the new features in Java 1.5
provide enhanced support for meta-programming. In particular, the
combination of annotations and run-time instrumentation provides for a
very dynamic form of attribute-oriented programming. These techniques
can be used to go far beyond what existing frameworks like XDoclet
provide.
References
- Sample code for this article
Don Schwarz is a Java developer for a large investment bank who specializes in metaprogramming and language integration.
Return to ONJava.com.
