For a long time, both users and providers were happy with runtime reflection access to annotations. Because it’s mainly focused on configuration, reflection occurs at startup time. In constrained environments, this is too much of a load for applications: the most well-known example of such an environment is the Android platform. One would want to have the fastest startup time there, and the startup-time reflection approach makes that slower.

An alternative to cope with that issue is to process annotations at compile-time. For that to happen, the compiler must be configured to use specific annotation processors. Those can have different outputs: simple files, generated code, etc. The tradeoff of that approach is that compilation takes a performance hit every time, but then startup time is not impacted.

One of the earliest frameworks that used this approach to generate code was Dagger: it’s a DI framework for Android. Instead of being runtime-based, it’s compile-time based. For a long time, compile-time code generation was limited to the Android ecosystem.

However, recently, back-end frameworks such as Quarkus and Micronaut also adopted this approach. The aim is to reduce application startup time through compile-time code generation in replacement of runtime introspection. Additionally, Ahead-of-Time compilation of the resulting bytecode to native code further reduces startup time, as well as memory consumption.

The world of annotation processors is huge: this section is a but a very small introduction so one can proceed further if wanted.

A processor is just a specific class that needs to be registered at compile-time. There are several ways to register them. With Maven, it’s just a matter of configuring the compiler plugin:

pom.xml <build> <plugins> <plugin> <groupId> org.apache.maven.plugins </groupId> <artifactId> maven-compiler-plugin </artifactId> <version> 3.8.1 </version> <configuration> <annotationProcessors> <annotationProcessor> ch.frankel.blog.SampleProcessor </annotationProcessor> </annotationProcessors> </configuration> </plugin> </plugins> </build>

The processor itself needs to implement Processor , but the abstract class AbstractProcessor implements most of its methods but process : in practice, it’s enough to inherit from AbstractProcessor . Here’s a very simplified diagram of the API:

Let’s create a very simple processor. It should only lists classes that are annotated with specific annotations. Real-world annotation processors would probably do something useful e.g. generate code, but this additional logic goes well beyond the scope of this post.

@SupportedAnnotationTypes ( "ch.frankel.blog.*" ) (1) @SupportedSourceVersion ( SourceVersion . RELEASE_8 ) public class SampleProcessor extends AbstractProcessor { @Override public boolean process ( Set <? extends TypeElement > annotations , (2) RoundEnvironment env ) { annotations . forEach ( annotation -> { (3) Set <? extends Element > elements = env . getElementsAnnotatedWith ( annotation ); (4) elements . stream () . filter ( TypeElement . class :: isInstance ) (5) . map ( TypeElement . class :: cast ) (6) . map ( TypeElement: : getQualifiedName ) (7) . map ( name -> "Class " + name + " is annotated with " + annotation . getQualifiedName ()) . forEach ( System . out :: println ); }); return true ; } }