Building Annotation-Driven Configuration

December 7, 2010

In the last few years, annotation-driven libraries have been on the rise in new and even existing libraries. Rather than use configuration files such as XML or properties, libraries now make use of annotations in JDK 5 to mix the configuration directly inline with the code. This simplifies the configuration by not having to maintain a separate configuration file. The purpose of this article is to explain a brief sample of how to build an annotation-driven configuration processor. Click to continue reading.

Before I start, let me note that there are plenty of better, pre-existing ways to do this such as dependency injection libraires including CDI, Spring, Guice, etc. However, for cases where you cannot use these libraries or where you do not want the additional dependency, annotation driven configuration management may be a good use. Also note that the source code in this article are only bits and pieces and may not always compile directly. The full source code is attached at the bottom of the article.

So, what does it mean to have a annotation-driven configuration? As noted earlier, prior to annotations, most libraries relied heavily on XML files, properties files, or several other file-based alternatives. For example, you might have a generic event system that you would want to support dynamic listeners. In the past, you might use a factory to load, parse, and add the listeners. For example:

<config>
    <listeners>
        <listener>com.example.listener.MyListener</listener>
    </listeners>
</config>

This results in a separate configuration file. Further, you would only get code completion in IDEs if there was a specified and accompanying DTD or XSD. For annotation-driven configuration, you could get rid of the configuration file and use the native JDK such as:

@Listener
public class MyListener {
...
}

This drastically simplifies the code and dependencies. To add a new listener, you only need to create a class and add an annotation rather than create and manage a separate configuration file where you could easily mistype and not find out until runtime. As annotations are compile time, in most cases you are ensured compatibility at compile time rather than runtime.

So, how do you build and handle such code? The answer is actually quite simple by relying on some existing libraries for searching out annotations in existing class files. At a high level, you create an annotation processor, search the class path for any class containing a given annotation(s), and then process each class with the appropriate business logic. Let’s go back to our simple generic event/listener model by using annotations to dynamically load listener classes to a given event class.

The first step is to add the necessary libraries. The library I have used the most is called Scannotation. The Scannotation library searches the class path using another library known as javassist to read class files looking for annotations. It literally reads every class in the class path so that there is no requirement to explicitly load the class first. For example, in the old JDBC days, you typically had to reference the JDBC driver to ensure it was loaded by the JVM so that JDBC could reference it and look it up automatically. However, you are prolly asking yourself, but what if I have thousands of JARs and classes in my class path…won’t that take forever to inspect each one? The answer is yes. However, you can control which packages to ignore so that the library will ignore certain class files. By default it ignores common JVM packages such as sun, com.sun, java, etc. As a result, the scanning is actually not terrible (on the order of 1-10 seconds typically). The benefits of dynamically finding annotations drastically outweighs the performance usually, IMO.

Anyways, back to the dependency. If you are using Maven, simply add the following dependency:

<dependency>
    <groupId>net.sf.scannotation</groupId>
    <artifactId>scannotation</artifactId>
    <version>1.0.2</version>
</dependency>

Othewise, visit the Scannotation download site and include the JAR in your class path. You will also need to download the javassist library. Both JAR libraries are included in the sample projects.

The next step is to create an annotation to use when annotating the listeners. If you have never created an annotation, they are quite simple and mimic a typical Java class. Actually, under the covers, annotations are essentially special classes that implement Annotation. But that is besides the point and just a tidbit for those with a passion for knowing how things work. The following is our simple annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Listener {
    Class<? extends Event>[] events();
}

The @Target annotation is a built-in JVM annotation that specifies where the annotation may be used. This may be TYPE for classes, METHOD for methods, FIELDS for fields, PARAMETER for method parameters, etc. To specify multiple, use an array such as @Target({ ElementType.TYPE, ElementType.METHOD })

The @Retention annotation is another bulit-in JVM annotation that specifies how the JVM internals use the annotation. RUNTIME specifies that the JVM should include details about the annotation and its usage within the class file for usage at runtime such as through reflection. For our purpose, this is required so that we can reflectively discover the listeners using Scannotation. For other purposes, you may only want the annotation for compile-time purposes such as a documentation processor.

To define an annotation, you essentially create an interface, but rather than specifying interface, you specify @interface to denote an annotation. The rest of the definition is the properties of the annotation. Each property is denoted with a type (ie: Class[]) and a method name. Typically, the method name is just the property name without the get/set prefix. As annotations are constants, there are no methods to set or change a value at runtime. As a result, you can specify default values for a property such as { int priority() default 5; }

Now that we have our annotation defined, we can begin to use it to dynamically configure our listeners.

@Listener(events=MyEvent.class)
public class MyListener implements EventListener {
    public void onEvent(Event event) {
        ...
    }
}

This listener is a standard POJO that implements our built-in interface EventListener that defines the base onEvent callback event. We annotate the class with @Listener to specify that the class should be a registered listener for any MyEvent events. We could create any number of further listeners for any number of other events.

That’s not everything though. Now comes the fun part…tying it all together. Obviously, this code compiles just fine, but if run, nothing would happen as the underlying code does not know what to do with MyListener or its annotations. That’s where we have to use the scannotation libraries to load and register the listeners. The best way to typically do this is creating a factory to hide away the implementation details of the scannotation libraries. For example, for testing purposes, you could use a different factory that returned hard-coded listeners until the annotation pieces were in place. Then, you could cut over the factory to use scannotation without any other changes.

public class EventListenerFactory {
    public static List<EventListener> getListeners(Class<? extends Event> eventClass) {
        ...
    }
}

This is a standard factory pattern where we return a list of registered listeners for a specified event class. To retrieve the listeners, we must dynamically find them all. To do that, we must first create an annotation database:

import org.scannotation.*;
 
...
 
protected static Map<String, Set<String>> loadAnnotations() {
    // create database
    AnnotationDB database = new AnnotationDB();
 
    // add any packages to ignore
    database.addIgnoredPackage(...);
 
    // specify where to search for annotations
    database.setScanClassAnnotations(true);
    database.setScanFieldAnnotations(true);
    database.setScanMethodAnnotations(true);
 
    // use the base class path (ie: java.class.path) to inspect all classes
    // excluding those in ignored classes
    //
    // NOTE: for application servers like Glassfish that use complex class
    // paths for web application paths, system paths, etc, this util class
    // will not work and you must explicitly specify those class paths.
    database.scanArchives(ClasspathUrlFinder.findClassPaths());
 
    // return the mapping of annotation classes to list of found classes
    return database.getAnnotationIndex();
}

The first part is creating the AnnotationDB instance. By default, Scannotation ignores the standard sun, java, com.sun, etc packages, but you can add any other ignored packages as necessary. Then, you can set where to scan for annotations: classes, fields, methods, etc. There are a variety of other methods you can use…reference the javadoc for scannotation.

Once the processor is configured, you need to scan the class path for annotations. You pass in the list of JARs or class files that you want to scan. The ClasspathUrlFinder class is a utility class that provides various helper methods to find all the classes. Note that running in a application server often times has more than just the java.class.path such as the web application path. As such, you will need to lookup those paths as well before scanning.

Now after the path has been scanned, you can get an index that maps annotation classes to the set of classes that contain the annotation (ie: Map>). Note that the set of classes just have to contain the annotation at any point (type level, method, fields, etc). So, you have to introspect each class to actually obtain where and how the annotation is used.

If we go back to the factory class for obtaining the listeners, we can put everything together. I typically add a public method such as

public static void registerListener(Class<? extends Event> clazz, EventListener listener) {
    List<EventListener> listeners = eventListeners.get(clazz);
    if (listeners == null) {
        listeners = new ArrayList<EventListener>();
        eventListeners.put(clazz, listeners);
    }
 
    listeners.add(listener);
}

This allows the internal load method to add listeners as well as external libraries to manually add listeners without using the annotations. Now we finish off the loading
using the add method and the load methods:

protected static void load() {
    if (!loaded) {
 
        // set as loaded
        loaded = true;
 
        // load annotations and get index of annotations
        Map<String, Set<String>> index = loadAnnotations();
 
        // get list of classes using the Listener annotation
        Set<String> classes = index.get(Listener.class.getName());
 
        // process each class registering listeners
        for (String className : classes) {
 
            // look the class and create a default instance
            Class<?> clazz = Class.forName(className);
            EventListener eventListener = (EventListener) clazz.newInstance();
 
            // obtain the Listener annotation
            Listener listener = clazz.getAnnotation(Listener.class);
            if (listener != null) {
 
                // get the list of declared events
                Class<? extends Event>[] eventClasses = listener.events();
 
                // register each listener
                for (Class<? extends Event> eventClass : eventClasses) {
                    registerListener(eventClass, eventListener);
                }
            }
        }
    }
}

This should be pretty self explanatory, but basically it uses the annotation index to look up the annotated classes and for each class it registers the events. Essentially, that is all it is and you get easy to use annotation-driven configuration. Want a new listener? Just create a class and annotate and place on the class path. The cool thing is that you can include the class in a separate JAR, just as long as it is on the class path.

You may be asking yourself, but who invokes the load method to start the process? I typically do this in the load method. Since the load method only runs once, we can invoke it each time (note: if you are running multiple threads, you will need to synchronize the load process to ensure it truly only runs once).

public static List<EventListener> getListeners(Class<? extends Event> eventClass) {
    load();
    return eventListeners.get(eventClass);
}

We can utilize that method to get the listeners whenever an event is fired. For example:

public void fireEvent(Event event) {
    List<EventListener> listeners =
        EventListenerFactory.getListeners(event.getClass());
 
    for (EventListener listener : listeners) {
        listener.onEvent(event);
    }
}

That’s it! However, we can make this even better, believe it or not. You may have noticed that each listener is tightly coupled to the EventListener interface. Although this works and is arguably not that bad, we can get rid of that coupling. We do that by placing the annotation at the method level. Prior to that though, we must change our Listener annotation to be supported on methods:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Listener {
    Class<? extends Event>[] events();
}

Now we can place our Listener annotation accordingly:

public class MyListener {
    @Listener(events=MyEvent.class)
    public void handleMyEvent(MyEvent event) {
        ...
    }
}

Notice that this class has no direct dependencies other than the event class. The annotation can be argued as being a dependency, but it is not used for logic…it is more of a declarative idiom. Further, we could have multiple methods in the class, each with a separate annotation. To handle this in the factory is quite simple. Rather than looking at the class for the annotation, we search for each declared method in the class.

protected static void load() {
    if (!loaded) {
 
        // set as loaded
        loaded = true;
 
        // load annotations and get index of annotations
        Map<String, Set<String>> index = loadAnnotations();
 
        // get list of classes using the Listener annotation
        Set<String> classes = index.get(Listener.class.getName());
 
        // process each class registering listeners
        for (String className : classes) {
 
            // look the class and create a default instance
            Class<?> clazz = Class.forName(className);
            Object eventListener = clazz.newInstance();
 
            // search each declared method (public, protected, etc)
            for (Method method : clazz.getDeclaredMethods()) {
                // obtain the Listener annotation
                Listener listener = method.getAnnotation(Listener.class);
                if (listener != null) {
 
                    // get the list of declared events
                    Class<? extends Event>[] eventClasses = listener.events();
 
                    // register each listener
                    for (Class<? extends Event> eventClass : eventClasses) {
 
                        // use a ListenerHandler to group the instance and method
                        registerListener(eventClass, new ListenerHandler(eventListener, method));
                    }
                }
            }
        }
    }
}

This is basically the same but we search the methods. Note that we use getDeclaredMethods rather than getMethods so that we only get the classes in the file and include all public, protected, etc files. To make this complete, it is best to run this against all parent classes as well. For each method then, we create a ListenerHandler to group the instance and the method. To invoke the event (such as a fire event), just invoke the handler:

public class ListenerHandler {
    ...
 
    public void invoke(Event event) {
        this.method.invoke(this.instance, event);
    }
}

Now we get loose coupling and can easily add listeners/events without touching a line of existing code or updating a configuration file.

Another neat idea I have started using this methodology for is annotating annotations to dynamically load other types of annotations. For example, we could allow other annotations to be defined and then annotate those annotations with the @Listener annotation. CDI makes use of this model quite extensively.

@Listener(events=MyEvent.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ModeListener {
    int mode();
}

Note that we must also update the Listener annotation to specify annotation type targets:

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Listener {
    Class<? extends Event>[] events();
}

Now, we can annotate our methods with @ModeListener rather than @Listener. This allows the method to be custom annotated with custom properties such as the mode property. To support this functionality in the factory is just a matter of loading all classes that are annotations and use the @Listener annotation and then search all methods that use those annotations.

protected void load() {
    // load annotations and get index of annotations
    Map<String, Set<String>> index = loadAnnotations();
 
    // get list of classes using the Listener annotation
    Set<String> annotationClasses = index.get(Listener.class.getName());
 
    // process each class searching for annotations
    for (String annotationClassName : annotationClasses) {
 
        // look up the class and verify annotation
        Class<? extends Annotation> annotationClass = 
            (Class<? extends Annotation>) Class.forName(annotationClassName);
        if (annotationClass.isAnnotation()) {
 
            // get events for the annotation
            Listener listener = 
                annotationClass.getAnnotation(Listener.class);
            Class<? extends Event> eventClasses[] = listener.events();
 
            // load each class using the custom annotation
            Set<String> classes = index.get(annotationClassName);
            for (String className : classes) {
 
                // look up class and create default instance
                Class<?> clazz = Class.forName(className);
                Object eventListener = clazz.newInstance();
 
                // search each declared method (public, protected, etc)
                for (Method method : clazz.getDeclaredMethods()) {
                    // obtain and verify the custom annotation
                    Annotation annotation = 
                        method.getAnnotation(annotationClass);
                    if (annotation != null) {
 
                        // register each listener
                        for (Class<? extends Event> eventClass : eventClasses) {
 
                            // use a ListenerHandler to group the instance and method
                            registerListener(eventClass, new ListenerHandler(eventListener, method));
                        }
                    }
                }
            }
        }
    }
}

Essentially what this example does is allows the developer to create custom annotations to group a set of common events for example so that you can specify just the custom annotation and its metadata without having to worry about each associated event. Now, if you want to update the event list, you just update the annotation rather than each place the annotation is used.

Hopefully this article helps to showcase some of the power of annotation-driven configuration as well as the power of the scannotation library for processing annotations at runtime dynamically. I have used this methodology in several projects and it vastly simplies updating code by having true decoupling of code. This is the precise reason why CDI and Spring have become so prominent and successful.

Leave a Reply