天天看點

Java編譯器注解處理器AbstractProcessor

作者:閃念基因

導讀

本文圍繞編譯器注解都是如何運作的呢? 又是怎麼自動生成代碼的呢?做出了詳細介紹。

概述

我們接觸的注解主要分為以下兩類:

  1. 運作時注解:通過反射在運作時動态處理注解的邏輯
  2. 編譯時注解:通過注解處理器在編譯期動态處理相關邏輯

平時我們接觸的架構大部分都是運作時注解,比如:@Autowire @Resoure @Bean 等等。

那麼我們平時有接觸過哪些編譯期注解呢,@Lombok @AutoService 等等

像這些編譯時注解的作用都是自動生成代碼,一是為了提高編碼的效率,二是避免在運作期大量使用反射,通過在編譯期利用反射生成輔助類和方法以供運作時使用。

那這些編譯器注解都是如何運作的呢? 又是怎麼自動生成代碼的呢?

我們今天來詳細介紹一下,不過在介紹之前,可以先簡單了解一下Java注解的基本概念:

Java注解:https://blog.csdn.net/u010634066/article/details/80384125

注解處理器

注解處理流程

注解編譯期處理流程最關鍵的一個類就是Processor ,它是注解處理器的接口類,我們所有需要對編譯期處理注解的邏輯都需要實作這個Processor接口,當然,AbstractProcessor 抽象類幫我們寫好了大部分都流程,是以我們隻需要實作這個抽象類就可以很友善的定義一個注解處理器;

Java編譯器注解處理器AbstractProcessor

注解處理流程由多輪完成。每一輪都從編譯器在源檔案中搜尋注解并選擇适合這些注解的 注解處理器(AbstractProcessor) 開始。每個注解處理器依次在相應的源上被調用。

如果在此過程中生成了任何檔案,則将以生成的檔案作為輸入開始另一輪。這個過程一直持續到處理階段沒有新檔案生成為止。

注解處理器的處理步驟:

  • 在java編譯器中建構;
  • 編譯器開始執行未執行過的注解處理器;
  • 循環處理注解元素(Element),找到被該注解所修飾的類,方法,或者屬性;
  • 生成對應的類,并寫入檔案;
  • 判斷是否所有的注解處理器都已執行完畢,如果沒有,繼續下一個注解處理器的執行(回到步驟1)。

AbstractProcessor

這是注解處理器的核心抽象類,我們主要來看看裡面的方法

getSupportedOptions()

預設的實作是 從注解SupportedOptions擷取值,該值是一個字元數組,例如

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

不過貌似該接口并沒有什麼用處。

有資料表示 該可選參數可以從processingEnv擷取到參數。

String resultPath = processingEnv.getOptions().get(參數);           

實際上這個擷取的參數是編譯期通過入參 -Akey=name 設定的,跟getSupportedOptions沒有什麼關系。

getSupportedAnnotationTypes

擷取目前的注解處理類能夠處理哪些注解類型,預設實作是從SupportedAnnotationTypes注解裡面擷取;

注解值是個字元串數組 String [] ;

比對上的注解,會通過目前的注解處理類的 process方法傳入。

例如下面使用 * 通配符支援所有的注解:

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


}           

又或者可以直接重寫這個接口:

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

最終他們生效的地方就是用來做過濾,因為處理的時候會擷取到所有的注解,然後根據這個配置來擷取自己能夠處理的注解。

getSupportedSourceVersion

擷取該注解處理器最大能夠支援多大的版本,預設是從注解 SupportedSourceVersion中讀取,或者自己重寫方法,如果都沒有的話 預設值是 RELEASE_6

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


}           

或者重寫(推薦 , 擷取最新的版本)

@Override
  public SourceVersion getSupportedSourceVersion() {
    //設定為能夠支援最新版本
    return SourceVersion.latestSupported();
  }           

init初始化

init是初始化方法,這個方法傳入了ProcessingEnvironment 對象。一般我們不需要去重寫它,直接使用抽象類就行了。

當然你也可以根據自己的需求來重寫

@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());


    }           

可以擷取到很多資訊,例如擷取編譯器自定義參數, 自定義參數的設定請看下面的 如何給編譯期設定入參 部分

一些參數說明

Java編譯器注解處理器AbstractProcessor

process 處理方法

process方法提供了兩個參數,第一個是我們請求處理注解類型的集合(也就是我們通過重寫getSupportedAnnotationTypes方法所指定的注解類型),第二個是有關目前和上一次循環的資訊的環境。

傳回值表示這些注解是否由此 Processor 聲明

如果傳回 true,則這些注解不會被後續 Processor 處理;

如果傳回 false,則這些注解可以被後續的 Processor 處理。

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.out.println("SzzTestProcessor.process.....;");


        return false;
    }           

我們可以通過RoundEnvironment接口擷取注解元素,注意annotations隻是注解類型,并不知道哪些執行個體被注解标記了,RoundEnvironment是可以知道哪些被注解标記了的。

Java編譯器注解處理器AbstractProcessor

關于這部分的使用介紹,請看下面的自定義注解處理器範例。

如何注冊注解處理器

上面介紹了注解處理器的一些核心方法,那麼我們如何注冊注解處理器呢?

并不是說我們實作了AbstractProcessor類就會生效,由于注解處理器(AbstractProcessor) 是在編譯期執行的,而且它是作為一個Jar包的形式來生效,是以我們需要将注解處理器作為一個單獨的Module來打包。

然後在需要使用到注解處理器的Module引用。

