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 ShadowProcessor for your own processor and override process()
import io.determann.shadow.api.annotation_processing.AP_Context;
import io.determann.shadow.api.annotation_processing.AP_Processor;

public class MyProcessor extends ShadowProcessor
{
   @Override
   public void process(final AnnotationProcessingContext 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
import io.determann.shadow.api.annotation_processing.AP_Context;
import io.determann.shadow.api.annotation_processing.AP_Processor;

public class MyProcessor extends ShadowProcessor
{
   @Override
   public void process(final AnnotationProcessingContext context) {
      for (Shadow shadow : 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_Context;
import io.determann.shadow.api.annotation_processing.AP_Processor;
import io.determann.shadow.api.lang_model.shadow.LM_Nameable;
import io.determann.shadow.api.lang_model.shadow.LM_QualifiedNameable;
import io.determann.shadow.api.lang_model.shadow.structure.LM_Property;
import io.determann.shadow.api.lang_model.shadow.type.LM_Class;
import io.determann.shadow.api.lang_model.shadow.type.LM_Type;

import java.util.List;
import java.util.stream.Collectors;

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 (LM_Class aClass : context
            .getClassesAnnotatedWith("io.determann.shadow.builder.BuilderPattern"))
      {
         String toBuildQualifiedName = aClass.getQualifiedName();
         //qualifiedName of the companion builder class
         String builderQualifiedName = toBuildQualifiedName + "ShadowBuilder";
         //simpleName of the companion builder class
         String builderSimpleName = aClass.getName() + "ShadowBuilder";
         String builderVariableName = uncapitalize(builderSimpleName);

         //create a record holding the code needed to render a property in the builder
         List<BuilderElement> builderElements =
               aClass.getProperties()
                     .stream()
                     .filter(LM_Property::isMutable)
                     .map(property -> renderProperty(builderSimpleName,
                                                     builderVariableName,
                                                     property))
                     .toList();

         //writes the builder
         context.writeAndCompileSourceFile(builderQualifiedName,
                                           renderBuilder(aClass,
                                                         toBuildQualifiedName,
                                                         builderSimpleName,
                                                         builderVariableName,
                                                         builderElements));
      }
   }

   /**
    * renders a companion builder class
    */
   private String renderBuilder(final LM_Class aClass,
                                final String toBuildQualifiedName,
                                final String builderSimpleName,
                                final String builderVariableName,
                                final List<BuilderElement> builderElements)
   {
      String fields = builderElements.stream()
                                     .map(BuilderElement::field)
                                     .collect(Collectors.joining("\n\n"));

      String mutators = builderElements.stream()
                                       .map(BuilderElement::mutator)
                                       .collect(Collectors.joining("\n\n"));

      String setterInvocations = builderElements.stream()
                                                .map(BuilderElement::toBuildSetter)
                                                .collect(Collectors.joining("\n\n"));
      return """
            package %1$s;
                  
            public class %2$s{
               %3$s
                  
            %4$s
                  
               public %5$s build() {
                  %5$s %6$s = new %5$s();
                  %7$s
                  return %6$s;
               }
            }
            """.formatted(aClass.getPackage().getQualifiedName(),
                          builderSimpleName,
                          fields,
                          mutators,
                          toBuildQualifiedName,
                          builderVariableName,
                          setterInvocations);
   }

   /**
    * Creates a {@link BuilderElement} for each property of the annotated pojo
    */
   private BuilderElement renderProperty(final String builderSimpleName,
                                         final String builderVariableName,
                                         final LM_Property property)
   {
      String propertyName = property.getName();
      String type = renderType(property.getType());
      String field = "private " + type + " " + propertyName + ";";

      String mutator = """
               public %1$s with%2$s(%3$s %4$s) {
                  this.%4$s = %4$s;
                  return this;
               }
            """.formatted(builderSimpleName,
                          capitalize(propertyName),
                          type,
                          propertyName);

      String toBuildSetter = builderVariableName + "." +
                             property.getSetterOrThrow().getName() +
                             "(" + propertyName + ");";

      return new BuilderElement(field, mutator, toBuildSetter);
   }

   /**
    * Used to render the code needed to render a property in the builder
    *
    * @param field ones rendered will hold the values being used to build the pojo
    * @param mutator ones rendered will set the value of the {@link #field}
    * @param toBuildSetter ones rendered will modify the build pojo
    */
   private record BuilderElement(String field,
                                 String mutator,
                                 String toBuildSetter) {}

   private static String renderType(LM_Type type)
   {
      if (type instanceof LM_QualifiedNameable qualifiedNameable)
      {
         return qualifiedNameable.getQualifiedName();
      }
      if (type instanceof LM_Nameable nameable)
      {
         return nameable.getName();
      }
      return type.toString();
   }
}
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
public class CustomerShadowBuilder{
   private java.lang.String name;

   public CustomerShadowBuilder withName(java.lang.String name) {
      this.name = name;
      return this;
   }

   public io.determann.shadow.builder.Customer build() {
     io.determann.shadow.builder.Customer customer = new io.determann.shadow.builder.Customer();
      customer.setName(name);
      return customer;
   }
}