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.