Annotation Processing: Introduction

AutoValue is one of my favourite annotation processing libraries, being able to create immutable value classes with support for builders is something that I believe every developer should have in their toolbox.

So what is annotation processing and what makes it so useful?

Many Java developers often lament with having to create so much boilerplate code, and often the language receives criticism for being so verbose. Personally I prefer a language to be explicit about its definition to save from any ambiguity but it can admittedly be somewhat tedious.

Utilising annotation processing and code generation means we can forgo the additional time and effort needed to create boilerplate code. Once an annotation processor has been registered it will process annotated code at compile time and generate classes.

It is important to note that you cannot modify existing Java classes, this requires bytecode weaving, so you can only generate new classes. This includes inner classes, so any annotated inner classes can only generate a new outer class.

Now ordinarily most guides would tell you to extend the AbstractProcessor, and whilst this is fine you can also use BasicAnnotationProcessor when multiple processing steps may be required, even if you only have a single processing step it can still be helpful to use to simplify the process.

With the BasicAnnotationProcessor you need only override two methods and then the job of actually processing is left to the ProcessingStep.

  • initSteps() returns an iterable of your processing steps
  • getSupportedSourceVersion() returns the version of Java to support
@AutoService(Processor.class)
public class AnnotationProcessor extends BasicAnnotationProcessor {

@Override
protected Iterable<? extends ProcessingStep> initSteps() {
return Collections.singletonList(new MyProcessingStep(processingEnv));
}

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

Above you can see that I’m also using the AutoService annotation which will automatically generate the META-INF resource necessary to register your annotation processor. That’s right, code is being generated from an annotation processor within another annotation processor!

When writing code in your annotation processor you need not worry too much about what libraries you are using since the process is happening during compile time, only the resulting code goes into your applications, which means feel free to use Guava if you like!

When extending the ProcessingStep you have another two methods to override that handling the actual processing of annotated classes.

class MyProcessingStep implements BasicAnnotationProcessor.ProcessingStep {

@Override
public Set<? extends Class<? extends Annotation>> annotations() {
return Collections.singleton(MyAnnotation.class);
}

@Override
public Set<Element> process(SetMultimap<Class<? extends Annotation>, Element> elementsByAnnotation) {
return new HashSet<>();
}
}

Here you can return your supported annotations that your processor can handle, and then your process method to actually process the elements.

When finished with your processing, you should return the elements not yet processed so that they may be passed onto the next processing step.

In the following article I’ll go into some of the annotation libraries I’ve been building recently and how you can go about unit testing your code.

Happy processing!