Detailed explanation of Java annotation compilation processing AbstractProcessor

overview

The annotations we contact are mainly divided into the following two categories

  1. Runtime Annotations : Dynamically process annotation logic at runtime via reflection

  2. Compile-time annotations : dynamically process related logic at compile time through annotation processors

Most of the frameworks we usually come into contact with are runtime annotations, such as: @Autowire @Resoure @Bean and so on.

So what compile-time annotations have we usually come into contact with, @Lombok @AutoServiceetc.

The function of these compile-time annotations is to automatically generate code, one is to improve the efficiency of coding , and the other is to avoid extensive use of reflection at runtime , and generate auxiliary classes and methods for runtime use by using reflection at compile time.

So how do these compiler annotations work? How do they automatically generate code?

Let's introduce it in detail today, but before introducing it, you can briefly understand the basic concepts of Java annotations
Java annotations

annotation processor

Annotation processing flow

The most critical class in the annotation compilation process is Processor , which is the interface class of the annotation processor. All of our logic that needs to process annotations in the compilation period needs to implement this Processor interface. Of course, the AbstractProcessor abstract class helps us write a big Parts are all processes, so we only need to implement this abstract class to easily define an annotation processor ;

The annotation processing pipeline is done in multiple rounds. Each round starts with the compiler searching the source files for annotations and selecting an annotation . Each annotation processor is invoked on the corresponding source in turn.

If any files are generated during this process, another round will start with the generated files as input. This process continues until no new files are generated during the processing phase.

The processing steps of the annotation processor:

  • build in the java compiler;
  • The compiler starts executing annotation processors that have not been executed;
  • Cycle through the annotation elements (Element) to find the class, method, or attribute modified by the annotation;
  • Generate the corresponding class and write it to the file;
  • Determine whether all annotation processors have been executed, if not, continue the execution of the next annotation processor (return to step 1).

图TODO

AbstractProcessor

This is the core abstract class of the annotation processor, let's take a look at the methods inside

getSupportedOptions()

The default implementation is SupportedOptionsto get the value from the annotation, which is a character array, for example


@SupportedOptions({
    
    "name","age"})
public class SzzTestProcessor extends AbstractProcessor {
    
    
}

But it seems that the interface is not useful.

Some sources indicate that this optional parameter can be obtained from processingEnv.

String resultPath = processingEnv.getOptions().get(参数);

In fact, the obtained parameters are -Akey=nameset , and have nothing to do with getSupportedOptions.

getSupportedAnnotationTypes

Obtain which annotation types the current annotation processing class can handle. The default implementation is SupportedAnnotationTypesto obtain from the annotation;
the annotation value is a string array String [];
the matching annotation will be passed in through the process method of the current annotation processing class .

For example, the following uses * wildcards to support all annotations

@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class PrintingProcessor extends AbstractProcessor {
    
    

}

Or you can directly override this interface

  @Override
  public ImmutableSet<String> getSupportedAnnotationTypes() {
    
    
    return ImmutableSet.of(AutoService.class.getName());
  }

In the end, the place where they take effect is used for filtering, because all the annotations will be obtained during processing, and then according to this configuration, the annotations that can be processed by themselves will be obtained.

getSupportedSourceVersion

Get the maximum version that the annotation processor can support. The default is to read SupportedSourceVersionfrom , or rewrite the method yourself. If there is none, the default value is RELEASE_6

@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class PrintingProcessor extends AbstractProcessor {
    
    

}

or rewrite ( recommended , get the latest version)

  @Override
  public SourceVersion getSupportedSourceVersion() {
    
    
    //设置为能够支持最新版本
    return SourceVersion.latestSupported();
  }

init initialization

init is the initialization method, which passes in the ProcessingEnvironment object. Generally, we don't need to rewrite it, just use the abstract class directly.
Of course, you can also according to your own needs to re-

    @Override
    public synchronized void init(ProcessingEnvironment pe) {
    
    
        super.init(pe);
        System.out.println("SzzTestProcessor.init.....");
        // 可以获取到编译器参数(下面两个是一样的)
        System.out.println(processingEnv.getOptions());
        System.out.println(pe.getOptions());

    }

You can get a lot of information, such as getting compiler custom parameters . For the setting of custom parameters, please refer to the following section on how to set parameters for compilation

Some parameter descriptions

