Annotation processing in Java is a compile-time mechanism that allows you to generate new source code based on annotations present in your existing code.

Let’s see it in action. Imagine you have a simple class with a ToString annotation:

import com.example.annotations.ToString;

@ToString
public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

After compilation, using an annotation processor, this class will automatically gain a toString() method:

// Generated code
public class User {
    private String name;
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
               "name='" + name + '\'' +
               ", age=" + age +
               '}';
    }
}

This is achieved by a special kind of Java program called an annotation processor. It runs during the compilation phase, inspecting your code for specific annotations and then generating new .java files (or other resources) based on what it finds. The compiler then includes these generated files in the final compilation, treating them as if you had written them yourself.

The core problem annotation processing solves is boilerplate code. Repetitive code for tasks like generating getters/setters, equals(), hashCode(), toString(), or even entire DTOs, builders, or DAO implementations, can be eliminated. This leads to cleaner, more maintainable code and reduces the chances of manual errors.

Internally, an annotation processor is a Java class that implements the javax.annotation.processing.Processor interface. The Java compiler discovers these processors (usually via META-INF/services/javax.annotation.processing.Processor files in your JARs) and invokes them during the compilation lifecycle. The Processor has an init method and a process method. The process method is where the magic happens: it receives a set of RoundEnvironment objects, each representing a "round" of processing. Within a round, you can query the RoundEnvironment to find elements (classes, methods, fields, etc.) annotated with specific annotation types you’re interested in. For each annotated element, you can then use the javax.lang.model API to inspect its properties (name, type, modifiers, enclosing elements, etc.) and the javax.annotation.processing.Filer API to create new source files.

The key levers you control as a developer using annotation processing are:

  1. The Annotations: You define the custom annotations that will signal to your processor what code to generate. These are simple interfaces annotated with @Retention(RetentionPolicy.SOURCE) and @Target(...) to specify where they can be used.
  2. The Processor Logic: This is the core Java code that reads the annotations and generates output. You use the javax.lang.model API to navigate the Abstract Syntax Tree (AST) of your code and the Filer API to write the generated files.
  3. Compiler Configuration: You tell the Java compiler (e.g., javac, Maven, Gradle) to include your annotation processor. This is typically done by adding the annotation processor library as a compile-time dependency.

A common misconception is that annotation processors are complex reflection-like tools. In reality, they operate before runtime and interact with the compiler’s internal model of your code. They don’t execute your application’s logic; they generate code that will be compiled and executed later. This means they have no runtime performance overhead and can provide powerful compile-time guarantees.

The Filer API, when used to create source files, has a subtle but important behavior: it will refuse to overwrite existing files. If your processor attempts to generate a file that already exists and is not generated by the processor itself, it will throw an FilerException. This forces you to carefully manage generated code and avoid conflicts with manually written code.

The next concept to explore is how to handle complex relationships between annotated elements, such as inheritance and interfaces, within your annotation processor.

Want structured learning?

Take the full Java course →