Java Annotation Processing
Annotation processing is Java's compile-time code generation mechanism. Annotated source code is read by an annotation processor that emits additional source code or modifies bytecode. The result is the developer-facing benefits of reflection (less boilerplate) without the runtime cost.
This page is about how annotation processing works, the major processors that have stuck, and when writing your own pays.
How it works
The Java compiler invokes annotation processors during compilation:
1. Compiler reads annotated source
2. Processors registered via `META-INF/services/javax.annotation.processing.Processor` get invoked
3. Each processor reads the AST, can emit new source files
4. New sources go through the same compilation rounds
The output: code generated at build time that the user never writes manually but uses directly.
The major processors
Lombok
The most-used Java annotation processor. Annotations like `@Getter`, `@Setter`, `@Builder`, `@Data`, `@Slf4j` generate the corresponding boilerplate at compile time.
```java
@Data
public class User {
private final String email;
private int loginCount;
}
// Lombok generates:
// - constructor
// - getEmail(), getLoginCount(), setLoginCount()
// - equals(), hashCode(), toString()
```
The trade-offs:
- Less boilerplate to read and maintain
- IDE support requires a Lombok plugin
- Some operations modify bytecode in non-standard ways
- Records (Java 14+) replace much of Lombok's value-object use case
For new code, prefer records over `@Data`. Lombok still has uses (`@Slf4j`, `@Builder` on non-records, `@With`), but the case for `@Data` has weakened.
MapStruct
Generates type-safe mappers between similar types — typically DTO ↔ entity:
```java
@Mapper
public interface OrderMapper {
OrderDTO toDto(Order order);
Order toEntity(OrderDTO dto);
}
```
MapStruct generates the implementation at compile time. Faster than reflection-based mapping (e.g., ModelMapper), with errors at compile time rather than runtime.
Dagger / Hilt
Compile-time dependency injection. Faster than Spring at startup; generates code that does the wiring. Common in Android and performance-sensitive backends.
The trade-off: less flexible than runtime DI; the dependency graph must be expressible at compile time.
Annotation processors in frameworks
Many frameworks use annotation processors:
- **JPA**: validation of `@Entity` annotations, generation of static metamodel classes
- **Spring**: Spring Boot's auto-configuration uses annotation processing for configuration metadata
- **Immutables**: generates immutable value classes from interface declarations
- **AutoValue**: similar to Immutables; alternative pattern
When to write your own
Most teams should not. The cases where it pays:
- **Repeated boilerplate that doesn't fit existing processors.** Maybe domain-specific value types or DTO patterns.
- **Compile-time validation.** Catch invalid annotation usage at build time rather than runtime.
- **Code generation for repetitive APIs.** REST controllers, CLI commands, etc. derived from a single source of truth.
The cost:
- Annotation processors are non-trivial to write
- Debugging generated code is harder than handwritten code
- Build complexity increases
Patterns to avoid
- **Annotation processing for runtime behavior.** Write the code if you mean it.
- **Generated code that hides important behavior.** When something goes wrong, the error message points at code the user didn't write.
- **Processors that depend on order of invocation.** Round-based compilation makes this fragile.
- **Stale generated sources.** Old generated code that doesn't match annotations causes compile failures or weird runtime behavior.
A modern reasonable position
For most Java teams:
- Use Lombok selectively (`@Slf4j`, `@Builder`); prefer records for value objects
- Use MapStruct for non-trivial DTO mapping
- Don't write annotation processors unless the use case is clear
- Prefer compile-time generation over runtime reflection where the choice is clean
Common failure patterns
- **Lombok everywhere without considering records.** Records often beat Lombok now.
- **MapStruct for trivial mappings.** Sometimes a simple constructor is clearer than annotation-driven generation.
- **Annotation processing as a maintenance burden.** Generated code that nobody understands.
- **IDE issues.** Some processors require IDE plugins; setup friction.
- **Build dependency confusion.** Processor JARs vs. generated-code JARs; getting the Maven scope wrong.
Further Reading
- [JavaReflectionAndProxies](JavaReflectionAndProxies) — Runtime alternative
- [JavaTwentyOneFeatures](JavaTwentyOneFeatures) — Records reduce Lombok need
- [MavenMultiModuleProjects](MavenMultiModuleProjects) — Build-tool annotation processor configuration
- [JavaBuildToolComparison](JavaBuildToolComparison) — Annotation processor support across tools
- [Java Hub](JavaHub) — Cluster index