method describe
Elements getElementUtils() Returns an object that implements the Elements interface, a tool class for manipulating elements.
File getFiler() Returns an object implementing the Filer interface for creating files, classes, and auxiliary files.
Messenger getMessenger() Returns an object implementing the Messager interface, which is used to report error messages and warning reminders.
Map<String,String> getOptions() Returns the specified parameter option.
Types getTypeUtils() Returns an object that implements the Types interface, a tool class for manipulating types.

process processing method

The process method provides two parameters, the first is the collection of annotation types we request to process (that is, the annotation type we specified by overriding the getSupportedAnnotationTypes method), and the second is the environment for information about the current and last cycle.

The return value indicates whether these annotations are declared by this Processor
. If it returns true, these annotations will not be processed by subsequent Processors;
if it returns false, 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 obtain annotation elements through the RoundEnvironment interface. Note that annotations are only annotation types, and we don’t know which instances are annotated. RoundEnvironment can know which ones are annotated.

method describe
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a) Returns a collection of elements annotated with the specified annotation type.
Set<? extends Element> getElementsAnnotatedWith(TypeElement a) Returns a collection of elements annotated with the specified annotation type.
processingOver() Returns true if loop processing is complete, false otherwise.

For an introduction to the use of this part, please see the following example of a custom annotation processor

How to register an annotation processor

Some core methods of annotation processors are introduced above, so how do we register annotation processors ?

It does not mean that we implement the AbstractProcessor class will take effect, because the annotation processor (AbstractProcessor) is executed at compile time, and it takes effect as a Jar package, so we need to use the annotation processor as a separate Module to pack.
Then you need to use the Module reference to the annotation processor .

When packaging the Module where this annotation processor is located, you need to pay attention to:

Because AbstractProcessor is essentially loaded through ServiceLoader (SPI), it wants to be successfully registered. then there are two ways

1. Configure SPI

  1. Create a file named under resource/META-INF.servicesthe folder ; the content inside is the fully qualified class name of javax.annotation.processing.Processoryour annotation processor
    insert image description here

  2. Process is prohibited during compilation. The reason for this is that if you do not prohibit Process, ServiceLoader will load the annotation processor you just set, but because it is during compilation, the Class file is not successfully loaded, so it will throws the following exception

    
    服务配置文件不正确, 或构造处理程序对象javax.annotation.processing.Processor: Provider org.example.SzzTestProcessor not found时抛出异常错误
    
    
    

    If it is compiled with Maven, please 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>
    
  3. The annotation processor is successfully packaged and can be provided to other modules.

2. Use @AutoService to automatically configure the SPI configuration file

@AutoService is a small plug-in open sourced by Google. It can automatically generate META-INF/servicesfiles for us, so you don't need to manually create configuration files.

Of course, the above <compilerArgument>-proc:none</compilerArgument>parameters are not needed either.

So there will be no such problems at compile time xxx not found . META-INF/servicesBecause your annotation processor has not been configured when compiling , no loading exception will be thrown.

For example, below, using @AutoService(Processor.class), he will automatically generate the corresponding configuration file for us.

@AutoService(Processor.class)
public class SzzBuildProcessor extends AbstractProcessor {
    
    

}

insert image description here

In addition, in fact, @AutoService automatically generates configuration files through AbstractProcessor.

For specific usage, please see: @AutoService Detailed Explanation

How to debug compile-time code

After we write the annotation processor ourselves, we may want to debug, so debugging at compile time is different from debugging at run time.

See: How to debug compile-time source code in IDEA

Maven related configuration (specify effective Processor)

If you are using Maven to compile, there are some parameters that can be set

For example, specify the source path where the annotation processor takes effect and the code is generated. default istarget/generated-sources/annotations

Unless special circumstances, generally do not need to set these parameters.

<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>
                <!-- 主动设置生成的源码的文件夹路径,默认的就是下面的地址。一般不需要主动设置除非你有自己的需求  -->
                <generatedSourcesDirectory>${
    
    project.build.directory} /generated-sources/</generatedSourcesDirectory>
                 <!-- 指定生效的注解处理器,这里设置之后,只会有下面配置的注解处理器生效; 一般情况也不用主动配置,可以将下面的全部删除 -->
                <annotationProcessors>
                    <annotationProcessor>
                            org.example.SzzTestProcessor
                    </annotationProcessor>
                </annotationProcessors>
            </configuration>
        </plugin>

    </plugins>
