How do you generate code from a processor using the Filer (and JavaPoet), and how do you report errors with the Messager?
answer
- Filer.createSourceFile/createResource — the only output channel
- Pass originating elements for incremental-build correctness
- Same file name twice => FilerException; never overwrite source
- JavaPoet (TypeSpec/MethodSpec, $T imports) instead of string concat
- Messager.printMessage(ERROR, msg, element) fails build with a location; don't throw
basics
~20 sThe Filer creates new files: call filer.createSourceFile(name) and write Java text to it. JavaPoet is a library that builds the Java source for you with a typed API instead of string concatenation. For problems, use the Messager to print an ERROR tied to the offending element, which fails the build.
solid answer
~40 sTo emit code you ask the Filer (from ProcessingEnvironment) to create an output: createSourceFile("com.example.Foo_Gen", originatingElements) for a .java file, or createResource(...) for arbitrary resources. You then write the file's content through the returned Writer. Hand-writing Java as strings is painful, so most processors use JavaPoet (com.squareup.javapoet): you build TypeSpec/MethodSpec/FieldSpec objects and JavaFile.writeTo(filer) handles imports, formatting, and writing. Pass the originating elements so incremental build tools can invalidate generated output when sources change. Crucially, never generate a file twice in one compilation (FilerException) and don't try to overwrite existing source. For diagnostics, use the Messager: messager.printMessage(Diagnostic.Kind.ERROR, "message", element) reports an error attached to a source element, which gives a precise location and fails the build; WARNING/NOTE don't fail it. Reporting via Messager is strongly preferred over throwing exceptions, which surface as opaque compiler crashes.
code
java · 47 lines@SupportedAnnotationTypes("com.example.GenerateHello")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public final class HelloProcessor extends AbstractProcessor {
private Filer filer;
private Messager messager;
private final Set<String> generated = new HashSet<>();
@Override public synchronized void init(ProcessingEnvironment env) {
super.init(env);
this.filer = env.getFiler();
this.messager = env.getMessager();
}
@Override public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
for (Element e : roundEnv.getElementsAnnotatedWith(GenerateHello.class)) {
if (e.getKind() != ElementKind.CLASS) {
// Report, don't throw: precise location + non-fatal aggregation.
messager.printMessage(Diagnostic.Kind.ERROR,
"@GenerateHello only allowed on classes", e);
continue;
}
TypeElement type = (TypeElement) e;
String pkg = processingEnv.getElementUtils()
.getPackageOf(type).getQualifiedName().toString();
String name = type.getSimpleName() + "_Hello";
if (!generated.add(pkg + "." + name)) continue; // guard double-gen
MethodSpec hello = MethodSpec.methodBuilder("hello")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(String.class)
.addStatement("return $S", "hello from " + type.getSimpleName())
.build();
TypeSpec spec = TypeSpec.classBuilder(name)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(hello).build();
try {
// originatingElement -> incremental builds invalidate correctly
JavaFile.builder(pkg, spec).build().writeTo(filer);
} catch (IOException ex) {
messager.printMessage(Diagnostic.Kind.ERROR,
"Failed to write: " + ex.getMessage(), e);
}
}
return true;
}
}go deeper
Knows the Filer creates new files and that JavaPoet helps build Java source; can print a message via Messager.
Uses createSourceFile with originating elements, builds output with JavaPoet, and reports errors with Messager rather than throwing.
Handles double-generation/FilerException, understands incremental-build implications of originating elements, and validates with element-attached ERROR diagnostics.
Designs generation for incrementality and round-staging, weighs JavaPoet vs templates, and structures diagnostics for good DX across a large processor.
## The Filer: the only sanctioned output channel A processor must not just `new FileWriter(...)` somewhere — the build system needs to *know* what was generated (for incremental builds, output directories, re-processing). So javac gives you the **`Filer`** (obtained from `ProcessingEnvironment.getFiler()` in `init`). Three creation methods: - **`createSourceFile(name, originatingElements...)`** → a new `.java` source file. The returned object's `openWriter()`/`openOutputStream()` is where you write. The new file is **compiled and re-processed** in a later round. - **`createClassFile(...)`** → a `.class` file directly (rare). - **`createResource(location, pkg, relativeName, originatingElements...)`** → an arbitrary resource (e.g. a `META-INF` file, a properties file). ### Originating elements (don't skip them) Each create call accepts **originating elements** — the source Elements that *caused* this file to be generated. Incremental compilers (Gradle) use this to know that if `User.java` changes, `User_Gen.java` must be regenerated. Omitting them leads to stale generated output under incremental builds. ### The cardinal rules 1. **Generate a given file name at most once per compilation.** Calling `createSourceFile` twice for the same name throws **`FilerException`**. Track what you've emitted (instance state across rounds). 2. **You cannot overwrite or modify existing source** — that's the APT additive constraint. You emit *new* names (e.g. a prefix/suffix convention). ## Writing the source: strings vs JavaPoet You *can* write Java by concatenating strings into the Filer's `Writer`, but it's error-prone (imports, generics, escaping). The de-facto standard is **JavaPoet** (`com.squareup.javapoet`), a small library with a fluent, typed model: - `MethodSpec`, `FieldSpec`, `ParameterSpec`, `TypeSpec` (the class/interface), `JavaFile` (the file + package). - `$T` placeholders take `ClassName`/`TypeName` objects so JavaPoet **manages imports automatically** and avoids fully-qualified-name clutter. - `JavaFile.builder(pkg, typeSpec).build().writeTo(filer)` writes straight through the Filer, including originating elements if you set them. JavaPoet is not part of the JDK — it's a third-party dependency you add to your processor module. (Alternatives: KotlinPoet for Kotlin, or raw templates.) ## The Messager: diagnostics done right Validation belongs in the processor, and the way to surface a problem is the **`Messager`** (`processingEnv.getMessager()`), not exceptions. Call: ``` messager.printMessage(Diagnostic.Kind.ERROR, "@AutoValue must be on an abstract class", element); ``` - **`Diagnostic.Kind`**: `ERROR` (fails the build), `WARNING`, `MANDATORY_WARNING`, `NOTE`, `OTHER`. - Passing the offending **`Element`** (and optionally an `AnnotationMirror`/`AnnotationValue`) makes the IDE/compiler point at the exact line. Without it, the error has no location. - Reporting an `ERROR` lets the round finish and the compiler report *all* errors at once; **throwing** from `process` instead crashes the processor and produces an ugly, location-less compiler message. So: validate → `printMessage(ERROR, …)` → `return` early, rather than `throw`. ## Putting it together (typical flow) In `process`: for each annotated element, **validate** (report ERROR via Messager and skip on failure), **model** the desired output, **build** it with JavaPoet, and **write** via `JavaFile.writeTo(filer)` with originating elements. Guard against double-generation. Do summary/cross-cutting validation in the `processingOver()` round. ## Why this design Routing all output through the Filer keeps generation visible to the build (incrementality, re-processing); JavaPoet removes string-bug risk; Messager errors give precise, aggregatable, build-failing diagnostics without crashing the compiler.
- Why pass originating elements to the Filer?They tell incremental build tools which source(s) caused a generated file, so the tool regenerates it when those sources change and avoids stale output.
- Why report errors via Messager instead of throwing from process()?Messager ERROR attaches a precise source location, lets the compiler aggregate and report all errors in the round, and fails the build cleanly; throwing crashes the processor with an opaque, location-less message.
- What happens if you call createSourceFile twice with the same name in one compilation?The Filer throws FilerException. You must track generated names (in instance state) and guard against regenerating the same file within a compilation.
saying these in an interview costs you the question
- Writing files with new FileWriter instead of the Filer
- Throwing exceptions for validation instead of Messager ERROR (crashes the compiler, no location)
- Generating the same file name twice (FilerException)
- Forgetting originating elements, causing stale output in incremental builds
- Calling Messager ERROR but not passing the element, so there's no source location