|
I enjoyed the recent ONJava.com article on using JDK 1.5 annotations in conjunction with bytecode transformation. Seeing how Don was nice enough to mention CGLIB as a possible implementation alternative, I figured I should give an example.
First off, I'd like to clarify that CGLIB is not really in the same genre as ASM, Serp, and BCEL. For one thing, CGLIB is totally dependent on ASM to do the actual bytecode parsing and writing. ASM is a great library--very fast and very small. But one thing it does not have is a lot of utility methods for emitting bytecode, or a framework for doing transformations. CGLIB adds these things and also uses them to provide the high-level functionality it is best known for, such as Enhancer.
Instead of reimplementing the status bar example, I thought I'd try
something which requires a bit more hoop-jumping in the bytecode
department. The goal is to add a new @Async method
annotation that when applied to a void method will
transparently cause the body of the method to be executed in a separate
thread. Here is the annotation definition:
import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.CLASS) public @interface Async { }
This annotation applies only to methods, and is retained in the class files but is not available at runtime. FWIW this is my first use of JDK 1.5 so let me know if I'm setting any bad examples here.
Our sample class that uses the annotation is called Sleeper:
public class Sleeper { public void sleep(int ms) { System.err.println(Thread.currentThread()); try { Thread.sleep(ms); } catch (InterruptedException e) { throw new RuntimeException(e); } } @Async public void sleepAsync(int ms) { sleep(ms); } }
Here we have defined two methods, sleep and sleepAsync.
Since sleepAsync delegates to sleep, they should both
do exactly the same thing--sleep the given number of milliseconds. That is, of course,
unless our @Async annotation is working correctly, in which case
calling sleepAsync will actually fork a separate thread.
To test this we only have to call either method a few times in a row. The normal synchronous method should obviously block on the call to sleep, and so each call will run one after the other. The asynchronous method, however, will return immediately, and all of the sleeping should happen in parallel.
The first step in our implementation is to implement an Ant
task to process our class files. This is very easy to provide by extending
the AbstractTransformTask from CGLIB. All we do here
is extend two protected methods: one to declare which attributes
we need to pass through our transformer, and the other to return the actual
ClassTransformer
itself, which we will define later.
import net.sf.cglib.transform.ClassTransformer; import net.sf.cglib.transform.AbstractTransformTask; import org.objectweb.asm.Attribute; import org.objectweb.asm.attrs.*; public class AsyncTask extends AbstractTransformTask { private static final Attribute[] ATTRIBUTES = { new RuntimeInvisibleAnnotations(), /* ... a bunch of other attribute prototypes omitted ... */ }; protected Attribute[] attributes() { return ATTRIBUTES; } protected ClassTransformer getClassTransformer(String name) { return new AsyncTransformer(); } }
Saving the hardest for last, the core concept of our bytecode
transformer is to move the body of the asynchronous method into a new method. The original method body is then replaced with
bytecode which creates a new thread. The thread's
run method will invoke the original code via reflection.
The implementation of the AsyncTransformer I will leave as an exercise for the reader.
Ha ha, just kidding. Here it is. A few items of interest:
@Async annotation is pretty straightforward.create_arg_array,
which pushes an Object[] onto the stack with all arguments to the current method, wrapping as necessary.The entire mini-project is available here (under 10K). JDK 1.5 is kind
of a moving target, so it requires CGLIB and ASM from CVS.
If everything is working correctly then running ant test should produce output similar to the following:
[junit] Starting normal run... [junit] Thread[main,5,main] [junit] Thread[main,5,main] [junit] Thread[main,5,main] [junit] Thread[main,5,main] [junit] Thread[main,5,main] [junit] Took 2514 ms [junit] Starting asynchronous run... [junit] Thread[Thread-0,5,main] [junit] Thread[Thread-1,5,main] [junit] Thread[Thread-2,5,main] [junit] Thread[Thread-3,5,main] [junit] Thread[Thread-4,5,main] [junit] Took 6 ms
Ta Da!