Annotation Processing

What is this?

Annotation-processors run at compile time. They process representations of the source code that is being compiled. They also can create new Files, including source files, and raise errors or warnings.

  • Featureset 6/10

  • Performance 10/10

  • Correctness 9/10

  • Usability 3/10

  • Maintainability 9/10

Getting started

It’s normal annotation processing with a better API. The setup is the same. The differences only start when extending`io.determann.shadow.api.annotation_processing.Ap.Processor` instead of javax.annotation.processing.AbstractProcessor. A good starting point for your own processor is AnnotationProcessingContext.getAnnotatedWith(java.lang.String).

Setup

We will create everything you need to get started with your first annotation processor using maven in this setup.

1) Two Modules

The annotation processor must be compiled first. Create two maven modules. One having the code to process and one containing the annotation processor.

processor module
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>io.determann</groupId>
    <artifactId>processor-example</artifactId>
    <version>0.3.0-SNAPSHOT</version>
</project>
processed module
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>io.determann</groupId>
    <artifactId>processed-example</artifactId>
    <version>0.3.0-SNAPSHOT</version>
</project>

2) Dependencies

The processor needs to depend on the shadow-api
        <dependency>
            <groupId>io.determann</groupId>
            <artifactId>shadow</artifactId>
            <version>0.3.0-SNAPSHOT</version>
        </dependency>
And the processed module needs to depend on the processor module
        <dependency>
            <groupId>io.determann</groupId>
            <artifactId>processor-example</artifactId>
            <version>0.3.0-SNAPSHOT</version>
        </dependency>

3) Processor paths

The module being processed needs to know the module it’s processed by
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>io.determann</groupId>
                            <artifactId>processor-example</artifactId>
                            <version>0.3.0-SNAPSHOT</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>

4) Disable annotation processing

Disable annotation processing in the processor module, otherwise the annotation processor would be used to process itself
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                    <!--don't compile the annotation processor using the annotation processor-->
                    <compilerArgument>-proc:none</compilerArgument>
                </configuration>
            </plugin>
        </plugins>
    </build>

5) The processor itself

Extend Ap.Processor for your own processor and override process()
public class MyProcessor extends Ap.Processor
{
   @Override
   public void process(final Ap.Context context) {
   }
}

6) Register this processor

Create a file in src/main/resources/META-INF/services/ called javax.annotation.processing.Processor and add your qualified path
io.determann.shadow.example.processor.MyProcessor

7) Annotation

Now create an Annotation to process in the processor module
public @interface MyAnnotation {}

8) Process

And finally process anything annotated with that annotation
public class MyProcessor extends Ap.Processor
{
   @Override
   public void process(final Ap.Context context) {
      for (Annotationable annotationable : context.getAnnotatedWith("org.example.MyAnnotation").all()) {
      }
   }
}

Simple Builder Example

An annotation to mark classes
@Target(ElementType.TYPE)
public @interface BuilderPattern {}
A Processor creating a simple Builder companion object
package io.determann.shadow.builder;

import io.determann.shadow.api.annotation_processing.Ap;
import io.determann.shadow.api.dsl.Dsl;
import io.determann.shadow.api.dsl.RenderingContext;
import io.determann.shadow.api.dsl.class_.ClassBodyStep;

import java.util.ArrayList;
import java.util.List;

import static java.lang.String.join;
import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.uncapitalize;

/// Builds a companion Builder class for each annotated class
public class ShadowBuilderProcessor
      extends Ap.Processor
{
   @Override
   public void process(final Ap.Context context)
   {
      // iterate over every class annotated with the BuilderPattern annotation
      for (Ap.Class toBuild : context
            .getClassesAnnotatedWith("io.determann.shadow.builder.BuilderPattern"))
      {
         // create the Builder Class
         ClassBodyStep step = Dsl.class_()
                                 .package_(toBuild.getPackage().getQualifiedName())
                                 .name(toBuild.getName() + "ShadowBuilder");

         String toBuildVariableName = uncapitalize(toBuild.getName());
         List<String> setterInvocations = new ArrayList<>();

         // create a record holding the code needed to render a property in the builder
         for (Ap.Property property : toBuild.getProperties()
                                            .stream()
                                            .filter(Ap.Property::isMutable)
                                            .toList())
         {
            String propertyName = property.getName();

            // render the existing field if possible, otherwise create a new one
            step = property.getField().map(step::field)
                           .orElse(step.field(Dsl.field(property.getType(), propertyName)));

            // render the wither
            step = step.method(Dsl.method()
                                  .public_()
                                  .resultType(step)//use the builder type
                                  .name("with" + capitalize(propertyName))
                                  .parameter(Dsl.parameter(property.getType(), propertyName))
                                  .body("""
                                        this.%1$s = %1$s;
                                        return this;""".formatted(propertyName)));

            // collect all setter invocations for the object being build
            setterInvocations.add(toBuildVariableName + "." +
                                  property.getSetterOrThrow().getName() +
                                  "(" + propertyName + ");");
         }

         // render the build method
         step = step.method(Dsl.method().public_().resultType(toBuild).name("build")
                               .body("""
                                     %1$s %2$s = new %1$s();
                                     %3$s
                                     return %2$s;
                                     """.formatted(toBuild.getName(),
                                                   toBuildVariableName,
                                                   join("\n\n", setterInvocations))));

         //writes the builder
         context.writeAndCompileSourceFile(toBuild.getQualifiedName() + "ShadowBuilder",
                                           step.renderDeclaration(RenderingContext.DEFAULT));
      }
   }
}
For a simple pojo like
package io.determann.shadow.builder;

import java.util.Objects;

@BuilderPattern
public class Customer
{
   private String name;

   public String getName()
   {
      return name;
   }

   public void setName(String name)
   {
      this.name = name;
   }

   @Override
   public boolean equals(Object o)
   {
      return this == o || o instanceof Customer customer && Objects.equals(getName(), customer.getName());
   }

   @Override
   public int hashCode()
   {
      return Objects.hash(getName());
   }

   @Override
   public String toString()
   {
      return "Customer{" +
             "name='" + name + '\'' +
             '}';
   }
}
This builder would be generated
package io.determann.shadow.builder;

import io.determann.shadow.builder.CustomerShadowBuilder;
import io.determann.shadow.builder.Customer;

class CustomerShadowBuilder {
   private String name;
   public CustomerShadowBuilder withName(String name) {
      this.name = name;
      return this;
   }
   public Customer build() {
      Customer customer = new Customer();
      customer.setName(name);
      return customer;
   }

}