How are Annotation Processors loaded?

Large projects can become complicated to build. One factor contributing to that is annotation processors. They can be loaded in many different ways. It can be hard to understand what Processors run and how seemingly unconnected changes affect that. I want to explain in this article how Annotation Processors are loaded and the potential pitfalls.

Java Compiler

Annotation Processors are loaded by the Java Compiler (javac). This Pseudocode demonstrates how.

public static List<AnnotationProcessor> getAnnotationProcessors(
      String[] args,
      List<AnnotationProcessor> programmaticallySetProcessors)
{     //source: javax.tools.JavaCompiler.CompilationTask#setProcessors(java.lang.Iterable)

   //source: com.sun.tools.javac.main.JavaCompiler.initProcessAnnotations()
   if (isOptionSet(args, "-proc:", "none") || !isRequested(args))
   {
      return Collections.emptyList();
   }

   return chooseProcessors(args, programmaticallySetProcessors);
}

//source: com.sun.tools.javac.main.JavaCompiler.explicitAnnotationProcessingRequested()
private static boolean isRequested(String[] args)
{
   return isOptionSet(args, "-processor") ||
          isOptionSet(args, "---processor-path") ||
          isOptionSet(args, "--processor-module-path") ||
          isOptionSet(args, "-proc:", "only") ||
          isOptionSet(args, "-proc:", "full") ||
          isOptionSet(args, "-A") ||
          isOptionSet(args, "-Xprint") ||
          hasLocation(ANNOTATION_PROCESSOR_PATH);
}

//source: com.sun.tools.javac.processing.JavacProcessingEnvironment.initProcessorIterator
private static List<AnnotationProcessor> chooseProcessors(
      String[] args,
      List<AnnotationProcessor> programmaticallySetProcessors)
{
   if (isOptionSet(args, "-Xprint"))(1)
   {
      return List.of(new PrintingProcessor());
   }
   if (programmaticallySetProcessors != null)(2)
   {
      return programmaticallySetProcessors;
   }

   List<AnnotationProcessor> processors = loadProcessors();

   List<String> processorNames = getOption(args, "-processor");(3)
   if (processorNames != null)
   {
      return processors.stream()
                       .filter(p -> processorNames.contains(p.getName()))
                       .toList();
   }
   return processors;
}

//source: com.sun.tools.javac.processing.JavacProcessingEnvironment#initProcessorLoader())
private static List<AnnotationProcessor> loadProcessors()
{
   if (hasLocation(ANNOTATION_PROCESSOR_MODULE_PATH))
   {
      return loadAnnotationProcessors(ANNOTATION_PROCESSOR_MODULE_PATH);(4)
   }
   if (hasLocation(ANNOTATION_PROCESSOR_PATH))
   {
      return loadAnnotationProcessors(ANNOTATION_PROCESSOR_PATH);(5)
   }
   return loadAnnotationProcessors(CLASS_PATH);(6)
}

Configurations

1 -Xprint

With this command line Option a build in javac Annotation Processor is used to print everything it sees about a class. This can be very useful to get familiar with the code model of annotation processing.

javac -Xprint HelloWorld.java
package io.determann.shadow.article.apt_loading;

public class HelloWorld {
   public static void main(String[] args) {
      System.out.println("Hello, World!");
   }
}

Annotation Processors can not access the content of methods. But default Constructors are present.

result
package io.determann.shadow.article.apt_loading;

public class HelloWorld {

  public HelloWorld();

  public static void main(java.lang.String[] args);
}
2 programmaticallySetProcessors The Java compiler has a programmatic Api where Annotation Processors can be set.
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
import javax.tools.JavaFileObject;
import java.util.List;

public class HelloCompiler {

   public static void main(String[] args) {

      JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
      List<JavaFileObject> javaFileObjects = createJavaFileObjects();

      JavaCompiler.CompilationTask compilationTask =
         compiler.getTask(null, null, null, null, null, javaFileObjects);

      //Set processors programaticly
      List<Processor> myProcessors = getProcessors();
      compilationTask.setProcessors(myProcessors);
      compilationTask.call();
   }
}
3 -processor With this command line Option a List of Annotation Processors can be set via their name. This is basically a filter for one of the other options or the classpath.
javac -processor MyAnnotationProcessor HelloWorld.java
4 --processor-module-path The module path for loading Annotation processors
5 --processor-path or -processorpath The path for loading Annotation processors.
6 --class-path The path for user class files and Annotation Processors.

Maven

During a build the build tool needs to invoke javac. This is how maven does it.

Without configuration

Not Modularised

Maven puts everything on the class-path including the source files that are being compiled.

Modularised

Every module that the application requires is placed on the module-path by Maven, while all other dependencies are placed on the class-path.

Configuration

Java Version Changes

Since java 23 Annotation Processing has to be explicitly requested and is no longer on per default. The official documentation is not all up to date.

Common Cases

Writing an Annotation Processor

Class-path scanning should be used with caution when writing a processor. The sources that are being compiled are on the class-path and javac will try to compile the Processor using itself.

Required Modularized Processors

If you want to use Annotation Processors to write your Processor both are modules and one requires the other, then the other processors will be put on the module-path and can not be automatically discovered. Some configuration is needed.

java >= 23

If you want to use Annotation Processors to write your Processors you have to enable them. I would recommend to explicitly configure them. Just using -proc:full may cause problems.

java < 23

You will need to tell javac that compiling a processor with itself doesn’t work.

Using Annotation Processors

Required Modularized Processors

If a Processor is modularised and required by the module that’s being compiled it will be put on the module-path and can not be automatically discovered. Some configuration is needed.

java >= 23

Since Java 23 Annotation Processing is disabled by default. -proc:full re-enables it.

java < 23

In java versions before 23, Annotation Processors are automatically detected and run without configuration.

Opinion

I would recommend to explicitly configure Annotation Processors in any case. Its more work, but leads to an easier understanding of the build process. It avoids some pitfalls. Who would expect an Annotation Processor to stop working, just because its now required?

Simplifications

  • Loading Annotation Processors is lazy

  • Before each round, except the last, javac tries to find more Processors

  • There are two class paths. one for javac and one for Annotation Processors in javac