By Renchuang Shi (Shizhen)
The common annotations are divided into the following two categories:
Most of the annotations we usually use are annotations at runtime, such as @Autowire, @Resoure, and @Bean.
The compile-time annotations we usually use include @Lombok
and @AutoService
.
These compile-time annotations are used to automatically generate code, thus improving coding efficiency and avoiding heavy use of Reflection at runtime by using Reflection at compile time to generate auxiliary classes and methods for use at runtime.
How do these compile-time annotations work? How do they automatically generate code?
One of the most critical classes in the annotation processing flow at compile time is the Processor, which is the interface of the annotation processor. All the logic we need to process annotations at compile time needs to implement this Processor interface. AbstractProcessor helps us write most of the processes, so we only need to implement this abstract class to define an annotation processor.
The annotation processing process requires multiple rounds to complete. Each round starts with the compiler searching for annotations in the source file and selecting the appropriate annotation processor (AbstractProcessor) for those annotations. Each annotation processor is called on the corresponding source in turn.
If any files are generated during this process, another round will begin with the generated files as input. This process continues until no new files are generated during the processing phase.
This is the core abstract class of the annotation processor. Let's focus on the method.
The default implementation is to get the value from the annotation, SupportedOptions
, and the value is a character array, such as:
@SupportedOptions({"name","age"})
public class SzzTestProcessor extends AbstractProcessor {
}
However, it seems that the interface is useless.
Some data indicate that this optional parameter can be obtained from processingEnv.
String resultPath = processingEnv.getOptions().get(parameter);
The obtained parameter is set through the input parameter -Akey=name
at compile time, and it has nothing to do with the getSupportedOptions() method.
It is used to get the annotation types that the current annotation processing class supports. The default implementation is obtained from the SupportedAnnotationTypes
annotation.
The annotation value is a string array, String [].
The matching annotations are passed through the process() method of the annotation processing class.
For example, the following uses *, a wildcard character, to support all annotations:
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class PrintingProcessor extends AbstractProcessor {
}
Or you can rewrite this interface directly:
@Override
public ImmutableSet<String> getSupportedAnnotationTypes() {
return ImmutableSet.of(AutoService.class.getName());
}
In the end, they are used for filtering because all annotations will be obtained during processing. Then, the annotations they can process will be obtained according to this configuration.
It is used to get the latest version that the annotation processor can support. The default implementation is obtained from the SupportedSourceVersion annotation, or you can rewrite the method by yourself. If there is no such version, the default value is RELEASE_6.
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class PrintingProcessor extends AbstractProcessor {
}
Or you can rewrite the method to get the latest supported version (recommended).
@Override
public SourceVersion getSupportedSourceVersion() {
// Specify the latest version that is supported.
return SourceVersion.latestSupported();
}
init is the initialization method that passes in the ProcessingEnvironment object. In general, we don't need to rewrite it, just use abstract classes.
You can also rewrite it according to your needs.
@Override
public synchronized void init(ProcessingEnvironment pe) {
super.init(pe);
System.out.println("SzzTestProcessor.init.....");
// You can get the compiler parameters (the following two are the same).
System.out.println(processingEnv.getOptions());
System.out.println(pe.getOptions());
}
You can obtain a lot of information. For example, you can obtain custom parameters of the compiler. Please see How to set input parameters for compilers below for more information about how to set custom parameters.
Some parameter descriptions:
The process() method provides two parameters. The first is the collection of types of annotations we request to be processed, which is the annotation types we specify by rewriting the getSupportedAnnotationTypes() method. The second is the context for information about the current and previous loops.
The return value indicates whether these annotations are declared by this processor. If true is returned, these annotations are not processed by subsequent processors. If false is returned, these annotations can be processed by subsequent processors.
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("SzzTestProcessor.process.....;");
return false;
}
We can get annotation elements through the RoundEnvironment interface. Note: Annotations are only annotation types, and we do not know which instances are annotated. However, we can know which instances are annotated through RoundEnvironment.
Please see the Example of Custom Annotation Processor below for more information.
The section above describes some core methods of the annotation processor, but how do we register the annotation processor?
It doesn't mean that after the AbstractProcessor class is implemented, the annotation processor will take effect. Since the annotation processor (AbstractProcessor) executes tasks at compile time and takes effect as a Jar package file, we need to package the AbstractProcessor as a separate module.
Then, reference the AbstractProcessor in the module that needs to use it.
When you package the module where the AbstractProcessor is located, please note the following:
AbstractProcessor essentially loads the SPI through the ServiceLoader, so there are two ways for AbstractProcessor to be registered.
Step 1: Create a file called javax.annotation.processing.Processor
under the resource/META-INF.services
folder. The content inside the file is the fully-qualified class name of your annotation processor.
Step 2: Set the Processor to be disabled at compile-time. The reason is that if you do not disable the Processor, ServiceLoader will load the annotation processor you just set. However, since the Class file was not successfully loaded at compile time, the following exception will be thrown.
The service configuration file is incorrect, or when you are constructing the processor object, javax.annotation.processing.Processor, the exception error: Provider org.example.SzzTestProcessor not found is thrown.
If Maven is used for compilation, add the following configuration: <compilerArgument>-proc:none</compilerArgument>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</execution>
<execution>
<id>compile-project</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
Step 3: If the annotation processor is packaged, it can be provided to other modules for use.
@AutoService is a small open-source plug-in from Google. It can automatically help you generate META-INF/services files, so you don't need to manually create configuration files.
In the section above <compilerArgument>-proc:none</compilerArgument>
arguments are not needed.
Therefore, there will be no errors mentioned above, such as xxx not found
at compile time. Since META-INF/services have not configured your annotation processor at compile time, no load exception will be thrown.
For example, if you use @AutoService(Processor.class), it will automatically generate the corresponding configuration file for you.
@AutoService(Processor.class)
public class SzzBuildProcessor extends AbstractProcessor {
}
In addition, the automatic generation of configuration files by @AutoService is implemented through AbstractProcessor.
We might want to debug our annotation processors, but the debugging at compile time is different from that at runtime.
If you use Maven to compile, there are some parameters you need to specify.
For example, you can specify the parameter and the source path where code is generated. The default value of the source path is target/generated-sources/annotations
.
You do not need to set these parameters unless there are special circumstances.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<!-- Set the path of generated source folders. The default path is as follows. Generally, you don't need to set this parameter unless you have your own special needs -->
<generatedSourcesDirectory>${project.build.directory} /generated-sources/</generatedSourcesDirectory>
<!-- Specifies the effective annotation processors. After this parameter is specified, only the configured annotation processors below will take effect. In general, it is unnecessary to specify this parameter, so you can delete all of the following -->
<annotationProcessors>
<annotationProcessor>
org.example.SzzTestProcessor
</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>
Annotation and annotation processor are separate modules. Annotation processors are only used at compile time, and the annotation module only needs to introduce the Jar package file of the annotation processor. So, we need to separate the annotation processor into separate modules.
When packaging, please package the module of the annotation processor first.
The custom processor class is eventually called during compilation by packaging it into a Jar file.
Let's suppose we have some simple POJO classes in our annotation user module that contain several fields:
public class Company {
private String name;
private String email ;
}
public class Personal {
private String name;
private String age;
}
We want to create a corresponding builder helper class to instantiate the POJO class more smoothly.
Company company = new CompanyBuilder()
.setName("ali").build();
Personal personal = new PersonalBuilder()
.setName("szz").build();
If there is no POJO class, it is too complicated to manually create the corresponding builder. We can automatically generate the corresponding builder for POJO classes in the form of annotations, but the builder is not generated for every POJO class, and it is generated on demand.
Step 1: Define a @BuildProperty annotation and annotate the method that needs to generate the corresponding set() method
Step 2: Customize the annotation processor to scan the @BuildProperty annotation and automatically generate a builder as required
For example, the code to generate the CompanyBuilder is listed below:
public class CompanyBuilder {
private Company object = new Company();
public Company build() {
return object;
}
public CompanyBuilder setName(java.lang.String value) {
object.setName(value);
return this;
}
}
Create an annotation processor module: szz-test-processor-handler
@BuildProperty
@Target(ElementType.METHOD) // Use the annotation on the method.
@Retention(RetentionPolicy.SOURCE) // It is available only during Source processing but unavailable at runtime.
public @interface BuildProperty {
}
Annotation Processor
@SupportedAnnotationTypes("org.example.BuildProperty") // Only process the annotation with the type of BuildProperty;
public class SzzBuildProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("SzzBuildProcessor.process ;");
for (TypeElement annotation : annotations) {
// Obtain all instances annotated by the annotation.
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);
// Check whether the annotation starts with set and only one argument is used as required.
Map<Boolean, List<Element>> annotatedMethods = annotatedElements.stream().collect(
Collectors.partitioningBy(element ->
((ExecutableType) element.asType()).getParameterTypes().size() == 1
&& element.getSimpleName().toString().startsWith("set")));
List<Element> setters = annotatedMethods.get(true);
List<Element> otherMethods = annotatedMethods.get(false);
// Print the case where the annotation was used incorrectly.
otherMethods.forEach(element ->
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@BuilderProperty must be applied to a setXxx method with a single argument", element));
if (setters.isEmpty()) {
continue;
}
Map<String ,List<Element>> groupMap = new HashMap();
// Group by fully-qualified class name. A builder is created for each class.
setters.forEach(setter ->{
// Fully-qualified class name
String className = ((TypeElement) setter
.getEnclosingElement()).getQualifiedName().toString();
List<Element> elements = groupMap.get(className);
if(elements != null){
elements.add(setter);
}else {
List<Element> newElements = new ArrayList<>();
newElements.add(setter);
groupMap.put(className,newElements);
}
});
groupMap.forEach((groupSetterKey,groupSettervalue)->{
// Obtain the class name SimpleName and the input parameters of the set() method.
Map<String, String> setterMap = groupSettervalue.stream().collect(Collectors.toMap(
setter -> setter.getSimpleName().toString(),
setter -> ((ExecutableType) setter.asType())
.getParameterTypes().get(0).toString()
));
try {
// Assemble the XXXBuild class and create the corresponding class file.
writeBuilderFile(groupSetterKey,setterMap);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
// Returning false indicates that other processors can continue to process the annotation after the current processor has processed it, and returning true indicates that other processors will no longer process the annotation after the current processor has processed it.
return true;
}
private void writeBuilderFile(
String className, Map<String, String> setterMap)
throws IOException {
String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}
String simpleClassName = className.substring(lastDot + 1);
String builderClassName = className + "Builder";
String builderSimpleClassName = builderClassName
.substring(lastDot + 1);
JavaFileObject builderFile = processingEnv.getFiler()
.createSourceFile(builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
if (packageName != null) {
out.print("package ");
out.print(packageName);
out.println(";");
out.println();
}
out.print("public class ");
out.print(builderSimpleClassName);
out.println(" {");
out.println();
out.print(" private ");
out.print(simpleClassName);
out.print(" object = new ");
out.print(simpleClassName);
out.println("();");
out.println();
out.print(" public ");
out.print(simpleClassName);
out.println(" build() {");
out.println(" return object;");
out.println(" }");
out.println();
setterMap.entrySet().forEach(setter -> {
String methodName = setter.getKey();
String argumentType = setter.getValue();
out.print(" public ");
out.print(builderSimpleClassName);
out.print(" ");
out.print(methodName);
out.print("(");
out.print(argumentType);
out.println(" value) {");
out.print(" object.");
out.print(methodName);
out.println("(value);");
out.println(" return this;");
out.println(" }");
out.println();
});
out.println("}");
}
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
System.out.println("----------");
System.out.println(processingEnv.getOptions());
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
Since the META-INF.services
is manually configured here, we need to set the Processor to be disabled at compile time. Otherwise, it will be loaded by ServiceLoader, and the class not found exception will be thrown during compilation. The main parameters are listed below:
<compilerArgument>-proc:none</compilerArgument>
And the code is as follows:
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</execution>
<execution>
<id>compile-project</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
Run the mvn install command, and other modules can be referenced.
Create a new module (szz-test-demo) and make it depend on the szz-test-processor-handler above
Use annotations on some of the Company's methods:
Once the Demo Module is compiled, the BuildXXX class will be generated in the target folder. Only the methods annotated with the @BuildProperty annotation will generate the corresponding methods.
If the @BuildProperty annotation is used in the wrong way, an exception will be printed out.
We can get some custom parameters of the compiler in the interface of init initialization.
String verify = processingEnv.getOptions().get("custom key");
Note: This obtained compiler parameter is the key starting with -A because it is obtained after filtering.
How can we set this custom parameter?
If you use IDEA for compilation:
-Akey=value or -Akey
If you use Maven for compilation:
1,046 posts | 257 followers
FollowAlibaba Cloud Community - October 25, 2022
Alibaba Clouder - April 19, 2021
mizhou - June 15, 2023
Alibaba Clouder - May 17, 2019
Alibaba Cloud Community - November 25, 2024
Alibaba Developer - June 25, 2021
1,046 posts | 257 followers
FollowExplore Web Hosting solutions that can power your personal website or empower your online business.
Learn MoreExplore how our Web Hosting solutions help small and medium sized companies power their websites and online businesses.
Learn MoreBuild superapps and corresponding ecosystems on a full-stack platform
Learn MoreWeb App Service allows you to deploy, scale, adjust, and monitor applications in an easy, efficient, secure, and flexible manner.
Learn MoreMore Posts by Alibaba Cloud Community
Dikky Ryan Pratama July 13, 2023 at 1:33 am
Awesome!