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.
<?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>
<?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
shadow-api
<dependency>
<groupId>io.determann</groupId>
<artifactId>shadow</artifactId>
<version>0.3.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.determann</groupId>
<artifactId>processor-example</artifactId>
<version>0.3.0-SNAPSHOT</version>
</dependency>
3) Processor paths
<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
<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
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
src/main/resources/META-INF/services/
called javax.annotation.processing.Processor
and add your qualified pathio.determann.shadow.example.processor.MyProcessor
Simple Builder Example
@Target(ElementType.TYPE)
public @interface BuilderPattern {}
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));
}
}
}
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 + '\'' +
'}';
}
}
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;
}
}