Processing Annotations in Java: Runtime vs Compile-Time with Examples and Best Practices

Illustration for Processing Annotations in Java: Runtime vs Compile-Time with Examples and Best Practices
By Last updated:

A common misconception among developers is thinking that all annotations in Java are processed the same way. Many assume annotations only work at runtime with reflection, missing out on the power of compile-time processing—where frameworks and libraries generate code or enforce rules before the program even runs.

For example:

  • Runtime annotations power frameworks like Spring (@Autowired, @Transactional) and Hibernate (@Entity).
  • Compile-time annotations power tools like Lombok (generates getters/setters) and MapStruct (generates mappers).

Confusing these two leads to errors such as expecting a Lombok annotation (@Data) to be available at runtime when in fact it’s removed after code generation.

Think of compile-time annotation processing as baking ingredients into a cake before serving, while runtime processing is like adding toppings just before eating. Both are powerful but serve very different purposes.


Compile-Time Annotation Processing

Compile-time annotation processing uses the JSR 269 annotation processing API (javax.annotation.processing).

Example: Custom Compile-Time Processor

import javax.annotation.processing.*;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;
import java.util.Set;

@SupportedAnnotationTypes("MyAnnotation")
@SupportedSourceVersion(javax.lang.model.SourceVersion.RELEASE_11)
public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
                "Processing: " + element.toString());
        }
        return true;
    }
}
  • Runs during compilation.
  • Can generate new source files or fail compilation if rules are violated.

Real-World Use

  • Lombok generates boilerplate code at compile-time.
  • MapStruct generates mapper implementations.

Runtime Annotation Processing

Runtime annotation processing relies on the Reflection API (java.lang.reflect).

Example: Processing Runtime Annotation

import java.lang.annotation.*;
import java.lang.reflect.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface LogExecution { }

class Service {
    @LogExecution
    public void doWork() {
        System.out.println("Working...");
    }
}

public class RuntimeProcessor {
    public static void main(String[] args) throws Exception {
        for (Method method : Service.class.getDeclaredMethods()) {
            if (method.isAnnotationPresent(LogExecution.class)) {
                System.out.println("Executing: " + method.getName());
                method.invoke(new Service());
            }
        }
    }
}
  • Uses reflection at runtime.
  • Behavior can be dynamically changed based on annotations.

Real-World Use

  • Spring scans annotations like @Component and @Autowired at runtime.
  • JUnit finds and executes methods annotated with @Test.

Key Differences Between Compile-Time and Runtime Processing

Aspect Compile-Time Runtime
When processed During compilation During program execution
Tools used JSR 269 API, annotation processors Reflection API
Use cases Code generation, validation, enforcing rules Dependency injection, ORM mapping, logging, testing
Performance No runtime overhead Adds runtime cost
Examples Lombok, MapStruct Spring, Hibernate, JUnit

📌 What's New in Java Versions?

  • Java 5 – Introduced annotations and reflection support.
  • Java 6 – JSR 269 (Annotation Processing API) standardized compile-time processing.
  • Java 8 – Repeatable annotations and type annotations introduced.
  • Java 9 – Module system introduced stricter reflective access rules.
  • Java 11 – No major updates for annotation processing.
  • Java 17 – No significant changes.
  • Java 21 – No significant changes.

Pitfalls and Best Practices

Pitfalls

  • Expecting compile-time annotations to exist at runtime (@Data in Lombok).
  • Overusing runtime reflection, causing performance degradation.
  • Complex annotation processors slowing down compilation.

Best Practices

  • Use compile-time processing for code generation and validation.
  • Use runtime processing for dynamic, framework-driven behavior.
  • Always specify @Retention properly (RUNTIME vs CLASS).
  • Document annotation usage for team members to avoid misuse.

Summary + Key Takeaways

  • Compile-time processing happens during compilation and is ideal for code generation and static validation.
  • Runtime processing happens while the program runs, ideal for framework-driven behavior.
  • Frameworks like Lombok and MapStruct rely on compile-time; frameworks like Spring, Hibernate, and JUnit rely on runtime.
  • Understanding the difference prevents errors and improves application performance and maintainability.

FAQ

  1. What is JSR 269?
    A specification that defines the standard API for compile-time annotation processing.

  2. Can an annotation be both compile-time and runtime?
    Yes, depending on how it’s defined (RetentionPolicy.CLASS vs RUNTIME).

  3. Why doesn’t Lombok work with reflection?
    Lombok generates code at compile-time; its annotations don’t exist at runtime.

  4. Which is faster: compile-time or runtime annotation processing?
    Compile-time is faster at runtime since no reflection is involved.

  5. Can I write my own annotation processor?
    Yes, using the javax.annotation.processing package.

  6. What’s the difference between RetentionPolicy.CLASS and RUNTIME?
    CLASS keeps annotations in bytecode but discards them at runtime, RUNTIME retains them for reflection.

  7. How does Spring Boot use annotation processing?
    It uses runtime reflection to scan classes annotated with @SpringBootApplication, @Service, etc.

  8. Can compile-time processing generate new classes?
    Yes, processors can generate Java source files or bytecode.

  9. What happens if a processor fails at compile-time?
    Compilation fails, preventing invalid code from running.

  10. When should I avoid runtime annotation processing?
    Avoid it in performance-critical loops; prefer compile-time checks when possible.