Java Annotation Processing: JSR 269 AST Code Generation and Bytecode Mechanics

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.


1. Quick-Reference: Annotation Processing vs. Reflection vs. Bytecode Weaving

+-----------------------------------------------------------------------------------------------------------------------+
|                                           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|
+-----------------------------------------------------------------------------------------------------------------------+

2. The JSR 269 Compiler Lifecycle: Multi-Round Processing

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 |
                                                   +--------------------+

The Invariant of Source Immutability

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).


3. Implementing a Custom JSR 269 Processor

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);
        }
    }
}

References

  1. Oracle Corporation. (2024). JSR 269: Pluggable Annotation Processing API Specification. JCP.
  2. Goetz, B., et al. (2006). Java Concurrency in Practice. Addison-Wesley.
  3. Bruneton, E., Lenglet, R., & Coupaye, T. (2002). ASM: a code manipulation tool to implement adaptable systems.