skip to content

Walk through how you implement a processor with AbstractProcessor: which annotations/methods configure it, and how process() and RoundEnvironment drive the work?

level: middleimportance: must knowfreq 58%

answer

  1. Extend AbstractProcessor, override process()
  2. @SupportedAnnotationTypes + @SupportedSourceVersion (or latestSupported())
  3. init() gives Filer/Messager/Elements/Types — stash them
  4. RoundEnvironment.getElementsAnnotatedWith() per round
  5. return true = claim annotations; processingOver() = final round

basics

~10 s

Extend AbstractProcessor, declare which annotations you handle with @SupportedAnnotationTypes and the Java version with @SupportedSourceVersion. Override process(), where RoundEnvironment.getElementsAnnotatedWith() gives you the annotated code to read and generate from.

solid answer

~40 s

You normally extend AbstractProcessor rather than implementing Processor directly. You declare the annotations you care about with @SupportedAnnotationTypes (or override getSupportedAnnotationTypes) and the supported Java version with @SupportedSourceVersion (commonly latestSupported()). The compiler calls init(ProcessingEnvironment) once, giving you the Filer, Messager, Elements, and Types utilities, which you stash. Then it calls process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) once per round. Inside process you call roundEnv.getElementsAnnotatedWith(MyAnnotation.class) to get the annotated Elements, inspect them via the element/type model, and generate code with the Filer. Returning true 'claims' those annotations so other processors won't also handle them; returning false lets them pass through. process is invoked across multiple rounds; the final round has roundEnv.processingOver() == true and an empty element set, which is where you do any deferred or summary work.

go deeper

for a junior

Can name AbstractProcessor and that you override process(); recognizes @SupportedAnnotationTypes.

for a middle

Implements the full lifecycle: configuration annotations, init() tool capture, process() with getElementsAnnotatedWith, and understands multiple rounds.

for a senior

Explains the claim semantics of the return value, processingOver() usage, cross-round instance state, and latestSupported() rationale.

for a principal

Reasons about multi-processor cooperation, generation staging across rounds, incremental/Gradle isolating-vs-aggregating processor classification, and failure semantics.

## The contract: Processor vs AbstractProcessor The compiler talks to your code through the `javax.annotation.processing.Processor` interface. Implementing it raw is tedious, so you extend **`AbstractProcessor`**, a base class that implements the boilerplate (caching the environment, reading the configuration annotations) and leaves you one method to override: `process`. ## Step 1 — declare what you handle Two configuration annotations on your processor class: - **`@SupportedAnnotationTypes({"com.example.AutoValue"})`** — the fully-qualified names of the annotations this processor wants. Wildcards like `"com.example.*"` are allowed. (Equivalently, override `getSupportedAnnotationTypes()`.) - **`@SupportedSourceVersion(SourceVersion.RELEASE_17)`** — the newest Java language version your processor understands. Most processors override `getSupportedSourceVersion()` to return `SourceVersion.latestSupported()` so they don't emit a 'source version not supported' warning on newer JDKs. ## Step 2 — init(ProcessingEnvironment) The compiler calls `init` **once**, passing a **`ProcessingEnvironment`**. From it you obtain and store the tools you'll need every round: - **`Filer`** — to write generated files. - **`Messager`** — to print notes/warnings/errors. - **`Elements`** (`getElementUtils()`) — helpers for working with program elements (look up a type by name, get docs, package of an element, etc.). - **`Types`** (`getTypeUtils()`) — helpers for working with types (is-subtype, erasure, etc.). ## Step 3 — process(...) per round Signature: `boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)`. - **`annotations`**: the subset of *your* supported annotations that actually appear in the code being processed this round. - **`RoundEnvironment roundEnv`**: the window into the current round. Its key methods: - `getElementsAnnotatedWith(Class/TypeElement)` → the **Elements** annotated with that annotation this round. An **Element** is a node in the program's structure: a `TypeElement` (class/interface), `ExecutableElement` (method/constructor), `VariableElement` (field/parameter), etc. - `getRootElements()` → the top-level types being compiled this round. - `processingOver()` → `true` on the final, extra round after all source has been generated; the element set is empty here. Use it for summary/validation work that needs everything generated. - `errorRaised()` → whether a prior processor reported an error. ### The return value matters `process` returns a **boolean meaning 'claimed'**: - `true` → you *claim* the annotations in `annotations`; the compiler will **not** offer them to subsequent processors. - `false` → the annotations remain available to other processors. Most single-owner processors return `false` unless they truly own an annotation exclusively; over-claiming can starve other processors. ## Rounds, concretely Round 1: your annotated source is presented; you generate `Foo_Generated.java`. Round 2: that generated file is now part of compilation; if *it* carries annotations you handle, you see them. Rounds continue until a round generates nothing. Then one **final round** runs with `processingOver() == true`. ## A minimal skeleton See the code example. Note: state that must survive across rounds (e.g. "defer this element until a type it references is generated") is kept in instance fields, because the *same processor instance* is reused across all rounds of one compilation. ## Why this shape The per-round, claim-based, tool-injected design lets many independent processors cooperate on one compilation, generate in stages, and fail the build cleanly via the Messager — all without runtime cost.

  • What does returning true from process() mean, and when should you return false?
    true claims the annotations so no other processor handles them; false lets them pass through. Return false unless you exclusively own the annotation, to avoid starving cooperating processors.
  • Why override getSupportedSourceVersion() to return latestSupported()?
    Otherwise the compiler warns that your declared @SupportedSourceVersion is older than the JDK in use. latestSupported() opts out of that warning by claiming support for the running compiler's newest version.
  • How do you carry state between rounds?
    Store it in instance fields of the processor — the same instance is reused across all rounds of a single compilation, e.g. to defer elements whose dependencies aren't generated yet.

saying these in an interview costs you the question

  • Thinking process() is called only once (it's once per round)
  • Believing the boolean return means 'success' rather than 'claimed'
  • Forgetting @SupportedSourceVersion and emitting unsupported-version warnings on new JDKs
  • Trying to read processed elements during the processingOver() round (set is empty)
  • Assuming a fresh processor instance per round (it's reused; instance state persists)

context