Peeking Inside the Box: Attribute-Oriented Programming with Java 1.5, Part 2 作者:Don Schw

类别:Java 点击:0 评论:0 推荐:

Peeking Inside the Box: Attribute-Oriented Programming with Java 1.5

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 StackTraceElements, 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 StackTraceElements 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 StackTraceElements 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 StackTraceElements.

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.

本文地址:http://com.8s8s.com/it/it15801.htm