Lombok常常用,可是你知道它的原理是什麼嗎?(二)

在上一篇Lombok常常用,可是你知道它的原理是什麼嗎?簡單介紹了註解處理器,是用來處理編譯期的註解的一個工具,咱們只是本身生成了一些代碼,可是和Lombok卻不同,由於Lombok是在原有類的基礎上增長了一些類,你那麼Lombok是如何作到修改原有類的內容呢?接下來咱們就再進一步瞭解Lombok的原理。java

Javac原理

既然咱們是在編譯期對類進行操做了,那麼咱們就須要瞭解在Java中Javac到底對程序作了什麼。Javac對代碼編譯的過程其實就是用Java來寫的,咱們能夠查看其源碼對其簡單的分析,如何下載源碼,Debug源碼這裏我就不進行分析了,推薦一篇文章寫的挺好的。Javac 源碼調試教程git

編譯過程大體分爲了三個階段程序員

  • 解析與填充符號表
  • 註解處理
  • 分析與字節碼生成

這三個階段的交互過程以下圖所示。github

解析與填充符號表

這一步驟是兩個步驟,包括瞭解析和填充符號,其中解析是分爲詞法分析語法分析兩個步驟。app

詞法分析和語法分析

詞法分析就是將源代碼的字符流轉變爲Java中的標記(Token)集合,單個字符是程序編寫過程當中最小的元素,而標記(Token)則是編譯過程當中最小的元素,關鍵字、變量名、字面量、運算符均可以成爲標記(Token)。好比在Java中int a = b+2,這段代碼則表示了6個標記Token,分別是int、a、=、b、+、2。雖然關鍵字int是由三個字符構成的,可是它只是一個Token,不能夠再拆分了。ide

語法分析是根據Token序列構造抽象對象樹的過程,抽象語法樹(Abstract syntax tree),是一種用來描述代碼語法結構的樹形表示方法,語法樹的每個節點都表明着程序代碼中的一個語法結構,例如包、類型、修飾符、運算符、接口、返回值甚至是代碼註釋都是一個語法結構。工具

語法分析分析出來的樹結構是由JCTree 來表示的,咱們能夠看一下它的子類有哪些。post

咱們本身建一個類,能夠觀察它在編譯過程當中用樹結構表示是一種怎樣的結構。測試

public class HelloJvm {

    private String a;
    private String b;

    public static void main(String[] args) {
        int c = 1+2;
        System.out.println(c);
        print();
    }

    private static void print(){

    }
}

你們注意我劃紅線的地方,能夠看到這些都是JCTree的子類。咱們能夠知道編譯期的樹是以JCCompilationUnit爲根節點,而後做爲類的構成元素例如方法、私有變量、class類,這些都是做爲樹的構成一種。this

填充符號表

> 填充符號表和咱們的Lombok原理關聯不大,這裏瞭解便可。

完成了語法分析和詞法分析之後,下一步就是填充符號表的過程,符號表是由一組符號地址和符號信息構成的表格,能夠將它想象成哈希表中的K-V值對的形式(符號表不必定是哈希表實現,可使有序符號表,樹狀符號表、棧結構符號表等)。符號表中所登記的信息在編譯的不一樣階段都要用到,在語義分析中,符號表所登記的內容將用於語義檢查(如檢查一個名字的使用和原先的說明是否一致)和產生中間代碼。在目標代碼生成階段,當對符號名進行地址分配時,符號表是地址分配的依據。

註解處理器

第一步的解析和填充符號表完成之後,接下來就是咱們的重頭戲註解處理器了。由於在這一步就是Lombok實現原理的關鍵。

在JDK1.5以後,Java語言提供了對註解的支持,這些註解與普通的Java代碼同樣,是在運行期間發揮做用的。在JDK1.6中實現了對JSR-269的規範,提供了一組插入式註解處理器的標準API在編譯期間對註解進行處理,咱們能夠把它看做是一組編譯器的插件,在這些插件裏面,能夠讀取,修改,添加抽象語法樹中的任意元素。

若是這些插件在處理註解期間對語法樹進行了修改,那麼編譯器將回到解析及填充符號表的過程從新處理,直到全部的插入式註解處理器都沒有了再對語法樹進行修改成止。每一次循環成爲一個Round。

有了編譯器註解處理的標準API後,咱們的代碼纔有可能干涉編譯器的行爲,因爲語法樹中的任意元素,甚至包括代碼註釋均可以在插件之中訪問到,因此經過插入式註解處理器實現的插件在功能上有很大的發揮空間。只要有足夠多的創意,程序員可使用插入式註解處理器來實現許多本來只能在編碼中完成的事情。

語義分析與字節碼生成

語法分析以後,編譯器得到了程序代碼的抽象語法樹表示,語法樹能表示一個結構正確的源程序的抽象,可是沒法保證源程序是符合邏輯的。而語義分析的主要任務就是對結構上正確的源程序進行上下文有關性質的審查,如進行類型檢查。

好比咱們有如下代碼

int a = 1;
boolean b = false;
char c = 2;

下面咱們有可能出現以下運算

int d = b+c;

其實上面的代碼在結構上能構成準確的語法樹,可是在語義上下面的運算是錯誤的。因此若是運行的話就會出現編譯不經過,沒法編譯。

本身實現一個簡單的Lombok

上面咱們瞭解了javac的過程,那麼咱們直接來本身寫一個簡單的在已有類中添加代碼的小工具,咱們就只生成set方法。首先寫一個自定義的註解類。

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

而後寫對於此註解類的註解處理器類

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("aboutjava.annotion.MySetter")
public class MySetterProcessor extends AbstractProcessor {

    private Messager messager;
    private JavacTrees javacTrees;
    private TreeMaker treeMaker;
    private Names names;

    /**
     * @Description: 1. Message 主要是用來在編譯時期打log用的
     *              2. JavacTrees 提供了待處理的抽象語法樹
     *              3. TreeMaker 封裝了建立AST節點的一些方法
     *              4. 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) {
        return false;
    }
}

此處咱們注意咱們在init方法中得到一些編譯階段的一些環境信息。咱們從環境中提取出一些關鍵的類,描述以下。

  • JavacTrees :提供了待處理的抽象語法樹
  • TreeMaker :封裝了操做AST抽象語法樹的一些方法
  • Names :提供了建立標識符的方法
  • Messager:主要是在編譯器打日誌用的

而後接下來咱們利用所提供的工具類對已存在的AST抽象語法樹進行修改。主要的修改邏輯存在於process方法中,若是返回是true的話,那麼javac過程會再次從新從解析與填充符號表處開始進行。process方法的邏輯主要以下

@Override
    public boolean process(Set<!--? extends TypeElement--> annotations, RoundEnvironment roundEnv) {
        Set<!--? extends Element--> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MySetter.class);
        elementsAnnotatedWith.forEach(e-&gt;{
            JCTree tree = javacTrees.getTree(e);
            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);
                        }
                    }
                    // 對於變量進行生成方法的操做
                    jcVariableDeclList.forEach(jcVariableDecl -&gt; {
                        messager.printMessage(Diagnostic.Kind.NOTE,jcVariableDecl.getName()+"has been processed");
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));
                    });
                    super.visitClassDef(jcClassDecl);
                }
            });
        });
        return true;
    }

其實看起來比較難,原理比較簡單,主要是咱們對於API的不熟悉因此看起來很差懂,可是主要意思就是以下

  1. 找到@MySetter註解所標註的類,得到其語法樹
  2. 遍歷其語法樹,找到其參數節點
  3. 本身建一個方法節點,並添加到語法樹中

用圖表示的話,咱們建了一個測試類TestMySetter,咱們知道其語法樹的大體結構以下圖所示。

那麼咱們的目標就是將其語法樹變成下圖所示,由於最終生成字節碼是根據語法樹來生成的,因此咱們在語法樹中添加了方法的節點,那麼在生成字節碼的時候就會生成對應方法的字節碼。

其中生成方法節點的代碼以下

private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl){

    ListBuffer<jctree.jcstatement> statements = new ListBuffer&lt;&gt;();
    // 生成表達式 例如 this.a = a;
    JCTree.JCExpressionStatement aThis = makeAssignment(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()));
    statements.append(aThis);
    JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

    // 生成入參
    JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), jcVariableDecl.getName(), jcVariableDecl.vartype, null);
    List<jctree.jcvariabledecl> parameters = List.of(param);

    // 生成返回對象
    JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());
    return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),getNewMethodName(jcVariableDecl.getName()),methodType,List.nil(),parameters,List.nil(),block,null);

}

private Name getNewMethodName(Name name){
    String s = name.toString();
    return names.fromString("set"+s.substring(0,1).toUpperCase()+s.substring(1,name.length()));
}

private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
    return treeMaker.Exec(
            treeMaker.Assign(
                    lhs,
                    rhs
            )
    );
}

最後咱們執行下面三個命令

javac -cp $JAVA_HOME/lib/tools.jar aboutjava/annotion/MySetter* -d
javac -processor aboutjava.annotion.MySetterProcessor aboutjava/annotion//TestMySetter.java
javap -p aboutjava/annotion/TestMySetter.class

能夠看到輸出的內容以下

Compiled from "TestMySetter.java"
public class aboutjava.annotion.TestMySetter {
  private java.lang.String name;
  public void setName(java.lang.String);
  public aboutjava.annotion.TestMySetter();
}

能夠看到字節碼中已經生成了咱們須要的setName方法。

代碼地址

總結

到目前爲止大概將Lombok的原理講明白了,其實就是對於抽象語法樹的各類操做。其實你們還能夠利用編譯期作許多的事情,例如代碼規範的檢查之類的。這裏我只寫了關於set方法的建立,你們有興趣的能夠本身寫代碼本身試一下關於Lombok的get方法的建立。

參考

相關文章
相關標籤/搜索