In high-performance Java architectures, relying on runtime reflection (java.lang.reflect) introduces severe performance penalties: slow method invocation lookups, classloader locking overhead, inability of the JIT compiler to inline calls, and runtime failure on missing configuration.
Compile-Time Annotation Processing (standardized under JSR 269: Pluggable Annotation Processing API) executes custom code generators directly within javac. By inspecting Abstract Syntax Tree (AST) elements during compilation, annotation processors generate type-safe boilerplate, dependency injection graphs (Dagger), immutable value classes (AutoValue, Immutables), and JSON serializers (Moshi, Jackson) with zero runtime overhead and compile-time validation.
+-----------------------------------------------------------------------------------------------------------------------+
| METAPROGRAMMING APPROACHES IN JAVA |
+-----------------------------------------------------------------------------------------------------------------------+
| Technique | Execution Timing | Runtime Performance | Failure Point | Tooling Examples|
+------------------------+------------------------+----------------------------+----------------------+-----------------+
| Runtime Reflection | Application Startup/Run| Poor (No JIT inlining) | Runtime Exception | Spring Core |
| APT / JSR 269 | Compilation (`javac`) | Zero Overhead (Pure Java) | Build Compilation Err| Dagger, MapStruct|
| Bytecode Weaving (ASM) | Post-compile / Loadtime| Fast (Direct opcodes) | Bytecode Verification| ByteBuddy, AspectJ|
| Dynamic Proxies | Runtime | Moderate (Interface bounds)| Runtime Exception | JDK Proxy, CGLIB|
+-----------------------------------------------------------------------------------------------------------------------+
Annotation processing does not execute in a single linear pass. javac orchestrates processing through a series of Rounds:
+-----------------------------+
| Initial Source Files (.java)|
+--------------+--------------+
|
v
+-----------------------------+
| Parse & Enter: Build AST |
+--------------+--------------+
|
v
+-------------------> +-----------------------------+
| | Execute Processors (Round t)|
| +--------------+--------------+
| |
| +----------------------+----------------------+
| | |
| New Sources Generated? |
| | |
| [ YES ] [ NO ]
| | |
| v v
| +--------------------+ +--------------------+
| | Parse New Sources | | Final Round |
| | (Round t+1) | | (over = true) |
| +---------+----------+ +---------+----------+
| | |
+--------------+ v
+--------------------+
| Analyze & Generate |
| Final .class Files |
+--------------------+
Under standard JSR 269 specification, an annotation processor cannot modify existing source files or change already generated AST nodes; it can only inspect elements (TypeElement, ExecutableElement, VariableElement) and generate new source files (Filer.createSourceFile()) or write resource files (Filer.createResource()).
(Note: Frameworks like Project Lombok bypass this invariant by hacking the internal OpenJDK com.sun.tools.javac.tree.JCTree AST classes, which creates compiler vendor coupling).
Here is a complete, production-grade processor that generates builder classes for annotated models:
package com.wikantik.processor;
import com.google.auto.service.AutoService;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.PrintWriter;
import java.util.Set;
@AutoService(Processor.class)
@SupportedAnnotationTypes("com.wikantik.annotation.AutoBuilder")
@SupportedSourceVersion(SourceVersion.RELEASE_21)
public class BuilderProcessor extends AbstractProcessor {
private Filer filer;
private Messager messager;
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.filer = processingEnv.getFiler();
this.messager = processingEnv.getMessager();
this.elementUtils = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(AutoBuilder.class)) {
if (element.getKind() != ElementKind.CLASS) {
messager.printMessage(Diagnostic.Kind.ERROR, "@AutoBuilder can only be applied to classes", element);
return true;
}
TypeElement typeElement = (TypeElement) element;
generateBuilderClass(typeElement);
}
return true; // Claim annotation
}
private void generateBuilderClass(TypeElement typeElement) {
String className = typeElement.getSimpleName().toString();
String packageName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
String builderClassName = className + "Builder";
try {
JavaFileObject builderFile = filer.createSourceFile(packageName + "." + builderClassName, typeElement);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
out.println("package " + packageName + ";");
out.println("public final class " + builderClassName + " {");
out.println(" // Generated builder implementation for " + className);
out.println(" public static " + builderClassName + " builder() { return new " + builderClassName + "(); }");
out.println("}");
}
} catch (Exception e) {
messager.printMessage(Diagnostic.Kind.ERROR, "Failed to write builder: " + e.getMessage(), typeElement);
}
}
}