4 minute read Published:

Annotation Processing in Java

Java 8 introduced Type Annotations. This means you can do something like the following:

public void processChange(@NonNull Boolean set){}

Java points out that it doesn’t supply a type checking framework but this got me to thinking. How can we as a company use annotations to make better internal code? Of course we all use annotations on a regular basis but how can we indicate to our coworkers our intentions and reduce bugs. This is one of the thought behind Design by Contract. I didn’t want to get into writing a full blown type checking framework but did want to understand the basic built in Java annotation mechanism. And so the following represents a tech demo project that I used to understand annotations better.

Documenter

Say you have a project—an open source project, a tutorial project, or even a new framework—that you’d like to make more sense out of for users. It would be nice if you could annotate pieces of the code and generate a report (or more preferably an IDE plugin) that would point to and describe that piece of code. So I created a library that allows you to supply a message and a priority of that message to parts of the code. The idea being that you could run you annotations on a project and it would create an ordered list of key points in the code..

Below is a trivial example of how one would use Documenter.

static void addTask(Project project){
    @Document(key="Must declare extensions in RunSimpleExtensions class", priority=1)
    project.extensions.create("runSimple", RunSimpleExtension)

    project.task("runSimple", type: JavaExec ) {
        project.afterEvaluate{
            @Document(key="First use of extensions", priority=2)
            main = project.runSimple.mainClass
            classpath = project.sourceSets.main.runtimeClasspath
            @Document(key="Second use of extensions", priority=3)
            args = project.runSimple.args
        }
    }
}

Document

Creating the annotation is trivial. Just use the @interface annotation type definition. The two things to consider when creating an annotation are (1)where you want to use it (the target) and (2) how long you want it to stay around (the retention policy). In the Document example I use a RetentionPolicy of SOURCE, which means the annotation will stay around for the shortest amount of time. The most useful thing would be for the annotations to be use in all possible places. If you don’t specify the Target or the Retention Java assumes that you want to keep the annotation and that you can use the annotation on any allowed type.

import static java.lang.annotation.ElementType.*;

@Retention(RetentionPolicy.SOURCE)
@Target({ANNOTATION_TYPE, CONSTRUCTOR, FIELD, METHOD, PARAMETER, PACKAGE, TYPE, LOCAL_VARIABLE, TYPE_PARAMETER, TYPE_USE })
public @interface Document{
    int priority();
    String key();
}

Service Provider

Let’s take a detour and talk about the Service Loader framework in Java. The Service Provider Interface was designed to allow a third-party to add functionality to an application. To do this, a user would implement an interface and put it on the classpath, then point to it in the META-INF file. It was the responsibility of the Service Provider to use the Service Loader to grab the classes from Java. Here is an example:

private ServiceLoader<Documenter> loader;
private DocumeterService() {
    loader = ServiceLoader.load(Documenter.class);
    Iterator<Document> documents = loader.iterator();
    while(documents.hasNext()) {
        Document d = documents.next();
    }
}

(This can be seen as a basic implementation of Dependency Injection.) Annotations are handle in a similar way, but instead of using the Service Loader, Java will pass the annotations to a class that you create that extends the javax.annotation.processing.Processor interface.

Abstract Processor

If you have created a javax.annotation.processing.Processor file and put it in your the META-INF/service/ folder then Java will call the class/es that you list in that file. The following is part of the AbstractProcessor for the Documenter project. (It is standard to extend the AbstractProcessor over directly implementing the Processor interface.)

@SupportedAnnotationTypes({ "com.scuilion.documenter.Document" })
public class AnnotationProcessor extends AbstractProcessor {
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Map<String, Note> documents;
        documents = new HashMap<>();
        if (!roundEnv.processingOver()) {
            Scanner scanner = new Scanner();
            scanner.scan(roundEnv.getElementsAnnotatedWith(com.scuilion.documenter.Document.class), documents);
        }
        return true;
    }
}
  • Line 1. Java will filter out all other annotations other than the one you specify. If you really want to handle all annotations in the system, then don’t declare a SupportedAnnotationTypes
  • Line 4. Round Environments hold the annotations that are available for processing.
  • Line 9. Scanner is an extension of ElementScanner class. Calling scan will pass the annotated elements to the appropriate visitXYZ method. For instance, visitPackage will be called when a package level annotation is used. (Have you ever scene a package annotated?)

The process is explicitely using the visitor pattern. I won’t got into the ElementScanner you can see my example on github. The result of what was done is to process each annotated position and output the type and the class the element was located in.