Abusing Java Annotation Processor API for "Fun & Profit" - Part I

PUBLISHED ON FEBRUARY 07, 2017 — SOFTWARE DEVELOPMENT

In this 2-part blog post, we’ll start by briefly explaining the purpose of annotation processors and how can we create and register one for fun (the profit will come later!). In the second part of this series, we’ll use and abuse its internal APIs to have even more fun! You’ll find yourself in a dark place, but with awesome cookies ☻

Annotation Processor

First of all, i’ll assume you’re already familiar with annotations, if that’s not the case, feel free to read Oracle’s documentation[1] on it.
Annotation Processors are essentially a mechanism designed to process annotations (no shit Sherlock) at compile time. The key here is compile time, but we’ll get there soon.
Basically, you register an annotation processor to be executed during compile time if certain conditions are met:

  1. The annotation processor must provide a no-arg constructor
  2. The code being compiled must contain annotations that are marked as supported by the annotation processor (wildcards are supported)
  3. The supported annotations must not have been claimed by another annotation processor which had previously run
  4. The annotation processor should support the source version release (-source). It’s not required, but you’ll get a warning: Supported source version 'RELEASE_<version>' from annotation processor 'your.annotation.Processor' less than -source '<source version>

For a more in-depth explanation of the above, go ahead and read the contract defined by Processor’s interface[2]

The main goal of annotation processors is to provide an extensible and dynamic way to generate additional data (code, metadata, etc) at compilation time. As we’ll see and have fun while doing so (drooling for them cookies already?), modifying existent code isn’t part of the API defined by JSR-259[3].

Creating an Annotation Processor

The API is kind enough to provide you with an abstraction[4] which you can extend and simply provide the configuration for your processor along with your own processing code as we can see below.

import java.util.Collections;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;

public class ImNotGonnaLetYouCompileProcessor extends AbstractProcessor {

  @Override
  public Set<String> getSupportedAnnotationTypes() {
    return Collections.singleton("*");
  }

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

  @Override
  public boolean process(final Set<? extends TypeElement> annotations,
    final RoundEnvironment roundEnv) {
      if (!roundEnv.processingOver()) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
            "Even though i'm a compiler and i don't have a nose, " +
            "i sense that your code smells. Please redo and try again.");
      }
      return true;
  }
}

The above processor isn’t particularly helpful, at all, but i bet you’ll laugh your ass off if you hijack one of your buddies compilation procedure and watch him trying to compile his code.
We’ll see how we can setup your compilation procedure to use the above annotation processor, but first, let me take a selfie let’s break it down:

  • AbstractProcessor
    This is essentially the abstraction that i have mentioned earlier, it provides support for:
    • Supported source versions via @SupportedSourceVersion[5] annotation.
    • Supported annotation types via @SupportedAnnotationTypes[6] annotation.
    • Supported processor options via @SupportedOptions[7] annotation.
    • Issues warnings when appropriate (i.e. no source version have been configured).
    • Holds a reference for ProcessingEnvironment[8] via the processingEnv variable after the processor initialization. This class provides a facility to create new files, report messages to the compiler (we certainly did that with the above processor!) along with other utility functions.
  • getSupportedAnnotationTypes
    This method enables the configuration of the supported annotations by our processor. Optionally, you can configure this via annotations if you use choose to extend AbstractProcessor.
    You must specify the canonical name of the supported annotations. Wildcards are also supported, where a single wildcard ‘*’ denotes all annotations (including no annotations).
  • getSupportedSourceVersion
    This method enables the configuration of the latest supported source version by our processor.
    You can also configure this via annotations if you use choose to extend AbstractProcessor, but you loose the flexibility of using latest() and
    latestSupported() for the configuration.
  • process(annotations, roundEnv)
    This is where all of the magic happens, where you implement your annotation processor code. But there are a few things that I want your to be aware before coding the hell out of it:
    • Annotation processing is done is [1..N] rounds: in each round both annotations being processed (annotations parameter) and annotated source elements (via roundEnv parameter) are handed to the processor.
    • RoundEnvironment[9] also contains information associated with the previous round.
    • The return value affects subsequent rounds. That is:
      • if true is returned, no subsequent processors will be asked to process the annotations being processed in that round.
      • if false is returned, subsequent processor might be asked to process them.

Registering an Annotation Processor

There are two ways for you to specify which annotation processors shall be used:

  1. Manually via the -processor compiler option
  2. Automatically by having the following packaging structure:
    Package your annotation processor code along with a file named javax.annotation.processing.Processor under a /META-INF/services/ directory. That file is a simple text file with 0..N processors inside, identified by their canonical name, one per line.

We’re gonna take on option 2 for the above processor, which is how it’s properly done.

Compile the «ImNotGonnaLetYouCompileProcessor» processor and create the META-INF/services/javax.annotation.processing.Processor file.

javac ImNotGonnaLetYouCompileProcessor.java
mkdir -p META-INF/services/
echo "ImNotGonnaLetYouCompileProcessor" > META-INF/services/javax.annotation.processing.Processor

We’re know able to create a jar file with the proper structure by issuing the following command:

jar cvf smelly-code-processor.jar ImNotGonnaLetYouCompileProcessor.class META-INF

You should have a jar file with the following structure:

Archive:  smelly-code-processor.jar
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  02-05-2017 16:40   META-INF/
       69  02-05-2017 16:40   META-INF/MANIFEST.MF
     1655  02-05-2017 16:39   ImNotGonnaLetYouCompileProcessor.class
        0  02-03-2017 14:14   META-INF/services/
       33  02-05-2017 16:38   META-INF/services/javax.annotation.processing.Processor

There’s really only one thing left to do. Clone your awesome jar file to your colleagues/buddies compile classpath (that’s up to you to figure that out - but a continuous integration is an easy target) and sit back, relax and enjoy the moment. Try not to laugh too much, as you’ll have some explaining to do right after ☻

Well, after this light introduction to the annotation processor API, we’re now able to create teh best a simple annotation processor. Creating a processor that generates data/code is also quite simple through the Filer[10] utility class accessible via processingEnv.getFiler().

On a side note, DO NOT generate code «by hand», use a template engine such as Apache Velocity[11] or any other that suits your needs. You can thank me later. I’m cool with cookies (the dark side flavour of course!), pancakes and waffles.. eheh.
Let me know on the comment section below if you would like to see an example on that as well.

That’s it folks, we’ll have some more fun in part II, which shall be available soon. Meanwhile, go ahead and prank your colleagues and buddies with our awesome processor!


[1] https://docs.oracle.com/javase/tutorial/java/annotations/
[2] https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/Processor.html
[3] https://www.jcp.org/en/jsr/detail?id=269
[4] https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/AbstractProcessor.html
[5] https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/SupportedAnnotationTypes.html
[6] https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/SupportedSourceVersion.html
[7] https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/SupportedOptions.html
[8] https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/ProcessingEnvironment.html
[9] https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/RoundEnvironment.html
[10] https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/Filer.html
[11] http://velocity.apache.org/

comments powered by Disqus