featured image Java Annotation Processors - An introduction

June 15, 2018 / by Sebastian Sdorra / In Software Craftsmanship

Java Annotation Processors - An introduction

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

This is the introduction part of a blogpost series, in which we will take a look at the basic functionality of annotation processors using a small example. The code used in this article can be viewed at https://github.com/cloudogu/annotation-processors/.
During the build, annotation processors are called by the compiler (javac) when one of the configured annotations has been found. In this context, the annotation processor is able to determine which annotations it wishes to be notified of. This can include single, multiple, or all annotations. If the compiler finds an annotation, it checks whether a processor has been registered for it. If this is the case, it is executed. At this point, the annotation processor can perform its task and determine whether additional processors should be called for the annotation found.
In the first section of this article, we will create a simple annotation processor that issues a log message during compilation. To begin with, we will only use this annotation processor with the help of the Java compiler. Then, we will see how our simple annotation processor can be used with a build tool, such as Maven.

A simple annotation processor

Annotation processors must implement the javax.annotation.processing.Processor interface; in most cases it is recommended to extend the javax.annotation.processing.AbstractProcessor class, as it contains useful auxiliary methods. Our example annotation processor is supposed to issue a notification when a certain annotation is found. To do so, we must first create our annotation (com/cloudogu/blog/annotationprocessor/log/Log.java):

@Target({ElementType.TYPE})
public @interface Log {}

The Target annotation with the parameter ElementType.TYPE from our Log annotation specifies that we can use @Log with all Java types (classes, interfaces, or enums). Whenever Javac finds this annotation, we want a message to be displayed on the console that shows us which class the annotation uses. The annotation processor (com/cloudogu/blog/annotationprocessor/log/LogProcessor.java/) for the Log annotation:

@SupportedAnnotationTypes("com.cloudogu.blog.annotationprocessor.log.Log")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class LogProcessor extends AbstractProcessor {

  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    for ( TypeElement annotation : annotations ) {
      for ( Element element : roundEnv.getElementsAnnotatedWith(annotation) ) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "found @Log at " + element);
      }
    }
    return true;
  }

}

The SupportedAnnotationTypes annotation determines the annotations for which our processor is called. It is also possible to use “*”; in this case, the processor is called for every annotation that is found. The SupportedSourceVersion annotation shows the latest Java version the annotation processor supports. If the annotation processor is used with a never version of Java, a warning will be shown informing the user that the processor does not support this version of Java.
We then have to implement the process method of the AbstractProcessor. Two values are passed to the method:

  • A set of java.lang.model.element.TypeElement, which contains all annotations found.
  • javax.annotation.processing.RoundEnvironment – with this object, it is possible to inspect the annotated elements that have been found.

If the process method returns true, no further annotation processors will be called for the annotation that was found. If it returns false, additional annotation processors can be notified for this annotation. By extending the AbstractProcessor, you can also access the processingEnv variable of the type javax.annotation.processing.ProcessingEnvironment. ProcessingEnvironment enables access to the compiler environment, for example to abort a build process or display a message on the console.
In our example:

  1. We first iterate using the set of annotations found: for ( TypeElement annotation : annotations ) {
  2. Using RoundEnvironment, we then search for the elements that have been annotated with this annotation: for ( Element element : roundEnv.getElementsAnnotatedWith(annotation) ) {
  3. Next, a log of all elements found with ProcessingEnvironment is displayed on the console: processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "found @Log at " + element);

Registration

In order for the compiler to be able to find the annotation processor, it must be registered. This is done using the ServiceLoader, introduced with Java 6. To do this, we have to create a file under META-INF/services/javax.annotation.processing.Processor, which contains the full name of our annotation processor. In our example this would be: com.cloudogu.blog.annotationprocessor.log.LogProcessor

Usage

To be able to use the processor, we need a class that is annotated with @Log, for example (com/cloudogu/blog/annotationprocessor/sample/Hello.java):

@Log
public class Hello {

  public static void main(String[] args) {
    System.out.println("Hello");
  }

}

After creating the example class, we can compile and test our annotation processor. First, we compile the annotation and the processor:
javac –cp . -proc:none com/cloudogu/blog/annotationprocessor/log/*.java
The parameter -proc:none deactivates all of the annotation processors. This is important, as the compiler already finds our registry (the file saved under META-INF/services), but our processor is not yet compiled. This would lead to an error. Now, we can compile our test class and thus test our annotation processor:
javac –cp . com/cloudogu/blog/annotationprocessor/sample/*.java
The compiler should now call the annotation processor and we should see the following line on the console:
Note: found @Log at com.cloudogu.blog.annotationprocessor.sample.Hello
Annotation processors can also be used with standard build tools and IDEs. In order to use annotation processors with Maven, for example, the build must be divided into separate modules. This separation must take place so that Maven can compile the annotation processor independently and before its use. Our example with Maven can be viewed at the following URL: https://github.com/cloudogu/annotation-processors/tree/master/part-1-maven . Here, it is important to remember that the maven-compiler-plugin must be configured in both modules. Since compiler warnings are to be issued in the sample module, showWarnings must be set to true, and in the log module, the compiler must be run with -proc:none, as described above (compilerArgs). If mvn clean install is now run in the parent module, the expected message will appear in the output:
[INFO] found @Log at com.cloudogu.blog.annotationprocessor.sample.Hello

Conclusion

In this blog post we have learned how to write, register and use a simple Annotation Processor. We have also highlighted the possibilities of Annotation Processors. In the next part we will look at the creation of configurations and into the documentation from annotations in more detail. The third part will be about the actual generation of code. So stay tuned!


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.