JVM系列六(自定義插入式注解器).


一、概述

從前面 文章 中我們可以了解到,javac 的三個步驟中,程序員唯一能干預的就是注解處理器部分,注解處理器類似於編譯器的插件,在這些插件里面,可以讀取、修改、添加抽象語法樹中的任意元素。因此,只要有足夠的創意,程序員可以通過自定義插入式注解處理器來實現許多原本只能在編碼中完成的事情。我們常見的 LombokHibernate Validator 等都是基於自定義插入式注解器來實現的。

要實現注解處理器首先要做的就是繼承抽象類 javax.annotation.processing.AbstractProcessor,然后重寫它的 process() 方法,process() 方法是 javac 編譯器在執行注解處理器代碼時要執行的過程。

public abstract boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);

該方法有兩個參數,“annotations” 表示此處理器所要處理的注解集合;“roundEnv” 表示當前這個 Round 中的語法樹節點,每個語法樹節點都表示一個 Element(javax.lang.model.element.ElementKind 可以查看到相關 Element)。

該方法的返回值是一個 boolean 類型,通知編譯器這個 Round 中的代碼是否發生變化,是否需要構建新的 JavaCompiler 實例,是否需要開啟新的 Round。

除了 process() 方法外,還有兩個可以配合使用的 Annotations:

@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)

@SupportedAnnotationTypes 表示注解處理器對哪些注解感興趣,“*” 表示對所有的注解都感興趣;@SupportedSourceVersion 指出這個注解處理器可以處理最高哪個版本的 Java 代碼。

另外 AbstractProcessor 還有一個很常用的實例變量 “processingEnv”,它在 init() 方法執行的時候創建,它代表了注解處理器框架提供的一個上下文環境,要創建新的代碼、向編譯器輸出信息、獲取其他工具類等都需要用到這個實例變量。

    public synchronized void init(ProcessingEnvironment processingEnv) {
      // ... 
    }

tips:每一個注解處理器在運行的時候都是單例的。

二、自定義

我們現在要自定義一個插入式注解器 — NameCheckProcessor,它要做的事情是對 Java 程序命名進行檢查,檢查的規則如下:

  • 類(或接口):符合駝式命名法,首字母大寫

  • 方法:符合駝式命名法,首字母小寫

  • 字段:

    • 類或實例變量:符合駝式命名法,首字母小寫
    • 常量要求全部是大寫字母或下划線構成,並且第一個字符不能是下划線。
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class NameCheckProcessor extends AbstractProcessor {

    private NameChecker nameChecker;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        nameChecker = new NameChecker(processingEnv);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (!roundEnv.processingOver()) {
            for (Element element : roundEnv.getRootElements()) {
                nameChecker.checkNames(element);
            }
        }
        return false;
    }
}

從上面代碼可以看到,NameCheckProcessor 最高能處理 JDK1.8 的代碼,並對所有的注解都感興趣,而在 process() 方法中是把當前 Round 中的每一個 RootElement 傳遞到一個名為 NameChecker 的檢查器中檢查邏輯,process() 方法返回 false,因為它只是檢查命名規范,並未改變語法樹。

NameChecker 負責檢查命名規范,這是它 github代碼鏈接,哈哈,具體代碼就不在文章里貼了,再貼一下文章就沒法看了都。

NameChecker 通過一個繼承 javax.lang.model.util.ElementScanner8 的 NameCheckScanner 類,以 Visitor 模式來完成對語法樹的遍歷,分別執行 visitType()、visitExecutable() 和 visitVariable() 來訪問類、方法和字段,這 3 個 visit 方法對各自的命名規則做相應的檢查。

自定義注解器寫好了,那么問題來了,注解器怎么用呢?

  • 通過 javac 命令的 “-processor” 參數來執行編譯時需要附帶的注解處理器,如果有多個注解處理器的話,用逗號進行分割。
  • 通過 JAVA SPI 加載。在 resources 目錄下新增 META-INF/services 目錄,目錄內添加名為 javax.annotation.processing.Processor 的文件,內容是自定義注解器的全類名,一行表示一個注解器。

三、應用

這里主要介紹下利用 Java SPI 加載自定義注解器的方式,我們的目標是生成一個 jar 包,類似於 Lombok ,這樣其它應用一旦引用了這個 jar 包,自定義注解器就能自動生效了。

1. 生成注解器 jar 包

首先,我們先來看下自定義注解器的目錄結構,在 javax.annotation.processing.Processor 文件中是自定義注解器的全類名。

org.jvm.processor.name.check.NameCheckProcessor

然后,在 pom.xml 中配置 proc 屬性,如果不配置的話,會有個 WARNNING 提示— 找不到 processor 的異常。

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <proc>none</proc>
                </configuration>
            </plugin>
        </plugins>
    </build>

最后,愉快的使用 mvn clean install 來 build 你的注解器 jar 包吧!

2. 使用注解器 jar 包

首先,在 pom.xml 中引入注解器 jar 包的依賴

        <dependency>
            <groupId>org.jvm.processor</groupId>
            <artifactId>processor</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>

其實,進行到這一步你的自定義注解器已經生效了!另外,maven-compiler-plugin 支持手動對需要運行的注解器進行設置。

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessors>
                        <annotationProcessor>
                            org.jvm.processor.name.check.NameCheckProcessor
                        </annotationProcessor>
                    </annotationProcessors>
                </configuration>
            </plugin>

tips: maven-compile-plugin 等編譯插件會吞掉 javax.annotation.processing.Messager 所打印的東西,而手動使用 javac 編譯器則不會。

四、總結

上文的注解器案例主要參考《深入理解 JVM 虛擬機》,后來又在網上看了一些大家的實踐,覺得還挺開拓思維的,大家可以試試看。

自定義注解器這東西,類似於攔截器功能,只要思維都大膽,感覺能玩出花來!

上文的演示的代碼可參見:https://github.com/JMCuixy/jvm-demo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM