Implement Automatic Discovery in Your Java Code with Annotations

来源:互联网 发布:mysql 内存多大合适 编辑:程序博客网 时间:2024/05/20 09:44

If you need to start and stop various parts of code in a Java-based web system, you can implement a solution that uses a single ServletContentListener to listen for start and stop events from the container. This Listener can use the java.util.ServiceLoader to find registered classes that want to listen for these events.

That would work well enough, but what about adding a compile-time annotation processor? When you annotate a static method with@Lifecycle(LifecycleEvent.STARTUP), it'll be invoked at startup (and vice versa for SHUTDOWN). The processor generates classes and registers them for the ServiceLoader. You could leverage this same mechanism for any event-bus model: listeners are registered at compile time with annotations and then called automatically by the bus when the event is triggered. In essence, you can use annotations to automatically discover code at runtime with a ServiceLoader.

In action, the concept is as follows:

  1. You annotate methods with @EventListener (which may contain some meta-info).
  2. An annotation processor writes an EventDispatcher for each @EventListener method, including the filters required by the meta-info in the annotation.
  3. The event bus uses java.util.ServiceLoader to find EventDispatcher implementations.
  4. When EventBus.dispatch is called, any interested methods that were annotated with@EventListener are invoked.
Continuous Quality for Mobile Apps - Reduce Your Time-to-Feedback
Download Now

This article demonstrates this concept by walking through the necessary steps for building an event bus that invokes annotated listener methods without any manual registration. The discussion starts with theEventBus, moves on to the annotation processor, and ends with a usage example.

  • Post a comment
  • Print Article

Organizing Your Code

The code download for the example contains two separate IDE projects:
  • EventBus – contains the event bus and the annotation processor
  • EventBusExample – contains an example usage of the event bus

When dealing with annotation processors, you generally should turn off the "Compile on Save" (or equivalent) option in your IDE. These options have a nasty habit of deleting the classes generated by the annotation processor, leaving you scratching your head.

The following sections will explain how the code in these projects works, with snippets provided for illustration.

The Annotation and the Event

The first thing you need is an @EventListener annotation to mark methods that will listen for events. Here is an example of the EventListener annotation, which is allowed to annotate only methods. It will be discarded after the code compiles, because all the processing is done against the source code.
@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public @interface EventListener {     String name() default ".*";     Class<?> source() default Object.class; }

Because this example is an event-bus model, it is best that the listener methods receive only the events they are interested in. To help enforce that rule, a BusEventObject class contains a name that you can filter (based on the name in the @EventListener annotation). To make it easier to filter events, the normal EventObject class has an added name field. The BusEventObject also serves as a marker for events that can be dispatched through the EventBus.

public abstract class BusEventObject extends EventObject {     private final String name;     public BusEventObject(            final Object source,            final String name) {         super(source);         if(name == null || name.isEmpty()) {            throw new IllegalArgumentException("empty or null name");        }         this.name = name;    }     public String getName() {        return name;    } }

The Annotation Processor

To start writing annotation processors, you first should familiarize yourself with thejavax.annotation.processing and javax.lang.model package groups. Generally, you skip implementing the Processor interface directly, and extend the convenience abstract classjavax.annotation.processing.AbstractProcessorAbstractProcessorneeds 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 theEventListenerAnnotationProcessor, you need to register it with a services file (the compiler uses theServiceLoader as well).

  • Post a comment
  • Print Article
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. TheRoundEnvironment (which represents this round of annotation processing) will have returned theElements (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 theJavaFileObject 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 ifstatement 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 theServiceLoader 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(") {");

Putting It All Together

The EventBus project results in a single JAR file with both the compile-time and runtime code (although you could break it into two JARs). Now you need to write a sub-class of BusEventObject that can be dispatched to listeners though theEventBus. You'll also need an @EventListener method to receive instances of your new event class. Finally: you'll need a class to dispatch the events from (a source).

To verify that the @EventListener method has an EventDispatcher generated, you'll need to make sure that the compiler knows to run the EventListenerAnnotationProcessor. This process varies from one IDE to another, but simply verifying that the JAR is on the classpath or in your project libraries often is enough. In some IDEs (such as Eclipse), you need to register the annotation processor by hand. For this example, a MessageEvent class will be dispatched through the event bus:

public class MessageEvent extends BusEventObject {    private final String message;     // constructor, etc.     public String getMessage() {        return message;    } }

You'll need an @EventListener to pick up the MessageEvent objects and do something with them. Remember that you can do this in any class, as long as the annotation processor is given a chance to see the code. For this example, this code opens a JOptionPane with the message.

@EventListenerpublic static void onMessage(final MessageEvent event) {    JOptionPane.showMessageDialog(            null,            event.getMessage(),            "Message Event",            JOptionPane.INFORMATION_MESSAGE);}

0 0
原创粉丝点击