用了那麼久的 Lombok,你知道它的原理麼?

序言

在寫 Java 代碼的時候,最煩寫 setter/getter 方法,自從有了 Lombok 插件不用再寫那些方法之後,感覺再也回不去了,那你們是否好奇過 Lombok 是怎麼把 setter/getter 方法給你加上去的呢?有的同學說我們 Java 引入 Lombok 之後會污染依賴包,那我們可不可以自己寫一個工具來代替 Lombok 呢?

知識點

分析

序言提到的問題其實都是同一個問題,就是如何去獲取和修改 Java 源代碼?

要回答這個問題,我們需要回答這幾個問題:

  1. Java 編譯器是如何解析 Java 源代碼的?

  2. 編譯器編譯源代碼都有哪些步驟?

  3. 我們在編譯器工作的時候,怎麼才能去增加內容或者是進行代碼分析?

希望大家看完本文能夠自己寫一個簡易的 Lombok 工具。

回答

如何解析源代碼

其實從我們的代碼到被編譯,中間隔了一個數據結構,叫做 AST(抽象樹)。具體的形式,可以查看下面的圖片。右邊的便是 AST 的數據結構了。

代碼編譯都有哪些步驟

整個編譯過程大致如下:

圖片來自 openjdk

  1. 初始化插入註解處理器

  2. 解析與填充符號表過程

a. 詞法分析、語法分析。將源代碼的字符流轉變爲標記集合,構造出抽象語法樹。

b. 填充符號表。產生符號地址和符號信息。

  1. 插入式註解處理器的註解處理過程:插入式註解處理器的執行階段。後面我會給大家帶來兩個此方面的實用實戰例子。

  2. 分析與字節碼生成過程

a. 標註檢查。對語法的靜態信息檢查。

b. 數據流及控制流分析。對程序動態運行過程進行檢查。

c. 解語法糖。將簡化代碼編寫的語法糖還原爲原有的形式。

d. 字節碼生成。將前面各個步驟所生成的信息轉化成爲字節碼。

我們知道了上面的理論之後,接下來我們進行實戰。帶着大家一起去修改 AST(抽象樹)。添加自己的代碼。

實戰

如何自己實現一個自動添加 Setter/Getter 的工具

首先,我們創建一個自己的註解。

@Retention(RetentionPolicy.SOURCE) // 註解只在源碼中保留
@Target(ElementType.TYPE) // 用於修飾類
public @interface MySetterGetter {
}

創建一個需要生成 setter/getter 方法的實體類

@MySetterGetter  // 打上我們的註解
public class Test {
    private String wzj;
}

接下來就來看一看如何來生成我們想要的字符串。

整體代碼如下:

@SupportedAnnotationTypes("com.study.practice.nameChecker.MySetterGetter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MySetterGetterProcessor extends AbstractProcessor {
    // 主要是輸出信息
    private Messager messager;
    private JavacTrees javacTrees;
    private TreeMaker treeMaker;
    private Names names;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.javacTrees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment)processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 拿到被註解標註的所有的類
        Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MySetterGetter.class);
        elementsAnnotatedWith.forEach(element -> {
            // 得到類的抽象樹結構
            JCTree tree = javacTrees.getTree(element);
            // 遍歷類,對類進行修改
            tree.accept(new TreeTranslator(){
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
                    // 在抽象樹中找出所有的變量
                    for(JCTree jcTree: jcClassDecl.defs){
                        if (jcTree.getKind().equals(Tree.Kind.VARIABLE)){
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl)jcTree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }
                    // 對於變量進行生成方法的操作
                    for (JCTree.JCVariableDecl jcVariableDecl : jcVariableDeclList) {
                        messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " has been processed");
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
                    }
        // 生成返回對象
        JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewSetterMethodName(jcVariableDecl.getName()), methodType, List.nil(), parameters, List.nil(), block, null);
    }
    /**
     * 生成 getter 方法
     * @param jcVariableDecl
     * @return
     */
    private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl){
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        // 生成表達式
        JCTree.JCReturn aReturn = treeMaker.Return(treeMaker.Ident(jcVariableDecl.getName()));
        statements.append(aReturn);
        JCTree.JCBlock block = treeMaker.Block(0, statements.toList());
        // 無入參
        // 生成返回對象
        JCTree.JCExpression returnType = treeMaker.Type(jcVariableDecl.getType().type);
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewGetterMethodName(jcVariableDecl.getName()), returnType, List.nil(), List.nil(), List.nil(), block, null);
    }
    /**
     * 拼裝Setter方法名稱字符串
     * @param name
     * @return
     */
    private Name getNewSetterMethodName(Name name) {
        String s = name.toString();
        return names.fromString("set" + s.substring(0,1).toUpperCase() + s.substring(1, name.length()));
    }
    /**
     * 拼裝 Getter 方法名稱的字符串
     * @param name
     * @return
     */
    private Name getNewGetterMethodName(Name name) {
        String s = name.toString();
        return names.fromString("get" + s.substring(0,1).toUpperCase() + s.substring(1, name.length()));
    }
    /**
     * 生成表達式
     * @param lhs
     * @param rhs
     * @return
     */
    private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
        return treeMaker.Exec(
                treeMaker.Assign(lhs, rhs)
        );
    }
}

