sixlegs.com / blog / java / async-annotations.html

Root Beer Logo Root Beer

Chris Nokleberg's Fizzy Weblog

July 2004
Su M Tu W Th F Sa
        1 2
4 5 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Previous  |  Next  |  More...
#  Transparent asynchronous methods using annotations

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:

  • JDK 1.5 annotations are now fully supported by CGLIB/ASM, so finding methods with the @Async annotation is pretty straightforward.
  • ASM is event-based, and sometimes transformations can be a little harder than with a tree-based package. In this case, however, it makes it very easy to shunt off the bytecode into a renamed method, just by returning a different visitor.
  • CGLIB does a lot more bytecode emitting than your average library, so over time a we've developed a number of useful helper methods. One example is the call to 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!

[Powered By FreeMarker]  [Valid Atom 1.0]  [Weblog Commenting by HaloScan.com]