Overview
Java Compiler Plugin API (also known as JSR 269: Pluggable Annotation Processing API) allows you to hook into the Java compilation process to analyze and modify code during compilation.
Key Components
- Annotation Processors: Process annotations during compilation
- AbstractProcessor: Base class for implementing processors
- ProcessingEnvironment: Provides compiler environment and utilities
- RoundEnvironment: Information about the current compilation round
- Elements: Utility for working with program elements
- Types: Utility for type operations
Basic Setup
1. Project Structure
compiler-plugin/ ├── src/main/java/ │ └── com/example/processor/ │ ├── SimpleProcessor.java │ └── BuilderProcessor.java ├── src/main/resources/ │ └── META-INF/services/ │ └── javax.annotation.processing.Processor └── pom.xml
2. Maven Dependencies
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>compiler-plugin</artifactId> <version>1.0.0</version> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- Compiler API --> <dependency> <groupId>com.google.auto.service</groupId> <artifactId>auto-service</artifactId> <version>1.0.1</version> <optional>true</optional> </dependency> <!-- For code generation --> <dependency> <groupId>com.squareup</groupId> <artifactId>javapoet</artifactId> <version>1.13.0</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>11</source> <target>11</target> <annotationProcessorPaths> <path> <groupId>com.google.auto.service</groupId> <artifactId>auto-service</artifactId> <version>1.0.1</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build> </project>
Basic Annotation Processor
1. Simple Processor Example
package com.example.processor;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.Element;
import javax.tools.Diagnostic;
import java.util.Set;
// AutoService automatically registers the processor
@SupportedAnnotationTypes("com.example.annotations.SimpleAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class SimpleProcessor extends AbstractProcessor {
private Messager messager;
private Filer filer;
private Elements elementUtils;
private Types typeUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.filer = processingEnv.getFiler();
this.elementUtils = processingEnv.getElementUtils();
this.typeUtils = processingEnv.getTypeUtils();
messager.printMessage(Diagnostic.Kind.NOTE, "SimpleProcessor initialized");
}
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
if (roundEnv.processingOver() || annotations.isEmpty()) {
return false;
}
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements =
roundEnv.getElementsAnnotatedWith(annotation);
for (Element element : annotatedElements) {
processAnnotatedElement(element, annotation);
}
}
return true; // Claim these annotations
}
private void processAnnotatedElement(Element element, TypeElement annotation) {
String message = String.format("Processing element: %s with annotation: %s",
element.getSimpleName(), annotation.getSimpleName());
messager.printMessage(Diagnostic.Kind.NOTE, message, element);
// Example: Validate that annotation is only on classes
if (element.getKind() != ElementKind.CLASS) {
messager.printMessage(Diagnostic.Kind.ERROR,
"SimpleAnnotation can only be applied to classes", element);
return;
}
// Additional processing logic here
analyzeClass((TypeElement) element);
}
private void analyzeClass(TypeElement classElement) {
// Analyze the class structure
String className = classElement.getQualifiedName().toString();
messager.printMessage(Diagnostic.Kind.NOTE,
"Analyzing class: " + className);
// You can access class members, methods, fields, etc.
classElement.getEnclosedElements().forEach(member -> {
messager.printMessage(Diagnostic.Kind.NOTE,
"Class member: " + member.getSimpleName() + " (" + member.getKind() + ")");
});
}
}
2. Service Registration
META-INF/services/javax.annotation.processing.Processor:
com.example.processor.SimpleProcessor com.example.processor.BuilderProcessor
Code Generation with JavaPoet
1. Builder Pattern Generator
package com.example.processor;
import com.example.annotations.Builder;
import com.squareup.javapoet.*;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.*;
@SupportedAnnotationTypes("com.example.annotations.Builder")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class BuilderProcessor extends AbstractProcessor {
private Messager messager;
private Filer filer;
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.filer = processingEnv.getFiler();
this.elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements =
roundEnv.getElementsAnnotatedWith(annotation);
for (Element element : annotatedElements) {
if (element.getKind() == ElementKind.CLASS) {
generateBuilderClass((TypeElement) element);
}
}
}
return true;
}
private void generateBuilderClass(TypeElement classElement) {
try {
String className = classElement.getSimpleName().toString();
String builderClassName = className + "Builder";
String packageName = elementUtils.getPackageOf(classElement).getQualifiedName().toString();
// Collect all non-static fields
List<FieldSpec> builderFields = new ArrayList<>();
List<MethodSpec> setterMethods = new ArrayList<>();
List<ParameterSpec> constructorParams = new ArrayList<>();
List<CodeBlock> constructorAssignments = new ArrayList<>();
for (Element enclosed : classElement.getEnclosedElements()) {
if (enclosed.getKind() == ElementKind.FIELD &&
!enclosed.getModifiers().contains(Modifier.STATIC)) {
VariableElement field = (VariableElement) enclosed;
String fieldName = field.getSimpleName().toString();
TypeName fieldType = TypeName.get(field.asType());
// Builder field
builderFields.add(FieldSpec.builder(fieldType, fieldName)
.addModifiers(Modifier.PRIVATE)
.build());
// Setter method
setterMethods.add(MethodSpec.methodBuilder("with" + capitalize(fieldName))
.addModifier(Modifier.PUBLIC)
.returns(ClassName.get(packageName, builderClassName))
.addParameter(fieldType, fieldName)
.addStatement("this.$L = $L", fieldName, fieldName)
.addStatement("return this")
.build());
// Constructor parameter and assignment
constructorParams.add(ParameterSpec.builder(fieldType, fieldName).build());
constructorAssignments.add(CodeBlock.of("this.$L = $L", fieldName, fieldName));
}
}
// Build method
MethodSpec buildMethod = MethodSpec.methodBuilder("build")
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.get(packageName, className))
.addStatement("return new $T($L)",
ClassName.get(packageName, className),
getFieldNames(builderFields))
.build();
// Builder class
TypeSpec builderClass = TypeSpec.classBuilder(builderClassName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addFields(builderFields)
.addMethods(setterMethods)
.addMethod(buildMethod)
.build();
// Write the Java file
JavaFile javaFile = JavaFile.builder(packageName, builderClass)
.addFileComment("Generated code - do not modify!")
.build();
javaFile.writeTo(filer);
messager.printMessage(Diagnostic.Kind.NOTE,
"Generated builder: " + packageName + "." + builderClassName);
} catch (IOException e) {
messager.printMessage(Diagnostic.Kind.ERROR,
"Failed to generate builder: " + e.getMessage(), classElement);
}
}
private String capitalize(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
private String getFieldNames(List<FieldSpec> fields) {
List<String> names = new ArrayList<>();
for (FieldSpec field : fields) {
names.add(field.name);
}
return String.join(", ", names);
}
}
2. Custom Annotation
package com.example.annotations;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE) // Only needed during compilation
public @interface Builder {
String prefix() default "with";
}
Advanced Processor Examples
1. Validation Processor
package com.example.processor;
import com.example.annotations.Validate;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeKind;
import javax.tools.Diagnostic;
import java.util.Set;
@SupportedAnnotationTypes("com.example.annotations.Validate")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class ValidationProcessor extends AbstractProcessor {
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
}
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements =
roundEnv.getElementsAnnotatedWith(annotation);
for (Element element : annotatedElements) {
validateElement(element);
}
}
return true;
}
private void validateElement(Element element) {
switch (element.getKind()) {
case FIELD:
validateField((VariableElement) element);
break;
case METHOD:
validateMethod((ExecutableElement) element);
break;
case CLASS:
validateClass((TypeElement) element);
break;
default:
messager.printMessage(Diagnostic.Kind.WARNING,
"@Validate not supported on " + element.getKind(), element);
}
}
private void validateField(VariableElement field) {
Validate validate = field.getAnnotation(Validate.class);
// Check if field is private
if (!field.getModifiers().contains(Modifier.PRIVATE)) {
messager.printMessage(Diagnostic.Kind.WARNING,
"Validated fields should be private", field);
}
// Check nullability for reference types
if (field.asType().getKind() == TypeKind.DECLARED && !isNullable(validate)) {
messager.printMessage(Diagnostic.Kind.ERROR,
"Non-nullable field must be initialized", field);
}
}
private void validateMethod(ExecutableElement method) {
Validate validate = method.getAnnotation(Validate.class);
// Validate return type
if (method.getReturnType().getKind() == TypeKind.VOID && validate.notNull()) {
messager.printMessage(Diagnostic.Kind.ERROR,
"void method cannot be annotated with notNull", method);
}
// Validate parameters
for (VariableElement param : method.getParameters()) {
if (param.asType().getKind() == TypeKind.DECLARED && validate.notNull()) {
messager.printMessage(Diagnostic.Kind.WARNING,
"Consider adding null checks for parameters", method);
}
}
}
private void validateClass(TypeElement clazz) {
// Check if class has a no-arg constructor
boolean hasNoArgConstructor = clazz.getEnclosedElements().stream()
.filter(element -> element.getKind() == ElementKind.CONSTRUCTOR)
.map(element -> (ExecutableElement) element)
.anyMatch(constructor -> constructor.getParameters().isEmpty());
if (!hasNoArgConstructor) {
messager.printMessage(Diagnostic.Kind.WARNING,
"Validated class should have a no-argument constructor", clazz);
}
}
private boolean isNullable(Validate validate) {
return validate != null && !validate.notNull();
}
}
2. Singleton Pattern Generator
package com.example.processor;
import com.example.annotations.Singleton;
import com.squareup.javapoet.*;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;
import java.io.IOException;
@SupportedAnnotationTypes("com.example.annotations.Singleton")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class SingletonProcessor extends AbstractProcessor {
private Filer filer;
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.filer = processingEnv.getFiler();
this.elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements =
roundEnv.getElementsAnnotatedWith(annotation);
for (Element element : annotatedElements) {
if (element.getKind() == ElementKind.CLASS) {
generateSingleton((TypeElement) element);
}
}
}
return true;
}
private void generateSingleton(TypeElement classElement) {
try {
String className = classElement.getSimpleName().toString();
String singletonClassName = className + "Singleton";
String packageName = elementUtils.getPackageOf(classElement).getQualifiedName().toString();
// Singleton instance field
FieldSpec instanceField = FieldSpec.builder(
ClassName.get(packageName, singletonClassName),
"INSTANCE")
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("new $T()", ClassName.get(packageName, singletonClassName))
.build();
// Private constructor
MethodSpec constructor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PRIVATE)
.build();
// Get instance method
MethodSpec getInstanceMethod = MethodSpec.methodBuilder("getInstance")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(ClassName.get(packageName, singletonClassName))
.addStatement("return INSTANCE")
.build();
// Delegate methods to original class
TypeSpec singletonClass = TypeSpec.classBuilder(singletonClassName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addField(instanceField)
.addMethod(constructor)
.addMethod(getInstanceMethod)
.addMethod(generateGetValueMethod(classElement))
.build();
JavaFile javaFile = JavaFile.builder(packageName, singletonClass)
.addFileComment("Generated Singleton - do not modify!")
.build();
javaFile.writeTo(filer);
} catch (IOException e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Failed to generate singleton: " + e.getMessage(), classElement);
}
}
private MethodSpec generateGetValueMethod(TypeElement classElement) {
return MethodSpec.methodBuilder("getValue")
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.get(classElement.asType()))
.addStatement("return new $T()", ClassName.get(classElement.asType()))
.build();
}
}
Using Java Compiler Tree API (Advanced)
1. Custom Compilation Task
package com.example.processor;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;
import com.sun.tools.javac.api.BasicJavacTask;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Names;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import java.util.Set;
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class ASTProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
return false; // Not claiming any annotations
}
@Override
public void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
// Get the JavacTask from the processing environment
JavacTask javacTask = (JavacTask) processingEnv.getProcessingTask();
// Add a task listener to hook into compilation phases
javacTask.addTaskListener(new ASTTaskListener(javacTask));
}
private static class ASTTaskListener implements TaskListener {
private final TreeMaker treeMaker;
private final Names names;
public ASTTaskListener(JavacTask javacTask) {
Context context = ((BasicJavacTask) javacTask).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
}
@Override
public void started(TaskEvent e) {
// Called when a compilation phase starts
}
@Override
public void finished(TaskEvent e) {
if (e.getKind() == TaskEvent.Kind.PARSE) {
// AST is available after parsing
JCTree.JCCompilationUnit unit = (JCTree.JCCompilationUnit) e.getCompilationUnit();
processCompilationUnit(unit);
}
}
private void processCompilationUnit(JCTree.JCCompilationUnit unit) {
// Traverse and modify the AST
unit.accept(new TreeScanner() {
@Override
public void visitMethodDef(JCTree.JCMethodDecl methodDecl) {
super.visitMethodDef(methodDecl);
// Example: Add logging to methods
if (shouldAddLogging(methodDecl)) {
addLoggingStatement(methodDecl);
}
}
});
}
private boolean shouldAddLogging(JCTree.JCMethodDecl method) {
return method.getModifiers().getFlags().contains(com.sun.tools.javac.code.Flags.PUBLIC)
&& !method.getName().toString().equals("main");
}
private void addLoggingStatement(JCTree.JCMethodDecl method) {
// Create a logging statement
JCTree.JCExpression println = treeMaker.Apply(
com.sun.tools.javac.util.List.nil(),
treeMaker.Select(
treeMaker.Select(
treeMaker.Ident(names.fromString("System")),
names.fromString("out")
),
names.fromString("println")
),
com.sun.tools.javac.util.List.of(
treeMaker.Literal("Entering method: " + method.getName())
)
);
// Add the logging statement at the beginning of the method
method.body.stats = method.body.stats.prepend(
treeMaker.Exec(println)
);
}
}
}
Testing Compiler Plugins
1. Testing Framework Setup
package com.example.processor.test;
import com.google.testing.compile.Compilation;
import com.google.testing.compile.Compiler;
import com.google.testing.compile.JavaFileObjects;
import org.junit.jupiter.api.Test;
import javax.tools.JavaFileObject;
import java.util.Arrays;
import static com.google.testing.compile.CompilationSubject.assertThat;
import static com.google.testing.compile.Compiler.javac;
class BuilderProcessorTest {
@Test
void testBuilderGeneration() {
JavaFileObject source = JavaFileObjects.forSourceString("com.example.TestClass",
"package com.example;\n" +
"import com.example.annotations.Builder;\n" +
"@Builder\n" +
"public class TestClass {\n" +
" private String name;\n" +
" private int age;\n" +
"}");
Compilation compilation = javac()
.withProcessors(new com.example.processor.BuilderProcessor())
.compile(source);
assertThat(compilation).succeeded();
assertThat(compilation)
.generatedSourceFile("com.example.TestClassBuilder")
.hasSourceEquivalentTo(JavaFileObjects.forSourceString(
"com.example.TestClassBuilder",
"// Expected generated builder code here"));
}
@Test
void testValidationErrors() {
JavaFileObject source = JavaFileObjects.forSourceString("com.example.InvalidClass",
"package com.example;\n" +
"import com.example.annotations.Validate;\n" +
"public class InvalidClass {\n" +
" @Validate(notNull = true)\n" +
" public String publicField; // Should warn about private\n" +
"}"));
Compilation compilation = javac()
.withProcessors(new com.example.processor.ValidationProcessor())
.compile(source);
assertThat(compilation).hadWarningContaining("should be private");
}
}
2. Maven Testing Configuration
<dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> <dependency> <groupId>com.google.testing.compile</groupId> <artifactId>compile-testing</artifactId> <version>0.21.0</version> <scope>test</scope> </dependency> </dependencies>
Deployment and Usage
1. Maven Plugin Configuration
<!-- In consuming project's pom.xml --> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>11</source> <target>11</target> <annotationProcessorPaths> <path> <groupId>com.example</groupId> <artifactId>compiler-plugin</artifactId> <version>1.0.0</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build>
2. Command Line Usage
# Compile with annotation processor javac -cp processor.jar -processor com.example.processor.SimpleProcessor MyClass.java # Process annotations only javac -proc:only -processor com.example.processor.SimpleProcessor MyClass.java # Disable annotation processing javac -proc:none MyClass.java
Best Practices
- Incremental Processing: Design processors to handle incremental compilation
- Error Reporting: Use
Messagerfor clear error messages with proper element locations - Performance: Avoid expensive operations in processors
- Resource Management: Properly manage generated files and resources
- Compatibility: Support multiple Java versions when possible
- Testing: Thoroughly test processors with various input scenarios
Common Pitfalls
- Forgetting Service Registration: Ensure processors are properly registered in META-INF/services
- Infinite Loops: Avoid generating code that triggers the same processor again
- Memory Leaks: Don't store references to compilation elements between rounds
- Platform Dependencies: Be cautious with compiler-specific APIs (like Javac Tree API)
Java compiler plugins are powerful tools for code generation, validation, and analysis. They enable compile-time metaprogramming and can significantly improve code quality and developer productivity.