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 checksCLASS: stored in class file; not visible at runtimeRUNTIME: 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
nullvalues; 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:
SOURCEorCLASSretention; runtime not required - Runtime frameworks:
RUNTIMEretention for reflection
Decision matrix:
- If only the compiler needs it →
SOURCE - If a processor generates code/resources → typically
SOURCEorCLASS - 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 pointProcessingEnvironment: services (types, elements, filer, messager, options)RoundEnvironment: elements for this round; incremental rounds until no new sourcesMessager: emit notes, warnings, errorsFiler: generate sources/resourcesElements/Types: model and type utilities
Processor lifecycle:
- Initialization:
init(ProcessingEnvironment) - For each round:
process(annotations, roundEnv) - 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)
Elementhierarchy:PackageElement,TypeElement(class/interface),ExecutableElement(method/ctor),VariableElement(field/param)TypeMirrorhierarchy:DeclaredType,PrimitiveType,ArrayType,TypeVariable,WildcardType- Utilities:
Elements: packages, names, annotation mirrors, docsTypes: 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.,
@Idrequires@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#createResourcefor 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
@Retentionbased on consumer (compiler vs runtime) - Be precise with
@Targetto prevent misuse - Validate inputs early using
Messager - Use
Elements/Typesfor robust symbol/type handling - Generate code via
Filerwith 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 tojavac -processor FilerException: Attempt to recreate a file: multiple rounds or duplicate generation → guard with state/idempotency- Types not found: use
Elements#getTypeElementand checknull; avoidClass.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. ````