An annotation processor inspects compiler elements during processing rounds and can reject a declaration with a compile-time diagnostic.
Java annotation processing: reject invalid declarations during compilation
This program targets Java 8 without preview flags. Use a full JDK for compiler-based examples.
A marker needs a consumer
A source annotation records an endpoint path, but the annotation itself does not validate that path. The processor reads the annotated type and emits an error when the value does not begin with a slash. That turns a declaration rule into a compiler check rather than a comment that reviewers must remember.
The program supplies the processor explicitly through the JavaCompiler API. It runs processing only against in-memory source units. No provider discovery or generated-source packaging is implied; command-line builds need their own processor registration and processor-path configuration.
Respect processing rounds and diagnostics
A processor can be called over multiple rounds. It should claim only annotation types that it owns, and avoid generating the same file repeatedly. This fixture does not generate files. It rejects one annotated type and accepts another, then checks the compiler task result instead of searching human-formatted diagnostic text.
Processing code runs with the build process privileges. A downloaded processor is executable build-time code, even if its dependency is described as tooling. Keep processor versions and inputs reviewable. A full JDK is required here because ToolProvider may not supply a compiler in a reduced runtime.
Working program
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URI;
import java.util.Collections;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
public class CompilerRouteAudit {
@Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE)
public @interface Route { String value(); }
static class RouteProcessor extends AbstractProcessor {
public Set<String> getSupportedAnnotationTypes() { return Collections.singleton(Route.class.getCanonicalName()); }
public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment round) {
for (javax.lang.model.element.Element endpoint : round.getElementsAnnotatedWith(Route.class))
if (!endpoint.getAnnotation(Route.class).value().startsWith("/"))
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Route must start with a slash", endpoint);
return true;
}
}
static boolean check(String path) throws Exception {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) throw new IllegalStateException("Full JDK required");
String text = "@CompilerRouteAudit.Route(\""+path+"\") class ReceiptEndpoint {}";
JavaFileObject unit = new SimpleJavaFileObject(URI.create("string:///ReceiptEndpoint.java"), JavaFileObject.Kind.SOURCE) {
public CharSequence getCharContent(boolean ignoreErrors) { return text; }
};
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
try (StandardJavaFileManager files = compiler.getStandardFileManager(diagnostics, null, null)) {
JavaCompiler.CompilationTask task = compiler.getTask(null, files, diagnostics,
java.util.Arrays.asList("-proc:only", "-classpath", System.getProperty("java.class.path")),
null, Collections.singletonList(unit));
task.setProcessors(Collections.singletonList(new RouteProcessor()));
return task.call();
}
}
public static void main(String[] args) throws Exception {
System.out.println(check("/receipts")); System.out.println(check("receipts"));
}
}Output
true
falseCosts and boundaries
Processing work follows the number of compiler elements visited and any generated output. This processor scans its annotated types once per relevant round and generates nothing. It validates only the slash prefix; uniqueness, HTTP behavior and a complete routing grammar are separate application checks. The compiler runs inside this test process without compiling arbitrary user submissions.
Common Mistakes
- Declaring an annotation does not automatically run a processor.
- Do not regenerate the same source file in every round.
- A compile-time syntax check does not establish runtime endpoint behavior.
Read next
Retention and annotation consumers, Declaration layout, Compiler and module boundaries.
