38.1 Annotation Processing Modern

Define custom annotations and generate boilerplate code at compile time using annotation processors.

Declaring Annotations

import java.lang.annotation.*;

````markdown
### 38.1 Modern Annotations and Processing Fundamentals

Build robust annotations and generate boilerplate at compile time with JSR 269 (annotation processing). This section covers annotation design, meta-annotations, targets and retention, type annotations, repeatable annotations, and a modern overview of the annotation processing pipeline with practical, end‑to‑end examples.

---

#### 38.1.1 Why Annotations (and When Not To)

- Express metadata that tools can read (compile-time and/or runtime)
- Enable cross-cutting concerns: mapping, validation, DI, serialization, testing
- Drive code generation to remove boilerplate (builders, adapters, registries)
- Replace naming conventions with explicit metadata

Use annotations when:
- Metadata is stable and broadly applicable
- You need tooling (compiler, processor, framework) to act on it
- You can validate misuse early (compile-time)

Prefer alternatives when:
- Behavior differs per instance (consider configuration/strategies instead)
- Complex logic is required (runtime behavior, not static metadata)
- Data belongs in external config (environment-driven concerns)

---

#### 38.1.2 Meta‑Annotations: `@Retention`, `@Target`, `@Documented`, `@Inherited`, `@Repeatable`