這個注解處理器 所在Module打包的時候需要注意:

因為AbstractProcessor本質上是通過ServiceLoader來加載的(SPI), 是以想要被成功注冊上。則有兩種方式:

一、配置SPI

1、在resource/META-INF.services檔案夾下建立一個名為javax.annotation.processing.Processor的檔案;裡面的内容就是你的注解處理器的全限定類名;

Java編譯器注解處理器AbstractProcessor

2、設定編譯期間禁止處理 Process,之是以這樣做是因為,如果你不禁止Process,ServiceLoader就會去加載你剛剛設定的注解處理器,但是因為是在編譯期,Class檔案被沒有被成功加載,是以會抛出下面的異常

服務配置檔案不正确, 或構造處理程式對象javax.annotation.processing.Processor: Provider org.example.SzzTestProcessor not found時抛出異常錯誤           

如果是用Maven編譯的話,請加上如下配置 <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、注解處理器打包成功,就可以提供給别的Module使用了

二、使用@AutoService 自動配置SPI的配置檔案

@AutoService 是Google開源的一個小插件,它可以自動的幫我們生成META-INF/services 的檔案,也就不需要你去手動的建立配置檔案了。

當然,上面的 <compilerArgument>-proc:none</compilerArgument>參數也不需要了。

是以也就不會有編譯期上述的問題xxx not found 問題了。因為編譯的時候META-INF/services 還沒有配置你的注解處理器,也就不會抛出加載異常了。

例如下面,使用@AutoService(Processor.class),他會自動幫我們生成對應的配置檔案。

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


}           
Java編譯器注解處理器AbstractProcessor

另外,實際上 @AutoService 自動生成配置檔案也是通過AbstractProcessor來實作的。

如何調試編譯期代碼

在我們自己寫了注解處理器之後,可能想要調試,那麼編譯期的調試跟運作期的調試不一樣。

Maven相關配置(指定生效的Processor)

如果你使用的是Maven來編譯,那麼有一些參數可以設定。

比如指定注解處理器生效 、代碼生成的源路徑。預設是 target/generated-sources/annotations

除非特殊情況,一般不需要設定這些參數。

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

           
注意事項           

注解和注解處理器是單獨的module:注解處理器隻需要在編譯的時候使用,注解的Module隻需要引入注解處理器的Jar包就行了。是以我們需要将注解處理器分離為單獨的module。

并且打包的時候請先打包注解處理器的Module.

自定義Processor類最終是通過打包成jar,在編譯過程中調用的。

Java編譯器注解處理器AbstractProcessor

自定義注解處理器範例

範例一:自動生成Build構造器

1. 需求描述

假設我們的注釋使用者子產品中有一些簡單的 POJO 類,其中包含幾個字段:

public class Company {


    private String name;


    private String email ;


}


public class Personal {


    private String name;


    private String age;
}           

我們想建立對應的建構器幫助類來更流暢地執行個體化POJO類

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

2. 需求分析

如果沒有POJO都要手動的去建立對應的Build建構器,未免太繁雜了,我們可以通過注解的形式,去自動的幫我們的POJO類生成對應的Build建構器,但是當然不是每個都生成,按需生成;

1、定義一個 @BuildProperty 注解,在需要生成對應的setXX方法的方法上标記注解

2、自定義 注解處理器掃描@BuildProperty注解,按照需求自動生成Build建構器。例如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、編碼

建立一個注解處理器Module:szz-test-processor-handler

@BuildProperty

@Target(ElementType.METHOD) // 注解用在方法上
@Retention(RetentionPolicy.SOURCE) // 盡在Source處理期間可用,運作期不可用
public @interface BuildProperty {
}           

注解處理器

@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、注冊注解處理器

Java編譯器注解處理器AbstractProcessor

5、配置編譯參數

因為這裡選擇的是手動配置了 META-INF.services; 是以我們需要配置一下編譯期間忽略Processor;不然編譯的時候就會被ServiceLoader加載,會抛出 class not found 的異常。

主要參數就是

<compilerArgument>-proc:none</compilerArgument>           

如下所示

<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、執行編譯打包

mvn install一下, 其他Module就可以引用了。

7、Demo Module 依賴注解處理器

建立一個新的Module: szz-test-demo ; 讓它依賴上面的 szz-test-processor-handler

并在Company的一些方法上使用注解。

Java編譯器注解處理器AbstractProcessor

8、Demo Module 進行編譯,會自動生成BuildCompany類

Demo Module 編譯之後,就會在target檔案夾生成BuildXXX類。并且隻有我們用注解BuildProperty标記了的方法才會生成對應的方法。

而且如果注解BuildProperty使用的方式不對,我們也會列印出來了異常。

Java編譯器注解處理器AbstractProcessor

如何給編譯期設定入參

在init初始化的接口中,我們可以擷取到編譯器的一些自定義參數;

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

注意這個擷取到的編譯器參數隻能擷取的是以-A開頭的參數,因為是過濾之後的

Java編譯器注解處理器AbstractProcessor

那麼這個自定義參數從哪裡設定的呢?

如果你是IDEA 編譯

-Akey=value 或者 -Akey

Java編譯器注解處理器AbstractProcessor

如果是用Maven編譯

Java編譯器注解處理器AbstractProcessor

作者:石臻臻(十真)

來源:微信公衆号:阿裡開發者

出處:https://mp.weixin.qq.com/s/VO39ZN_7uO1srMuHFxu9hQ

繼續閱讀