代碼有點多,我們逐一拆解說明:

下面這是整個代碼結構的腦圖,後面的講解會基於這個順序。

a. 註解

@SupportedAnnotationTypes 表示我們需要監聽的註解,比如我們之前定義的 @MySetterGetter。

@SupportedSourceVersion 表示我們想要對什麼版本的 Java 源代碼進行處理。

b. 父類

AbstractProcessor 是本次的核心類,編譯器在編譯的時候會掃描此類的子類。其中有一個子類必須實現的核心方法 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv),此方法如果是返回爲 true 就說明編譯的那個類抽象樹的結構又變化,需要重新進行詞法分析和語法分析(可以查看上面提到的那個編譯流程圖)。如果返回的是 false 就說明沒有變化。

c. process 方法

主要的操作邏輯是:

  1. 拿到所有被我們 MySetterGetter 標註的類。

  2. 遍歷所有的類,生成類的抽象樹結構。

  3. 對類進行操作:

a. 找到類中所有的變量。

b. 對變量進行生成 Set 和 Get 方法。

  1. 返回 true,說明類結構變了,需要重新解析。如果是 false 說明沒有變,不用重新解析。

d. 操作 JCTree 樹

主要是在操作抽象樹,可以查看文末附件中的文章進行學習。

e. 方法名稱拼接

這一塊兒和字符串拼接沒啥區別,用過反射的同學應該也都清楚這個操作了。

到此爲止,我們就已經介紹完了 Lombok 的原理。怎麼樣是不是很簡單。接下來,就讓我們把它運行起來,投入到實戰之中。

f. 運行

最後來看一下如何正確的運行這個我們寫的工具。

1. 環境

我的系統環境是 macOs Monterey;

java 版本是

openjdk version "1.8.0_302"
OpenJDK Runtime Environment (Temurin)(build 1.8.0_302-b08)
OpenJDK 64-Bit Server VM (Temurin)(build 25.302-b08, mixed mode)

2. 編譯 processor

在你存放 MySetterGetter 和 MySetterGetterProcessor 兩個類的目錄下進行編譯。

javac -cp $JAVA_HOME/lib/tools.jar MySetterGetter.java MySetterGetterProcessor.java

執行成功後會出現這三個 class 文件。

3. 聲明插入式註解處理器

  1. 在你的工程的 resources 下面創建一個包,名稱爲:META-INFO.services

  2. 然後創建一個文件,名稱爲:javax.annotation.processing.Processor

  3. 將你的註解處理器的地址填入,我的配置是這樣的:

com.study.practice.nameChecker.MySetterGetterProcessor

4. 用我們的工具去編譯目標類

比如我們本次是要編譯那個 test.java。

它的內容再回顧一下:

@MySetterGetter  // 打上我們的註解
public class Test {
    private String wzj;
}

然後我們就去編譯它(注意類前面的路徑。這個你們得換成自己的工程目錄。)

javac -processor com.study.practice.nameChecker.MySetterGetterProcessor com/study/practice/nameChecker/Test.java

執行之後如果沒有修改我的代碼的話會打印這幾個字符串:

process 1
process 2
注: wzj has been processed
process 1

最後會生成 Test.class 文件。

5. 成果

最後的 class 文件解析出來就是這個樣子的。如下圖所示:

看到 Setter/Getter 方法就說明我們已經大功告成了!是不是很簡單。

到此爲止,我們就學會了如何自己寫一個屬於自己的簡易 Lombok 的插件了。

附件

treemarker 的介紹:

http://www.docjar.com/docs/api/com/sun/tools/javac/tree/TreeMaker.html

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/BZzHAZcEn4T5rXrNKeb5Pw