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
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
src/main/resources/META-INF/services/
called javax.annotation.processing.Processor
and add your qualified pathio.determann.shadow.example.processor.MyProcessor
7) Annotation
public @interface MyAnnotation {}
8) 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) {
for (Shadow shadow : context.getAnnotatedWith("org.example.MyAnnotation").all()) {
}
}
}
Simple Builder Example
@Target(ElementType.TYPE)
public @interface BuilderPattern {}
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();
}
}
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 + '\'' +
'}';
}
}
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;
}
}