Annotation Processing
About
Annotation Processing is a powerful feature in Java that allows developers to intercept and process annotations at compile time. It plays a vital role in code generation, validation, and automation of boilerplate tasks in large-scale applications and frameworks like Spring, Lombok, Dagger, etc.
Annotation processing is a compile-time mechanism that inspects the source code for annotations and can generate additional source files, perform validations, or create metadata.
It is handled by tools that implement the javax.annotation.processing.Processor
interface (now jakarta.annotation.processing.Processor
in Jakarta EE) and is integrated with the Java compiler (javac
).
Annotation processors run during compilation and do not affect runtime behavior directly, but the code they generate or validate influences the final application.
When Is Annotation Processing Used ?
Annotation processing is used when:
We want to generate source code automatically (e.g., builder classes, DTOs, factories).
We want to validate annotation usage (e.g., checking constraints).
We want to generate configuration metadata (e.g., Spring factories, JSON schema, etc.).
We are building a framework or library that relies on compile-time structure.
Examples
Generate boilerplate code
Lombok
Dependency Injection
Dagger, Hilt
JSON Serialization
AutoValue, Gson, Jackson
Configuration Metadata
Spring Boot
Mapper Generation
MapStruct
How It Works ?
We create a custom annotation.
We implement an annotation processor that:
Identifies target annotations.
Uses the compiler’s Abstract Syntax Tree (AST) or Elements/Mirrors API.
Optionally generates new source files.
The annotation processor is registered using
META-INF/services/javax.annotation.processing.Processor
file.During compilation,
javac
invokes the processor, processes annotations, and generates code before final compilation completes.
Use Case: @AutoToString Annotation
We’ll create a custom annotation @AutoToString
that, when applied to a class, generates a helper class with a toString()
method implementation for that class.
This is a compile-time code generation example - the processor will generate code based on our annotation.
Folder Structure
annotation-processor/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/processor/AutoToStringProcessor.java
└── com/example/annotations/AutoToString.java
│ │ └── resources/
│ │ └── META-INF/services/javax.annotation.processing.Processor
├── pom.xml
Define the Annotation
// file: AutoToString.java
package com.example.annotations;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface AutoToString {}
@Target(TYPE)
— used on classes@Retention(SOURCE)
— we only care during compile time
Implement the Annotation Processor
// file: AutoToStringProcessor.java
package com.example.processor;
import com.example.annotations.AutoToString;
import javax.annotation.processing.*;
import javax.lang.model.element.*;
import javax.lang.model.*;
import javax.tools.*;
import java.io.Writer;
import java.util.Set;
@SupportedAnnotationTypes("com.example.annotations.AutoToString")
@SupportedSourceVersion(SourceVersion.RELEASE_21)
public class AutoToStringProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(AutoToString.class)) {
if (element.getKind() != ElementKind.CLASS) continue;
TypeElement classElement = (TypeElement) element;
String className = classElement.getSimpleName().toString();
String packageName = processingEnv.getElementUtils()
.getPackageOf(classElement).getQualifiedName().toString();
String generatedClassName = className + "ToStringGenerator";
try {
JavaFileObject builderFile = processingEnv.getFiler()
.createSourceFile(packageName + "." + generatedClassName);
try (Writer writer = builderFile.openWriter()) {
writer.write("package " + packageName + ";\n\n");
writer.write("public class " + generatedClassName + " {\n");
writer.write(" public static String toString(" + className + " obj) {\n");
writer.write(" return \"" + className + " [");
boolean first = true;
for (VariableElement field : ElementFilter.fieldsIn(classElement.getEnclosedElements())) {
String fieldName = field.getSimpleName().toString();
if (first) first = false;
else writer.write(" + \", ");
writer.write(fieldName + "=\" + obj." + fieldName);
}
writer.write(" + \"]\";\n");
writer.write(" }\n}\n");
}
} catch (Exception e) {
e.printStackTrace();
}
}
return true;
}
}
It identifies all classes annotated with
@AutoToString
.For each class, it generates a helper class like
PersonToStringGenerator.java
.It creates a static method that builds a
toString
output using the class fields.
Register the Processor
Path: src/main/resources/META-INF/services/javax.annotation.processing.Processor
Contents:
com.example.processor.AutoToStringProcessor
This tells the compiler which class is an annotation processor.
Use the Annotation in a Client Class
// file: Person.java
package com.example.model;
import com.example.annotations.AutoToString;
@AutoToString
public class Person {
public String name;
public int age;
}
When this is compiled, the processor generates:
// file: PersonToStringGenerator.java
package com.example.model;
public class PersonToStringGenerator {
public static String toString(Person obj) {
return "Person [name=" + obj.name + ", age=" + obj.age + "]";
}
}
Use the Generated Code
public class Person {
public static void main(String[] args) {
Person p = new Person();
p.name = "Alice";
p.age = 30;
String result = PersonToStringGenerator.toString(p);
System.out.println(result); // Output: Person [name=Alice, age=30]
}
}
Build the JAR using Maven
1. Add pom.xml
pom.xml
Here is a basic Maven config
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>annotation-processor</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Annotation Processor</name>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
</project>
No dependencies are needed unless we use external libraries in our processor.
2. Package the JAR
Run this from inside our project folder
mvn clean package
This will generate a JAR in
target/annotation-processor-1.0-SNAPSHOT.jar
That JAR will contain
Our processor class
The
@AutoToString
annotationThe
META-INF/services/javax.annotation.processing.Processor
file
Use the JAR in Another Project
Now in our actual project (say, where Person.java
is):
Put the above JAR in the classpath
Compile with:
javac -cp annotation-processor-1.0-SNAPSHOT.jar -processorpath annotation-processor-1.0-SNAPSHOT.jar Person.java
This will trigger our annotation processor during compilation.
Use the JAR in Maven Project
In our main project's pom.xml
, we can declare the processor as a build-time dependency only:
<dependency>
<groupId>com.example</groupId>
<artifactId>annotation-processor</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>provided</scope> <!-- only during compile time -->
</dependency>
And for automatic annotation processing:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.example</groupId>
<artifactId>annotation-processor</artifactId>
<version>1.0-SNAPSHOT</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Last updated