```java
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)      // or CLASS/SOURCE
@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD })
@Documented                               // include in Javadoc
@Inherited                                // inherited by subclasses (type-level only)
public @interface Entity {
  String table() default "";
}
  • @Retention:
    • SOURCE: removed by compiler; ideal for compile‑time checks
    • CLASS: stored in class file; not visible at runtime
    • RUNTIME: available via reflection
  • @Target: where the annotation can be used (type, field, method, parameter, constructor, local variable, annotation type, package, module, type use, type parameter)
  • @Documented: included in Javadoc
  • @Inherited: inherited on subclasses (only for class annotations)
  • @Repeatable: allows multiple instances of an annotation on the same element

Repeatable example:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Roles.class)
public @interface Role { String value(); }

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Roles { Role[] value(); }

@Role("reader")
@Role("editor")
public class DocumentService {}

38.1.3 Annotation Elements: Types, Defaults, and Constraints

Legal element types:

  • Primitive types, String, Class<?>, enum, annotation, arrays of these
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cacheable {
  String cacheName();               // required
  int ttl() default 60;             // seconds
  Class<?> keyType() default Object.class;
}

Usage with defaults and arrays:

@Cacheable(cacheName = "users")
public User findUser(long id) { ... }

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Indexes {
  Index[] value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Index {
  String[] columns();
  boolean unique() default false;
}

@Indexes({
  @Index(columns = {"email"}, unique = true),
  @Index(columns = {"last_name", "first_name"})
})
class Person {}

Constraints and pitfalls:

  • No null values; use sentinel (e.g., empty string) or optional wrapper annotation
  • Values must be compile‑time constants
  • Avoid large arrays in runtime annotations (bytecode size)

38.1.4 Type Annotations (JSR 308)

Annotate any use of a type: type arguments, type casts, implements, throws, arrays.

import java.lang.annotation.*;

@Target({ ElementType.TYPE_USE, ElementType.TYPE_PARAMETER })
@Retention(RetentionPolicy.CLASS)
public @interface NonEmpty {}

List<@NonEmpty String> names;       // annotate type argument
Comparable<@NonEmpty String> cmp;

void f(Object o) {
  String s = (@NonEmpty String) o; // annotate cast
}

Tooling (e.g., the Checker Framework) can enforce constraints for type usages.


38.1.5 Runtime vs Compile‑Time Processing: Choosing Retention Wisely

  • Compile‑time only checks: SOURCE + annotation processor validation (fast, zero runtime overhead)
  • Code generation: SOURCE or CLASS retention; runtime not required
  • Runtime frameworks: RUNTIME retention for reflection

Decision matrix:

  • If only the compiler needs it → SOURCE
  • If a processor generates code/resources → typically SOURCE or CLASS
  • If a framework at runtime reads it → RUNTIME

38.1.6 Modern Annotation Processing (JSR 269) Overview

The Java compiler service for processing annotations exposes:

  • Processor/AbstractProcessor: entry point
  • ProcessingEnvironment: services (types, elements, filer, messager, options)
  • RoundEnvironment: elements for this round; incremental rounds until no new sources
  • Messager: emit notes, warnings, errors
  • Filer: generate sources/resources
  • Elements/Types: model and type utilities

Processor lifecycle:

  1. Initialization: init(ProcessingEnvironment)
  2. For each round: process(annotations, roundEnv)
  3. End: roundEnv.processingOver() is true → finalization

38.1.7 Minimal End‑to‑End Example: @Entity*Meta Codegen

Annotation definition:

package com.example.meta;

import java.lang.annotation.*;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface Entity {
  String table() default "";
}

Usage:

package com.acme.domain;

import com.example.meta.Entity;

@Entity(table = "users")
public class User {
  String name;
  int age;
}

Processor skeleton:

package com.example.meta.processor;

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

@SupportedAnnotationTypes("com.example.meta.Entity")
@SupportedSourceVersion(SourceVersion.RELEASE_21)
public class EntityProcessor extends AbstractProcessor {
  @Override
  public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
  }

  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    for (Element el : roundEnv.getElementsAnnotatedWith(
        processingEnv.getElementUtils().getTypeElement("com.example.meta.Entity"))) {
      if (el.getKind() != ElementKind.CLASS) {
        processingEnv.getMessager().printMessage(
          Diagnostic.Kind.ERROR, "@Entity only on classes", el);
        continue;
      }
      TypeElement type = (TypeElement) el;
      generateMetaClass(type);
    }
    return true; // handled
  }

  private void generateMetaClass(TypeElement type) {
    String pkg = processingEnv.getElementUtils().getPackageOf(type).getQualifiedName().toString();
    String simple = type.getSimpleName().toString();
    String metaName = simple + "Meta";
    String qualified = pkg.isEmpty() ? metaName : pkg + "." + metaName;
    try (var writer = processingEnv.getFiler()
        .createSourceFile(qualified, type)
        .openWriter()) {
      writer.append("package ").append(pkg).append(";\n\n")
          .append("public final class ").append(metaName).append(" {\n")
          .append("  public static final String TABLE = \"")
          .append(resolveTable(type)).append("\";\n")
          .append("}\n");
    } catch (Exception e) {
      processingEnv.getMessager().printMessage(
        Diagnostic.Kind.ERROR, "Generation failed: " + e.getMessage(), type);
    }
  }

  private String resolveTable(TypeElement type) {
    for (AnnotationMirror am : type.getAnnotationMirrors()) {
      if (am.getAnnotationType().toString().equals("com.example.meta.Entity")) {
        for (var e : am.getElementValues().entrySet()) {
          if (e.getKey().getSimpleName().contentEquals("table")) {
            return e.getValue().getValue().toString();
          }
        }
      }
    }
    return type.getSimpleName().toString().toLowerCase() + "s";
  }
}

Registration options:

1) Services file: META-INF/services/javax.annotation.processing.Processor

com.example.meta.processor.EntityProcessor

2) AutoService (Google):

@com.google.auto.service.AutoService(Processor.class)
public class EntityProcessor extends AbstractProcessor { ... }

Compiling:

javac -cp auto-service-1.1.1.jar \
    -processor com.example.meta.processor.EntityProcessor \
    com/acme/domain/User.java

Expected generated file:

package com.acme.domain;

public final class UserMeta {
  public static final String TABLE = "users";
}

38.1.8 Elements & Types Essentials (Quick Primer)

  • Element hierarchy: PackageElement, TypeElement (class/interface), ExecutableElement (method/ctor), VariableElement (field/param)
  • TypeMirror hierarchy: DeclaredType, PrimitiveType, ArrayType, TypeVariable, WildcardType
  • Utilities:
    • Elements: packages, names, annotation mirrors, docs
    • Types: assignability, erasure, subtyping checks, type construction

Usages:

TypeElement clazz = (TypeElement) el;
for (Element member : processingEnv.getElementUtils().getAllMembers(clazz)) {
  if (member.getKind() == ElementKind.FIELD) {
    VariableElement field = (VariableElement) member;
    TypeMirror t = field.asType();
    boolean isString = processingEnv.getTypeUtils()
      .isSameType(t, processingEnv.getElementUtils()
        .getTypeElement("java.lang.String").asType());
    // ...
  }
}

38.1.9 Validating Annotation Usage (Fail Fast)

Emit compiler errors/warnings via Messager:

processingEnv.getMessager().printMessage(
  javax.tools.Diagnostic.Kind.ERROR,
  "@Entity must be top-level, non-abstract class",
  el
);

Common checks:

  • Enforce applicable ElementKind
  • Visibility (e.g., public class)
  • Disallow generic type params where undesired
  • Ensure field types are supported (e.g., String, int, custom mappers)
  • Cross‑annotation consistency (e.g., @Id requires @Entity)

38.1.10 Code Generation with JavaPoet (Quick Start)

import com.squareup.javapoet.*;
import javax.lang.model.element.Modifier;

TypeSpec meta = TypeSpec.classBuilder("UserMeta")
  .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
  .addField(FieldSpec.builder(String.class, "TABLE", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
    .initializer("$S", "users").build())
  .build();

JavaFile javaFile = JavaFile.builder("com.acme.domain", meta).build();
javaFile.writeTo(processingEnv.getFiler());

Guidelines:

  • Prefer JavaPoet for escaping and formatting
  • Use Filer#createResource for non‑source files (e.g., service descriptors)
  • Always include originating elements to support incremental builds

38.1.11 Rounds, Idempotency, and Incremental Processing Basics

  • Processors may run in multiple rounds
  • Generate sources/resources deterministically
  • Avoid re‑generating the same files (check existence or be idempotent)
  • Declare incremental support in Gradle (advanced; see 38.5)

Idempotent pattern:

var fo = processingEnv.getFiler().createSourceFile(qualifiedName, origin);
try (var w = fo.openWriter()) { /* write exactly once */ }

38.1.12 JPMS (Modules) Basics for Processors

  • Processors themselves usually live on the annotationProcessor path (not the module path)
  • If using modules, ensure annotation types are visible to compilation units
  • Opening packages is not normally needed for processors (they operate on source/bytecode models)

38.1.13 Testing Processors (Smoke Test)

Use Google compile-testing to verify diagnostics and generated sources:

import com.google.testing.compile.*;
import org.junit.jupiter.api.Test;
import static com.google.testing.compile.Compiler.javac;
import static com.google.testing.compile.JavaFileObjects.forSourceString;

class EntityProcessorTest {
  @Test void generatesMeta() {
  var input = forSourceString("com.acme.domain.User",
    "package com.acme.domain;\n" +
    "@com.example.meta.Entity(table=\"users\") class User {}\n");

  javac().withProcessors(new com.example.meta.processor.EntityProcessor())
    .compile(input)
    .generatedSourceFile("com.acme.domain.UserMeta")
    .hasSourceEquivalentTo(forSourceString("com.acme.domain.UserMeta",
    "package com.acme.domain;\n" +
    "public final class UserMeta { public static final String TABLE=\"users\"; }\n"));
  }
}

38.1.14 Practical Checklist

  • Choose correct @Retention based on consumer (compiler vs runtime)
  • Be precise with @Target to prevent misuse
  • Validate inputs early using Messager
  • Use Elements/Types for robust symbol/type handling
  • Generate code via Filer with originating elements
  • Keep generation deterministic and idempotent
  • Add clear compiler messages with suggestions
  • Provide quick‑start docs for users (options, how to enable)

38.1.15 Troubleshooting Quick Guide

  • Missing processor execution? Ensure it’s on annotationProcessorPath (Gradle/Maven) or supplied to javac -processor
  • FilerException: Attempt to recreate a file: multiple rounds or duplicate generation → guard with state/idempotency
  • Types not found: use Elements#getTypeElement and check null; avoid Class.forName
  • Incremental build issues: attach originating elements and avoid global filesystem scans
  • IDE vs CLI mismatch: ensure IDE uses same processor path and options

This section established a solid foundation: annotation design, retention/targets, type/ repeatable annotations, and a complete overview of modern processing. In the next section (38.2), we’ll dive deeper into the Processor API: Elements/Types model, messages, files, rounds, and incremental processing strategies. ````