One of the cool new features of Java 8 is the support for lambda expressions. Lambda expressions lean heavily on the
In this post, we’ll look at annotations and how to process them so you can implement your own cool features.
Annotations were added in Java 5. The Java language comes with some predefined annotations, but you can also define custom annotations.
Many frameworks and libraries make good use of custom annotations. JAX-RS, for instance, uses them to turn POJOs into REST resources.
Annotations can be processed at compile time or at runtime (or even both).
At runtime, you can use the reflection API. Each element of the Java language that can be annotated, like class or method, implements the
AnnotatedElement interface. Note that an annotation is only available at runtime if it has the
Compile-Time Annotation Processing
Java 5 came with the separate
apt tool to process annotations, but since Java 6 this functionality is integrated into the compiler.
You can either call the compiler directly, e.g. from the command line, or indirectly, from your program.
In the former case, you specify the
-processor option to
javac, or you use the
ServiceLoader framework by adding the file
META-INF/services/javax.annotation.processing.Processor to your jar. The contents of this file should be a single line containing the fully qualified name of your processor class.
ServiceLoader approach is especially convenient in an automated build, since all you have to do is put the annotation processor on the classpath during compilation, which build tools like Maven or Gradle will do for you.
Compile-Time Annotation Processing From Within Your Application
You can also use the compile-time tools to process annotations from within your running application.
Rather than calling
javac directly, use the more convenient
JavaCompiler interface. Either way, you’ll need to run your application with a JDK rather than just a JRE.
JavaCompiler interface gives you programmatic access to the Java compiler. You can obtain an implementation of this interface using
ToolProvider.getSystemJavaCompiler(). This method is sensitive to the
JAVA_HOME environment variable.
getTask() method of
JavaCompiler allows you to add your annotation processor instances. This is the only way to control the construction of annotation processors; all other methods of invoking annotation processors require the processor to have a public no-arg constructor.
A processor must implement the
Processor interface. Usually you will want to extend the
AbstractProcessor base class rather than implement the interface from scratch.
Each annotation processor must indicate the types of annotations it is interested in through the
getSupportedAnnotationTypes() method. You may return
* to process all annotations.
The other important thing is to indicate which Java language version you support. Override the
getSupportedSourceVersion() method and return one of the
With these methods implemented, your annotation processor is ready to get to work. The meat of the processor is in the
true, the annotations processed are claimed by this processor, and will not be offered to other processors. Normally, you should play nice with other processors and return
Elements and TypeMirrors
The annotations and the Java elements they are present on are provided to your
process() method as
Element objects. You may want to process them using the Visitor pattern.
The most interesting types of elements are
TypeElement for classes and interfaces (including annotations),
ExecutableElement for methods, and
VariableElement for fields.
Element points to a
TypeMirror, which represents a type in the Java programming language. You can use the
TypeMirror to walk the class relationships of the annotated code you’re processing, much like you would using reflection on the code running in the JVM.
Annotation processing happens in separate stages, called rounds. During each round, a processor gets a chance to process the annotations it is interested in.
The annotations to process and the elements they are present on are available via the
RoundEnvironment parameter passed into the
If annotation processors generate new source or class files during a round, then the compiler will make those available for processing in the next round. This continues until no more new files are generated.
The last round contains no input, and is thus a good opportunity to release any resources the processor may have acquired.
Initializing and Configuring Processors
Annotation processors are initialized with a
ProcessingEnvironment. This processing environment allows you to create new source or class files.
It also provides access to configuration in the form of options. Options are key-value pairs that you can supply on the command line to
javac using the
-A option. For this to work, you must return the options’ keys in the processor’s
Finally, the processing environment provides some support routines (e.g. to get the JavaDoc for an element, or to get the direct super types of a type) that come in handy during processing.
To get the most accurate information during annotation processing, you must make sure that all imported classes are on the classpath, because classes that refer to types that are not available may have incomplete or altogether missing information.
When processing large numbers of annotated classes, this may lead to a problem on Windows systems where the command line becomes too large (> 8K). Even when you use the
JavaCompiler interface, it still calls
javac behind the scenes.
The Java compiler has a nice solution to this problem: you can use argument files that contain the arguments to
javac. The name of the argument file is then supplied on the command line, preceded by
JavaCompiler.getTask() method doesn’t support argument files, so you’ll have to use the underlying
Remember that the
getTask() approach is the only one that allows you to construct your annotation processors. If you must use argument files, then you have to use a public no-arg constructor.
If you’re in that situation, and you have multiple annotation processors that need to share a single instance of a class, you can’t pass that instance into the constructor, so you’ll be forced to use something like the Singleton pattern.
Annotations are an exciting technology that have lots of interesting applications. For example, I used them to extract the resources from a REST API into a resource model for further processing, like generating documentation.
I’m very interested to learn what you have used them for. Please leave a comment below.