JavaImplement Automatic Discovery in Your Java Code with Annotations

Implement Automatic Discovery in Your Java Code with Annotations

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

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.

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 the EventBus, moves on to the annotation processor, and ends with a usage example.

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 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(“ttif(event instanceof “);
pw.println(eventType.getQualifiedName());
pw.println(“tttt&& nameFilter.matcher(event.getName()).matches()”);
pw.append(“tttt&& 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 the EventBus. 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.

@EventListener
public static void onMessage(final MessageEvent event) {
JOptionPane.showMessageDialog(
null,
event.getMessage(),
“Message Event”,
JOptionPane.INFORMATION_MESSAGE);
}

This example listener MessageListener is a catch-all listener that will receive all MessageEvent objects dispatched through the event bus.

The only thing left to do is dispatch a MessageEvent through the EventBus:

EventBus.dispatch(new MessageEvent(
this,
“message”,
“Hello World!”));

The MessageEvent constructor takes the source of the event, the event name, and the message. This event will go to any number of methods annotated by @EventListener and accepting a MessageEvent as their parameter.


Where’s the Output?

When you’ve compiled the code, take a look at the build directory. Next to each class file with an @EventListener method, there should be a *EventDispatcher Java source file and the resulting class file. If those files aren’t there, make sure that you’ve set up your build environment so that the EventListenerAnnotationProcessor is visible to the compiler and annotation processing tools (refer back to the “Organizing Your Code” section).

Hopefully, you’ll find that using annotations with the ServiceLoader will make your life easier.


Code Download



For Further Reading



About the Author






Jason Morris is the IS architect for a company named Travelstart Services. When he’s not coding or coming up with new ways to tackle development problems, he’s working on his lightweight O/R mapping layer: EoD SQL; or blogging technical ideas and rants on lemnik.wordpress.com. Reach him at jason.morris@travelstart.net.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories