Playing with Java annotation processing

Posted on Fri 06 January 2012 in Coding

I found Project Lombok via a Tweet, and was intrigued when I saw that a single annotation could trigger automatic code generation transparently during compilation of a source file. How do they do that?

Some digging lead me to discover Java’s annotation processing API, and how one can use SPI (Service Provider Interfaces) to transparently invoke an annotation processor during compilation. Here’s how to do it:

First, we create a simple annotation type (package declaration and imports omitted for brevity):

@Retention(RetentionPolicy.SOURCE)
public @interface PrintMe {}

With the @Retention annotation, we specify that we don’t need the annotations of this type beyond compilation time, so the compiler can discard them.

Next, we create an annotation processor that prints elements that are annotated using the @PrintMe annotation. We can inherit from the AbstractProcessor class. Using the @SupportedAnnotationTypes annotation, we specify which annotations we handle:

@SupportedAnnotationTypes(
  {"com.programmaticallyspeaking.aptdemo.PrintMe"}
)
public class AnnotationProcessor extends AbstractProcessor {

The main method to implement is process:

  public boolean process(Set<? extends TypeElement> annotations,
                         RoundEnvironment env) {

We get hold of the current Messager instance to be able to print messages:

    Messager messager = processingEnv.getMessager();

Then we iterate over the annotations (there is only one, so the code is a bit more generic than it needs to be) and the elements that are annotated by them:

    for (TypeElement te : annotations) {
      for (Element e : env.getElementsAnnotatedWith(te)) {

At this point, we can do a lot of interesting stuff, but for now let’s just print each annotated element:

        messager.printMessage(Diagnostic.Kind.NOTE,
                              "Printing: " + e.toString());

Finally, we wrap up and return true to claim the handled annotations so that no other annotation processor can use them:

      }
    }
    return true;
  }

To avoid a warning about source version when the annotation processor is run, we specify that we handle the latest source version that the current execution environment supports:

  @Override
  public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
  }
}

And that’s it for the annotation processor. Next, we need to specify where the annotation processor resides using SPI. We do this by creating a file called javax.annotation.processing.Processor in the META-INF/services directory. The contents of the file should be:

com.programmaticallyspeaking.aptdemo.AnnotationProcessor

Also, Maven’s compiler plugin needs some configuration. First to increase the source and target levels and second to disable annotation processing (as the plugin gets mighty confused by our SPI file):

<plugin>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>2.3.2</version>
  <configuration>
    <source>1.6</source>
    <target>1.6</target>
    <!-- Disable annotation processing for ourselves. -->
    <compilerArgument>-proc:none</compilerArgument>
  </configuration>
</plugin>

The projects in its entirety can be fetched from GitHub.

Building the project should result in a file aptdemo-1.0-SNAPSHOT.jar. Say that we have the following test source file:

import com.programmaticallyspeaking.aptdemo.PrintMe;

@PrintMe
public class Test {
  @PrintMe
  public static void a1(int x) {
  }

  public static void a2(String[] arr) {
  }
}

Compiling it with the aptdemo JAR on the classpath should result in:

$ javac -cp target/apt-demo-1.0-SNAPSHOT.jar Test.java
Note: Printing: Test
Note: Printing: a1(int)

Note that no compilation flags for annotation processing are needed. It’s completely transparent! Note also that the a2 method, which is not annotated, is not printed during compilation.

That’s all for now! Happy annotation processing! :-)