October 23, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

Implement Automatic Discovery in Your Java Code with Annotations

  • December 16, 2009
  • By Jason Morris
  • Send Email »
  • More Articles »

The Annotation Processor

To start writing annotation processors, you first should familiarize yourself with the javax.annotation.processing and javax.lang.model package groups. Generally, you skip implementing the Processor interface directly, and extend the convenience abstract class javax.annotation.processing.AbstractProcessor. AbstractProcessor needs some information about your implementation, which you provide using annotations. The EventListenerAnnotationProcessor in the example code declaration looks like this:
@SupportedSourceVersion(SourceVersion.RELEASE_5)
@SupportedAnnotationTypes(EventListenerAnnotationProcessor.ANNOTATION_TYPE)
public class EventListenerAnnotationProcessor extends AbstractProcessor {

The @SupportedSourceVersion tells AbstractProcessor that you want only source files written for Java 5 or higher; while the @SupportedAnnotationTypes tells it which annotations you're interested in (EventListener.class.getName() won't work as an annotation value, because the compiler can't evaluate such expressions).

public static final String ANNOTATION_TYPE = "eventbus.EventListener";

For simplicity, the annotation processor is broken into two main classes (EventListenerAnnotationProcessor and EventDispatcherGenerator) and a generic utility class (ServiceRegistration). In order for the compiler's annotation tool to execute the EventListenerAnnotationProcessor, you need to register it with a services file (the compiler uses the ServiceLoader as well).

eventbus.processor.EventListenerAnnotationProcessor

The Services registration file (META-INF/services/javax.annotation.processing.Processor) is named according to the interface that ServiceLoader must find implementations of.

The first action of the EventListenerAnnotationProcessor.process() method is to find all the @EventListener methods being compiled in this round.

final Elements elements = processingEnv.getElementUtils();
final TypeElement annotation = elements.getTypeElement(ANNOTATION_TYPE);
final Set<? extends Element> methods =
        roundEnv.getElementsAnnotatedWith(annotation);

Element objects are much like reflection objects for the compiler and annotation processor. TypeElement is like Class, while ExecutableElement is similar to Constructor or Method. The RoundEnvironment (which represents this round of annotation processing) will have returned the Elements (in this case, methods) that are annotated by an @EventListener annotation.

The EventDispatcherGenerator

The EventDispatcherGenerator is a very simple code generator. You may prefer to use a template engine (such as FreeMarker or Velocity) to generate your source code, but the code for this example is written out with a PrintWriter. Each ExecutableElement representing an @EventListener annotated method gets passed to the EventDispatcherGenerator.generate, which will write out the source code for an EventDispatcher.
for(final Element m : methods) {
    // ensure that the element is a method
    if(m.getKind() == ElementKind.METHOD) {
        final ExecutableElement method = (ExecutableElement)m;
        results.add(generator.generate(method));
    }
}

The EventDispatcherGenerator will need to generate a Java source file for each of these methods. An annotation processor uses the Filer object given by the ProcessingEnvironment to create source files to which it can write code.

final JavaFileObject file = processingEnvironment.getFiler().createSourceFile(
        className, // ie: com.mydomain.example.OnMessageDispatcher
        method);     // ie: com.mydomain.example.Listener.onMessage(MessageEvent)

The given ExecutableElement for the Filer in this example represents the annotated method (the second argument in createSourceFile). This tells the environment that you're generating source code related to that method, which is not required but helpful nonetheless. The code then uses the JavaFileObject to open a Writer and start generating source code.

final Writer writer = file.openWriter();
final PrintWriter pw = new PrintWriter(writer);
pw.append("package ").append(packageName).println(';');

The values specified in the @EventListener annotation for the method are used to generate an if statement that will filter BusEventObjects before invoking the annotated method. The EventDispatcherGenerator writes an if statement into the source code that will decide whether or not to dispatch the event object to the @EventListener method.

public final class EventBus {
 
    private static final EventDispatcher[] DISPATCHERS;
 
    static {
        final ServiceLoader<EventDispatcher> loader =
                ServiceLoader.load(EventDispatcher.class);
 
        final List<EventDispatcher> list = new ArrayList<EventDispatcher>();
 
        for(final EventDispatcher dispatcher : loader) {
            list.add(dispatcher);
        }
 
        DISPATCHERS = list.toArray(new EventDispatcher[list.size()]);
    }
 
    private EventBus() {
    }
 
    public static void dispatch(final BusEventObject object) {
        if(object == null) {
            throw new IllegalArgumentException("null event object");
        }
 
        for(final EventDispatcher dispatcher : DISPATCHERS) {
            dispatcher.dispatch(object);
        }
    }
 
    public static interface EventDispatcher {
 
        void dispatch(BusEventObject object);
 
    }
}

Registering the EventDispatcher

The final task for generating the EventDispatchers is to list all of them in a services file, so that the ServiceLoader can find them when EventBus is initialized. There are a few tricks in this process. The annotation processor gives you a list of only the methods that are included in the current compile. Given that developers don't generally compile their entire code-base at once, the processor code will need to keep track of both methods that have already been compiled and those being compiled at the time. This is the job of the ServiceRegistration class.

First, you tell the ServiceRegistration to read any existing service files for EventDispatchers in the source path or class output directory. Next, you add the newly compiled EventDispatcher classes, and then write the new service file out to the class output directory.

final AnnotationHelper annotation = new AnnotationHelper(
                method,
                EventListenerAnnotationProcessor.ANNOTATION_TYPE,
                environment.getElementUtils());
 
final String nameFilter = (String)annotation.getValue("name");
final TypeElement sourceFilter = (TypeElement)environment.getTypeUtils().
        asElement((TypeMirror)annotation.getValue("source"));
 
pw.println("\tpublic void dispatch(eventbus.BusEventObject event) {");
 
pw.print("\t\tif(event instanceof ");
pw.println(eventType.getQualifiedName());
pw.println("\t\t\t\t&& nameFilter.matcher(event.getName()).matches()");
pw.append("\t\t\t\t&& event.getSource() instanceof ").
        append(sourceFilter.getQualifiedName()).println(") {");


Tags: open source, Google, Android, fragmentation, Device



Page 2 of 3



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel