featured image Java Annotation Processors - Creating Configurations

August 06, 2018 / by Sebastian Sdorra / In Software Craftsmanship

Java Annotation Processors - Creating Configurations

+++ If you want to read the post in German, you can download the original article, published in JAVA PRO 02/2018. +++

In the first blog post of this series we have learned how to write, register and use a simple Annotation Processor. In this part we will take a closer look at the creation of configurations.

Generating configuration files

In the second section, we would like to focus on generating configuration files for a simple plugin library. To do this, we will write an annotation processor that exports all classes which are annotated with an @Extension annotation to an XML file. In addition to the full name of the class, the Javadoc for the class is also written to the XML file. Additionally, we will write a class that allows us to read these files from the classpath.

It is also possible to find all classes with an @Extension annotation without using an annotation processor. However, to do this, you have to open all elements of the classpath (folders and Jar files), load each class and check with Reflection to see if the class has the annotation you are looking for. This method is a lot more laborious, more prone to errors, and significantly slower.

The Extension annotation

@Documented
@Target(ElementType.TYPE)
public @interface Extension {
}

The Extension annotation is very similar to the Log annotation from the first section, apart from the Documented annotation. @Documented ensures that our annotation shows up in the Javadoc of the annotated class.

The extension annotation processor

The ExtensionProcessor first compiles all classes that are annotated with our Extension annotation into a set:

Set<ExtensionDescriptor> descriptors = new LinkedHashSet<>();
for ( TypeElement annotation : annotations ) {
    for ( Element extension : roundEnv.getElementsAnnotatedWith(annotation) ) {
        ExtensionDescriptor descriptor = createDescriptor(extension);
        descriptors.add(descriptor);
    }
}

The createDescriptor method saves the name and the Javadoc of the annotated class in its own class, called ExtensionDescriptor. The name can be queried via the element type: extension.asType().toString()

The JavaDoc of the class can be accessed via the Elements of the ProcessingEnvironment: processingEnv.getElementUtils().getDocComment(extension).trim()

Once we have collected all of the extensions, we can write our XML file. In order for our XML file to be accessible in the classpath, it must be saved to the correct directory. The correct directory can be determined using the Filer class of the ProcessingEnvironment:

Filer filer = processingEnv.getFiler();
FileObject fileObject = filer.getResource(StandardLocation.CLASS_OUTPUT, "", "extensions.xml");
File extensionsFile = new File(fileObject.toUri());

Now we just need to populate the Extensions file with content. To do this, we create a Wrapper class for our ExtensionDescriptor class and annotate both with JAXB annotations. Then, we can write the Extensions file with the help of JAXB: JAXB.marshal(new ExtensionDescriptorWrapper(descriptors), file);

With the ExtensionProcessor, we now have all we need to save all classes with an Extension annotation in one file during compilation. The result should look something like this:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<spl-extensions>
    <extensions>
        <className>com.cloudogu.blog.AhoiService</className>
        <description>Says ahoi to someone.</description>
    </extensions>
    <extensions>
        <className>com.cloudogu.blog.HelloService</className>
        <description>Says hello to someone.</description>
    </extensions>
</spl-extensions>

This file should be located in the same directory as the compiled classes (in Maven: target/classes).

Extension Util

To read out the extensions during runtime, a simple helper class can be written:

public static List<ExtensionDescriptor> getExtensions() throws IOException {
    List<ExtensionDescriptor> descriptors = new ArrayList<>();
    Enumeration<URL> extensionFiles = Thread.currentThread().getContextClassLoader().getResources(LOCATION);
    while (extensionFiles.hasMoreElements()) {
        URL extensionFile = extensionFiles.nextElement();
        ExtensionDescriptorWrapper extensionDescriptorWrapper = JAXB.unmarshal(extensionFile, ExtensionDescriptorWrapper.class);
        descriptors.addAll(extensionDescriptorWrapper.getExtensions());
    }
    return descriptors;
}

Using this method, all of the Extension XML files in the classpath can be found. Moreover, all classes annotated with an Extension annotation are saved to a list. Since we are using the ContextClassLoader of the thread, it is possible that our Extension XML files are located in different JAR files.

If we now wish to export all of the Extension classes of our application, we can use the following code:

for (ExtensionDescriptor descriptor : Extensions.getExtensions()) {
    System.out.println(descriptor);
}

The entire example can be found under part-2 of the GitHub repository.

Examples from the open-source world

A prominent example of an annotation processor that generates configuration files is the META-INF/services generator from Kohsuke Kawaguchi, which can generate a configuration for the Java 6 ServiceLoader from a MetaInfServices annotation.

Another example is the plugin framework of SCM-Manager 2.0.0. In the first version of SCM-Manager, classpath scanning was still used to find extensions. Switching to annotation processors drastically reduced the boot time for SCM-Manager 2.

Conclusion

In this second blog post of the series on Java annotation processors we focused on creating configuration files as well as the extension annotation processor. The third and last part we will show how code can be generated with annotation processors.


Sebastian Sdorra
Sebastian Sdorra

- Software Development -

As expert for OpenSource and DevOps, Automatation is his passion – no matter if it is IT-infrastructure, software deployments or even coffee makers. Sebastian works as head developer on Cloudogu Ecosystem and the SCM-Manager and an expert with the mission to make other developers life easier.