</build>

Precautions

Annotations and annotation processors are separate modules: the annotation processor only needs to be used at compile time, and the annotation module only needs to import the Jar package of the annotation processor. So we need to separate the annotation processor into a separate module.

And when packaging, please package the Module of the annotation processor first .

The custom Processor class is finally packaged into a jar and called during the compilation process.

Custom Annotation Processor Example

Example 1: Automatically generate Build constructor

1. Requirements description

Let's say we have some simple POJO class in our annotated user module with a few fields:


public class Company {
    
    

    private String name;

    private String email ;
    
}

public class Personal {
    
    

    private String name;

    private String age;
}

We want to create corresponding builder helper classes to instantiate POJO classes more smoothly

        Company company = new CompanyBuilder()
                .setName("ali").build();
        Personal personal = new PersonalBuilder()
                .setName("szz").build();

2. Demand analysis

If there is no POJO, it is too complicated to manually create the corresponding Build builder. We can automatically generate the corresponding Build builder for our POJO class through annotations, but of course not every one is generated. Generated on demand;

  1. Define a @BuildProperty annotation, and mark the annotation on the method that needs to generate the corresponding setXX method

  2. The custom annotation processor scans @BuildProperty annotations and automatically generates Build builders according to requirements. For example, CompanyBuilder

    	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;
        }
    
    }
    

3. Coding

Create an annotation processor Module: szz-test-processor-handler

@BuildProperty

@Target(ElementType.METHOD) // 注解用在方法上
@Retention(RetentionPolicy.SOURCE) // 尽在Source处理期间可用,运行期不可用
public @interface BuildProperty {
    
    
}

annotation processor

@SupportedAnnotationTypes("org.example.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) {
    
    
            // 获取所有被该注解 标记过的实例
            Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);

            // 按照需求 检查注解使用的是否正确 以set开头,并且参数只有一个
            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);

            // 打印注解使用错误的case
            otherMethods.forEach(element ->
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                            "@BuilderProperty 注解必须放到方法上并且是set开头的单参数方法", element));

            if (setters.isEmpty()) {
    
    
                continue;
            }


            Map<String ,List<Element>> groupMap = new HashMap();

            // 按照全限定类名分组。一个类创建一个Build
            setters.forEach(setter ->{
    
    
                // 全限定类名
                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)->{
    
    
                //获取 类名SimpleName 和 set方法的入参
                Map<String, String> setterMap = groupSettervalue.stream().collect(Collectors.toMap(
                        setter -> setter.getSimpleName().toString(),
                        setter -> ((ExecutableType) setter.asType())
                                .getParameterTypes().get(0).toString()
                ));
                try {
    
    
                    // 组装XXXBuild类。并创建对应的类文件
                    writeBuilderFile(groupSetterKey,setterMap);
                } catch (IOException e) {
    
    
                    throw new RuntimeException(e);
                }

            });
        }

        // 返回false 表示 当前处理器处理了之后 其他的处理器也可以接着处理,返回true表示,我处理完了之后其他处理器不再处理
        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();
    }


}

4. Register the annotation processor

insert image description here

5. Configure compilation parameters

Because the manual configuration is selected here META-INF.services; so we need to configure to ignore the Processor during compilation;
the main parameters are

<compilerArgument>-proc:none</compilerArgument>

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>


6. Execute compilation and packaging

After mvn install, other Modules can be referenced.

7. Demo Module depends on annotation processor

Create a new Module: szz-test-demo ; make it depend on the above szz-test-processor-handler

And use annotations on some methods of Company.
insert image description here

8. Compile the Demo Module and automatically generate the BuildCompany class

After the Demo Module is compiled, the BuildXXX class will be generated in the target folder. And only the methods marked with the annotation BuildProperty will generate the corresponding methods.
And if the annotation BuildProperty is used in the wrong way, we will also print out the exception.
insert image description here

Example 2:

To be added.

How to set parameters for compilation

In the interface initialized by init, we can get some custom parameters of the compiler;

    String verify = processingEnv.getOptions().get("自定义key");

Note that the obtained compiler parameters can only obtain -A开头the corresponding parameters, because they are filtered

insert image description here

So where is this custom parameter set from?

If you are IDEA compile

-Akey=value 或者 -Akey

insert image description here

If compiled with Maven

insert image description here

Guess you like

Origin blog.csdn.net/u010634066/article/details/129941559