天天看點

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

一、概述

從前面 文章 中我們可以了解到,javac 的三個步驟中,程式員唯一能幹預的就是注解處理器部分,注解處理器類似于編譯器的插件,在這些插件裡面,可以讀取、修改、添加抽象文法樹中的任意元素。是以,隻要有足夠的創意,程式員可以通過自定義插入式注解處理器來實作許多原本隻能在編碼中完成的事情。我們常見的 Lombok、Hibernate 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 檔案中是自定義注解器的全類名。

JVM系列六(自定義插入式注解器).
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>
           

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

四、總結

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

  • https://blog.csdn.net/qiaoyl113/article/details/80063602
  • https://www.jianshu.com/p/554c5491bea6

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

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