Source-Level Annotation Processing in Java

In the preceding section, you saw how to analyze annotations in a running program. Another use for annotation is the automatic processing of source files to produce more source code, configuration files, scripts, or whatever else one might want to generate.

1. Annotation Processors

Annotation processing is integrated into the Java compiler. During compilation, you can invoke annotation processors by running

javac -processor ProcessorClassName1,ProcessorClassName2,. . . sourceFiles

The compiler locates the annotations of the source files. Each annotation processor is executed in turn and given the annotations in which it expressed an interest. If an annotation processor creates a new source file, the process is repeated. Once a processing round yields no further source files, all source files are compiled.

An annotation processor implements the Processor interface, generally by ex­tending the AbstractProcessor class. You need to specify which annotations your processor supports. In our case:

@SupportedAnnotationTypes(“com.horstmann.annotations.ToString”)

@SupportedSourceVersion(SourceVersion.RELEASE_8)

public class ToStringAnnotationProcessor extends AbstractProcessor

{

public boolean process(Set<? extends TypeElement> annotations,

RoundEnvironment currentRound)

{

}

}

A processor can claim specific annotation types, wildcards such as “com.horstmann.*” (all annotations in the com.horstmann package or any subpackage), or even “*” (all annotations).

The process method is called once for each round, with the set of all annotations that were found in any files during this round, and a RoundEnvironment reference that contains information about the current processing round.

2. The Language Model API

Use the language model API for analyzing source-level annotations. Unlike the reflection API, which presents the virtual machine representation of classes and methods, the language model API lets you analyze a Java program according to the rules of the Java language.

The compiler produces a tree whose nodes are instances of classes that im­plement the javax.lang.model.element.Element interface and its subinterfaces: TypeElement, VariableElement, ExecutableElement, and so on. These are the compile-time analogs to the Class, Field/Parameter, Method/Constructor reflection classes.

I do not want to cover the API in detail, but here are the highlights that you need to know for processing annotations:

  • The RoundEnvironment gives you a set of all elements annotated with a particular annotation. Call the method

Set<? extends Etement> getEtementsAnnotatedWith(Ctass<? extends Annotation> a)

  • The source-level equivalent of the AnnotateEtement interface is AnnotatedConstruct. Use the methods

A getAnnotation(Ctass<A> annotationType)

A[] getAnnotationsByType(Ctass<A> annotationType)

to get the annotation or repeated annotations for a given annotation class.

  • A TypeEtement represents a class or interface. The getEnctosedEtements method yields a list of its fields and methods.
  • Calling getSimpteName on an Element or getQuatifiedName on a TypeEtement yields a Name object that can be converted to a string with toString.

3. Using Annotations to Generate Source Code

As an example, we will use annotations to reduce the tedium of implementing toString methods. We can’t put these methods into the original classes— annotation processors can only produce new classes, not modify existing ones.

Therefore, we’ll add all methods into a utility class ToStrings:

public class ToStrings

{

public static String toString(Point obj)

{

Generated code

}

public static String toString(Rectangte obj)

{

Generated code

}

public static String toString(Object obj)

{

return Objects.toString(obj);

}

}

We don’t want to use reflection, so we annotate accessor methods, not fields:

@ToString

public class Rectangle

{

@ToString(includeName=false) public Point getTopLeft() { return topLeft; }

@ToString public int getWidth() { return width; }

@ToString public int getHeight() { return height; }

}

The annotation processor should then generate the following source code:

public static String toString(Rectangle obj)

{

var result = new StringBuilder();

result.append(“Rectangle“);

result.append(“[“);

result.append(toString(obj.getTopLeft()));

result.append(“,”);

result.append(“width=”);

result.append(toString(obj.getWidth()));

result.append(“,”);

result.append(“height=”);

result.append(toString(obj.getHeight()));

result.append(“]”);

return result.toString();

}

The “boilerplate” code is in gray. Here is an outline of the method that produces the toString method for a class with a given TypeElement:

private void writeToStringMethod(PrintWriter out, TypeElement te)

{

String className = te.getQualifiedName().toString();

Print method header and declaration of string builder

ToString ann = te.getAnnotation(ToString.class);

if (ann.includeName())

Print code to add class name

for (Element c : te.getEnclosedElements())

{

ann = c.getAnnotation(ToString.class);

if (ann != null)

{

if (ann.includeName()) Print code to add field name

Print code to append toString(obj.methodName())

}

}

Print code to return string

}

 

And here is an outline of the process method of the annotation processor. It creates a source file for the helper class and writes the class header and one method for each annotated class.

public boolean process(Set<? extends TypeElement> annotations,

RoundEnvironment currentRound)

{

if (annotations.size() == 0) return true;

try {

JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(

“com.horstmann.annotations.ToStrings”); try (var out = new

PrintWriter(sourceFile.openWriter()))

{

Print code for package and class

for (Element e : currentRound.getElementsAnnotatedWith(ToString.class))

{

if (e instanceof TypeElement)

{

TypeElement te = (TypeElement) e;

writeToStringMethod(out, te);

}

}

Print code for toString(Object)

}

catch (IOException ex)

{

processingEnv.getMessager().printMessage(

Kind.ERROR, ex.getMessage());

}

}

return true;

}

For the tedious details, check the book’s companion code.

Note that the process method is called in subsequent rounds with an empty list of annotations. It then returns immediately so it doesn’t create the source file twice.

First, compile the annotation processor, and then compile and run the test program as follows:

javac sourceAnnotations/ToStringAnnotationProcessor.java

javac -processor sourceAnnotations.ToStringAnnotationProcessor rect/*.java

java rect.SourceLevelAnnotationDemo

This example demonstrates how tools can harvest source file annotations to produce other files. The generated files don’t have to be source files. Annota­tion processors may choose to generate XML descriptors, property files, shell scripts, HTML documentation, and so on.

Source: Horstmann Cay S. (2019), Core Java. Volume II – Advanced Features, Pearson; 11th edition.

Leave a Reply

Your email address will not be published. Required fields are marked *