How To Manipulate Source Code Using Java Annotation Processing API

Technology

The source level annotations are introducing in Java 5, it provides the features to create additional source files during the compilation stage.

The source code files are not limited to java files, files can be any kind like metadata, documentation or any type of resource.

Annotation Processing API is actively used in many open source frameworks like Spring Query DSL and JPA etc.

The limitation with this API is it only used to generate new files, it will not modify the existing files.

Execution

word-image

The annotation processing is done at multiple rounds. Each round starts with compiler searching for the annotations in java source, choosing the annotation processors suited for these annotations, each annotation processor is called in corresponding sources.

If any files are created during the process, another round start with generated source files as input, it will continue until no new files are generated during the processing stage.

The annotation processing API is located in the javax.annotation.processing package. The main interface that you’ll have to implement is the Processor interface, which has a partial implementation in the form of AbstractProcessor class. This class is the one we’re going to extend to create our own annotation processor.

Setting Up the Project

For demonstrations of annotation processing, we will spilt our project into 2 modules, one of them annotation-processing module which will contain annotation processor, another will be annotation-user module.

First, we will implement annotation-processing module, for annotation processing we are going to use Google`s auto-service library which used to create processor meta-data.

It is recommended to use Google auto-service and maven compiler plugin latest versions found in Maven Central Repository.

<properties>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

<auto-service.version>1.0-rc4</auto-service.version>

</properties>

<dependencies>

<dependency>

<groupId>com.google.auto.service</groupId>

<artifactId>auto-service</artifactId>

<version>${auto-service.version}</version>

<scope>provided</scope>

</dependency>

</dependencies>

<build>

<plugins>

<plugin>

<groupId>org.apache.maven.plugins</groupId>

<artifactId>maven-compiler-plugin</artifactId>

<version>3.8.0</version>

<configuration>

<release>11</release>

</configuration>

</plugin>

</plugins>

</build>

And annotation-user simply use annotation-processor as its dependency.

<dependency>

<groupId>org.sravan</groupId>

<artifactId>Annotation-Processing</artifactId>

<version>${project.version}</version>

</dependency>

Let’s create a @BuilderProperty annotation in the annotation-processor module for the setter methods. It will allow us to generate the Builder class for each class that has its setter methods annotated:

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.SOURCE)

public@interfaceBuilderProperty {

}

The @Target annotation with the ElementType.METHOD parameter ensures that this annotation can be only put on a method.

The SOURCE retention policy means that this annotation is only available during source processing and is not available at runtime.

Implementing Processor

word-image

AbstractProcessor is the abstract implementation of Processor interface, now lets extends the this class and implement the abstract methods.

@SupportedSourceVersion(SourceVersion.RELEASE_8)

@SupportedAnnotationTypes(“org.sravan.annotation.processor.BuilderProperty”)

@AutoService(Processor.class)

publicclass BuilderProcessor extends AbstractProcessor {

@Override

publicboolean process(Set<? extends TypeElement>annotations, RoundEnvironment roundEnv) {

returnfalse;

}

We should specify the annotations that this processor is capable of processing and also source code version. This can be done either by implementing getSupportedAnnotationTypes and getSupportedSourceVersion of the Processor interface or by annotating your class with @SupportedAnnotationTypes and @SupportedSourceVersion annotations.

And the process method returns true if all supported annotations are processed, and don’t want to process by another annotation processor down the list.

@AutoService annotation is itself processed by the annotation processor from the auto-service library. This processor generates the META-INF/services/javax.annotation.processing.Processor file containing the BuilderProcessor class name.
annotations variable in process method will contain all the annotation types found in the class.

RoundEnvironment instance to receive all elements annotated with the @BuilderProperty annotation.

for(TypeElement annotation: annotations)

{

Set<? extends Element>annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);

…

}

@BuilderProperty annotation’s user could erroneously annotate methods that are not actually setters. The setter method name should start with set, and the method should receive a single argument. So lets separate the methods which are correctly annotated and other erroneously annotated methods using Collections.partitionBy() collector.

Map<Boolean, List<Element>>annotatedMethods = annotatedElements.stream().collect(Collectors.partitioningBy(element -> ((ExecutableType)element.asType()).getParameterTypes().size()==1 &&

element.getSimpleName().toString().startsWith(“set”)));

List<Element>setters = annotatedMethods.get(true);

List<Element>otherMethods = annotatedMethods.get(false);

We should warn the user if the method is incorrectly annotated, for this we can use AbstractProcessor.processingEnv protected field.

otherMethods.forEach(element ->processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, “@BuilderProperty must be applied to a setXxx method with a single argument”, element));

and also if the correctly annotated methods are not present then we can skip the current iteration.

if(setters.isEmpty())

{

continue;

}

Then we need to get the fully qualified class name using setter methods using below code.

String className = ((TypeElement) setters.get(0).getEnclosingElement()).getQualifiedName().toString();

we will store setter methods name, and argument type in map and use it iterate to generate source file.

Map<String,String>setterMap = setters.stream().collect(Collectors.toMap(setter ->setter.getSimpleName().toString(), setter -> ((ExecutableType)setter.asType()).getParameterTypes().get(0).toString()));

Now lets create Person class in annotation-user module like below,

publicclass Person {

privateintage;

private String name;

publicint getAge() {

returnage;

}

@BuilderProperty

publicvoidsetAge(intage) {

this.age = age;

}

public String getName() {

returnname;

}

@BuilderProperty

publicvoid setName(String name) {

this.name = name;

}

}

Running the Example

Although the Java Annotation Processing API is easy but sometimes you would need a custom development to do the source level annotation.

If you don’t want to go through all that trouble I would suggest you should consider Java development outsourcing which is rather easy and hassle-free.

Anyways moving on, now if we compile annotation-user module below java file is generated under annotation-user/target/generated-Sources/annotations/com/baeldung/annotation/PersonBuilder.java and it will look like below:

package org.sravan.annotation;

public class PersonBuilder {

private Person object = new Person();

public Person build() {

return object;

}

public PersonBuilder setName(java.lang.String value) {

object.setName(value);

return this;

}

public PersonBuilder setAge(int value) {

object.setAge(value);

return this;

}

}

Conclusion

In this content, we learned how source-level annotations are processed to generate the source files using the Google Auto Service Library.

The Source code can be found in the attached zip file.

https://drive.google.com/file/d/1z0sszFN47JZAYimd1oexHF2v7bGZQ1Vb/view?usp=sharing

STAY UP TO DATE

Sign up today to stay informed with industry news & trends