Lombok 原理與實現

Sumkor發表於2021-12-29

本文主要包含以下內容:

  1. Lombok 的實現機制分析。
  2. 插入式註解處理器的說明及使用。
  3. 動手實現 lombok 的 @Getter 和 @Setter 註解。
  4. 配置 IDEA 以除錯 Java 編譯過程。

1. Lombok

官網介紹:

Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java.
Never write another getter or equals method again, with one annotation your class has a fully featured builder, Automate your logging variables, and much more.

在 Maven 中新增 Lombok 依賴:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
    <scope>provided</scope>
</dependency>

簡單的例子如下,通過新增註解的方式,自動生成 getter/setter 方法。

package com.sumkor;

import lombok.Getter;
import lombok.Setter;

/**
 * @author Sumkor
 * @since 2021/12/27
 */
@Setter
@Getter
public class MyTest {

    private String value;

    public static void main(String[] args) {
        MyTest myTest = new MyTest();
        myTest.setValue("hello");
        System.out.println(myTest.getValue());
    }
}

編譯後的程式碼如下:

package com.sumkor;

public class MyTest {
    private String value;

    public MyTest() {
    }

    public static void main(String[] args) {
        MyTest myTest = new MyTest();
        myTest.setValue("hello");
        System.out.println(myTest.getValue());
    }

    public void setValue(String value) {
        this.value = value;
    }

    public String getValue() {
        return this.value;
    }
}

2. Annotation Processor

2.1 Javac 編譯器

《深入理解 Java 虛擬機器:JVM 高階特性與最佳實踐》中的第 10 章《前端編譯與優化》對 Javac 編譯器進行了介紹。

Javac 編譯過程如下:

從 Javac 程式碼的總體結構來看,編譯過程大致可以分為 1 個準備過程和 3 個處理過程,它們分別如下所示。

  1. 準備過程:初始化插入式註解處理器。
  2. 解析與填充符號表過程,包括:詞法、語法分析;填充符號表。
  3. 插入式註解處理器的註解處理過程。
  4. 分析與位元組碼生成過程。

Javac的編譯過程

重點關注對插入式註解處理器的說明:

JDK 5 之後,Java 語言提供了對註解(Annotations)的支援,註解在設計上原本是與普通的 Java 程式碼一樣,都只會在程式執行期間發揮作用的。但在 JDK 6 中又提出並通過了 JSR-269 提案,該提案設計了一組被稱為“插入式註解處理器”的標準 API,可以提前至編譯期對程式碼中的特定註解進行處理,從而影響到前端編譯器的工作過程。我們可以把插入式註解處理器看作是一組編譯器的外掛,當這些外掛工作時,允許讀取、修改、新增抽象語法樹中的任意元素。如果這些外掛在處理註解期間對語法樹進行過修改,編譯器將回到解析及填充符號表的過程重新處理,直到所有插入式註解處理器都沒有再對語法樹進行修改為止。

可以看到 Lombok 是基於插入式註解處理器來實現:

有了編譯器註解處理的標準 API 後,程式設計師的程式碼才有可能干涉編譯器的行為,由於語法樹中的任意元素,甚至包括程式碼註釋都可以在外掛中被訪問到,所以通過插入式註解處理器實現的外掛在功能上有很大的發揮空間。只要有足夠的創意,程式設計師能使用插入式註解處理器來實現許多原本只能在編碼中由人工完成的事情。譬如 Java 著名的編碼效率工具 Lombok,它可以通過註解來實現自動產生 getter/setter 方法、進行空置檢查、生成受查異常表、產生 equals() 和 hashCode() 方法,等等,幫助開發人員消除 Java 的冗長程式碼,這些都是依賴插入式註解處理器來實現的。

2.2 Java 註解

Java 中的註解分為執行時註解和編譯時註解,通過設定元註解 @Retention 中的 RetentionPolicy 指定註解的保留策略:

/**
 * Annotation retention policy.  The constants of this enumerated type
 * describe the various policies for retaining annotations.  They are used
 * in conjunction with the {@link Retention} meta-annotation type to specify
 * how long annotations are to be retained.
 *
 * @author  Joshua Bloch
 * @since 1.5
 */
public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

說明:

  • SOURCE:表示註解的資訊會被編譯器拋棄,不會留在 class 檔案中,註解的資訊只會留在原始檔中。
  • CLASS:表示註解的資訊被保留在 class 檔案中。當程式編譯時,但不會被虛擬機器讀取在執行的時候。
  • RUNTIME:表示註解的資訊被保留在 class 檔案中。當程式編譯時,會被虛擬機器保留在執行時。

日常開發中使用的註解都是 RUNTIME 型別的,可以在執行期被反射呼叫讀取。

javax.annotation.Resource

@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
public @interface Resource

而 Lombok 中的註解是 SOURCE 型別的,只會在編譯期間被插入式註解處理器讀取。

lombok.Getter

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Getter

2.3 插入式註解處理器

自定義的註解處理器需要繼承 AbstractProcessor 這個類,基本的框架大體如下:

package com.sumkor.processor;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import java.util.Set;

@SupportedAnnotationTypes("com.sumkor.annotation.Getter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return true;
    }
}

其中:

  • @SupportedAnnotationTypes 代表了這個註解處理器對哪些註解感興趣,可以使用星號*作為萬用字元代表對所有的註解都感興趣。
  • @SupportedSourceVersion 指出這個註解處理器可以處理哪些版本的 Java 程式碼。
  • init() 用於獲取編譯階段的一些環境資訊。
  • process() 可以編寫處理語法樹的具體邏輯。如果不需要改變或新增抽象語法樹中的內容,process() 方法就可以返回一個值為 false 的布林值,通知編譯器這個輪次中的程式碼未發生變化。

本文後續會實現具體的自定義的插入式註解處理器,並進行打斷點除錯。

2.4 Javac APT

利用插入式註解處理器在編譯階段修改語法樹,需要用到 Javac 中的註解處理工具 APT(Annotation Processing Tool),這是 Sun 為了幫助註解的處理過程而提供的工具,APT 被設計為操作 Java 原始檔,而不是編譯後的類。

本文使用的是 JDK 8,Javac 相關的原始碼存放在 tools.jar 中,要在程式中使用的話就必須把這個庫放到類路徑上。注意,到了 JDK 9 時,整個 JDK 所有的 Java 類庫都採用模組化進行重構劃分,Javac 編譯器就被挪到了 jdk.compiler 模組,並且對該模組的訪問進行了嚴格的限制。

jdk.compiler

2.5 JCTree 語法樹

com.sun.tools.javac.tree.JCTree 是語法樹元素的基類,包含以下重要的子類:

  • JCStatement:宣告語法樹節點,常見的子類如下

    • JCBlock:語句塊語法樹節點
    • JCReturn:return語句語法樹節點
    • JCClassDecl:類定義語法樹節點
    • JCVariableDecl:欄位/變數定義語法樹節點
  • JCMethodDecl:方法定義語法樹節點
  • JCModifiers:訪問標誌語法樹節點
  • JCExpression:表示式語法樹節點,常見的子類如下

    • JCAssign:賦值語句語法樹節點
    • JCIdent:識別符號語法樹節點,可以是變數,型別,關鍵字等

JCTree 利用的是訪問者模式,將資料與資料的處理進行解耦。部分原始碼如下:

public abstract class JCTree implements Tree, Cloneable, DiagnosticPosition {

    public int pos = -1;

    public abstract void accept(JCTree.Visitor visitor);

}

利用訪問者 TreeTranslator,可以訪問 JCTree 上的類定義節點 JCClassDecl,進而可以獲取類中的成員變數、方法等節點並進行修改。

編碼過程中,可以利用 javax.annotation.processing.Messager 來列印編譯過程的相關資訊。
注意,Messager 的 printMessage 方法在列印 log 的時候會自動過濾重複的 log 資訊。

比起列印日誌,利用 IDEA 工具對編譯過程進行 debug,對 JCTree 語法樹會有更為直觀的認識。
文末提供了在 IDEA 中除錯插入式註解處理器的配置。

3. 動手實現

分別建立兩個專案,用於實現和驗證 @Getter 和 @Setter 註解。

建立專案 lombok-processor,包含自定義註解和插入式註解處理器。
建立專案 lombok-app,該專案依賴了 lombok-processor 專案,使用其中的自定義註解進行測試。

3.1 processor 專案

專案整體結構如下:
lombok-processor

Maven 配置

由於需要在編譯階段修改 Java 語法樹,需要呼叫語法樹相關的 API,因此將 JDK 目錄下的 tools.jar 引入當前專案。

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>1.8</version>
    <scope>system</scope>
    <systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>

lombok-processor 專案採用 Java SPI 機制,使其自定義的插入式註解處理器對 lombok-app 專案生效。由於 lombok-processor 專案在編譯期間需要排除掉自身的插入式註解處理器,因此配置 maven resource 以過濾掉 SPI 檔案,等到打包的時候,再將 SPI 檔案加入 lombok-processor 專案的 jar 包中。
此外,為了方便除錯,將 lombok-processor 專案的原始碼也釋出到本地倉庫中。

完整的 maven build 配置如下:

<build>
    <!-- 配置一下resources標籤,過濾掉META-INF資料夾,這樣在編譯的時候就不會找到services的配置 -->
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <excludes>
                <exclude>META-INF/**/*</exclude>
            </excludes>
        </resource>
    </resources>

    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.1</version>
            <configuration>
                <source>${maven.compiler.target}</source>
                <target>${maven.compiler.target}</target>
            </configuration>
        </plugin>
        <!-- 在打包前(prepare-package生命週期)再把services資料夾重新拷貝過來 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-resources-plugin</artifactId>
            <version>2.6</version>
            <executions>
                <execution>
                    <id>process-META</id>
                    <phase>prepare-package</phase>
                    <goals>
                        <goal>copy-resources</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>target/classes</outputDirectory>
                        <resources>
                            <resource>
                                <directory>${basedir}/src/main/resources/</directory>
                                <includes>
                                    <include>**/*</include>
                                </includes>
                            </resource>
                        </resources>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        <!-- Source attach plugin -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-source-plugin</artifactId>
            <version>3.0.1</version>
            <configuration>
                <attach>true</attach>
            </configuration>
            <executions>
                <execution>
                    <phase>compile</phase>
                    <goals>
                        <goal>jar</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

自定義註解

自定義註解主要使用了兩個元註解:

  • @Target({ElementType.TYPE}) 表示是對類的註解。
  • @Retention(RetentionPolicy.SOURCE)表示這個註解只在編譯期起作用,在執行時將不存在。

自定義 @Getter 註解:

package com.sumkor.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE}) 
@Retention(RetentionPolicy.SOURCE) 
public @interface Getter {
}

自定義 @Setter 註解:

package com.sumkor.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Setter {
}

自定義註解處理器

BaseProcessor

定義抽象的基類 BaseProcessor,用於統一獲取編譯階段的工具類,如 JavacTrees、TreeMaker 等。
由於本專案需要在 IDEA 中進行除錯和執行,因此引入 IDEA 環境的 ProcessingEnvironment。

package com.sumkor.processor;

import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Names;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import java.lang.reflect.Method;

/**
 * @author Sumkor
 * @since 2021/12/27
 */
public abstract class BaseProcessor extends AbstractProcessor {

    protected Messager messager;   // 用來在編譯期打log用的
    protected JavacTrees trees;    // 提供了待處理的抽象語法樹
    protected TreeMaker treeMaker; // 封裝了建立AST節點的一些方法
    protected Names names;         // 提供了建立識別符號的方法

    /**
     * 獲取編譯階段的一些環境資訊
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        processingEnv = jbUnwrap(ProcessingEnvironment.class, processingEnv);
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.trees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
        this.names = Names.instance(context);
    }

    /**
     * 獲取 IDEA 環境下的 ProcessingEnvironment
     */
    private static <T> T jbUnwrap(Class<? extends T> iface, T wrapper) {
        T unwrapped = null;
        try {
            final Class<?> apiWrappers = wrapper.getClass().getClassLoader().loadClass("org.jetbrains.jps.javac.APIWrappers");
            final Method unwrapMethod = apiWrappers.getDeclaredMethod("unwrap", Class.class, Object.class);
            unwrapped = iface.cast(unwrapMethod.invoke(null, iface, wrapper));
        }
        catch (Throwable ignored) {}
        return unwrapped != null? unwrapped : wrapper;
    }

}

GetterProcessor

自定義註解 @Getter 對應的註解處理器如下,程式碼流程:

  1. 獲取被 @Getter 註解修飾的類。
  2. 找到類上的所有成員變數。
  3. 為成員變數構造 getter 方法。

難點在於對 JavacTrees 和 TreeMaker 相關 API 的使用上,文中關鍵程式碼均有註釋,方便理解。

package com.sumkor.processor;

import com.sumkor.annotation.Getter;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeTranslator;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Name;

import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;

/**
 * @author Sumkor
 * @since 2021/12/24
 */
@SupportedAnnotationTypes("com.sumkor.annotation.Getter")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class GetterProcessor extends BaseProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager.printMessage(Diagnostic.Kind.NOTE, "========= GetterProcessor init =========");
    }

    /**
     * 對 AST 進行處理
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 獲取被自定義 Getter 註解修飾的元素
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(Getter.class);
        set.forEach(element -> {
            // 根據元素獲取對應的語法樹 JCTree
            JCTree jcTree = trees.getTree(element);
            jcTree.accept(new TreeTranslator() {
                // 處理語法樹的類定義部分 JCClassDecl
                @Override
                public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                    List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil();
                    for (JCTree tree : jcClassDecl.defs) {
                        // 找到語法樹上的成員變數節點,儲存到 jcVariableDeclList 集合
                        if (tree.getKind().equals(Tree.Kind.VARIABLE)) {
                            JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) tree;
                            jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl);
                        }
                    }
                    // 為成員變數構造 getter 方法,並新增到 JCClassDecl 之中
                    jcVariableDeclList.forEach(jcVariableDecl -> {
                        messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " has been processed");
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
                    });
                    super.visitClassDef(jcClassDecl);
                }

            });
        });
        return true;
    }

    /**
     * 為成員遍歷構造 getter 方法
     */
    private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
        ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
        // 生成表示式 return this.value;
        statements.append(treeMaker.Return(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName())));
        // 加上大括號 { return this.value; }
        JCTree.JCBlock body = treeMaker.Block(0, statements.toList());
        // 組裝方法
        return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewMethodName(jcVariableDecl.getName()), jcVariableDecl.vartype, List.nil(), List.nil(), List.nil(), body, null);
    }

    /**
     * 駝峰命名法
     */
    private Name getNewMethodName(Name name) {
        String s = name.toString();
        return names.fromString("get" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
    }

}

SetterProcessor

自定義註解 @Setter 對應的註解處理器 SetterProcessor,整體流程與 GetterProcessor 差不多:

  1. 獲取被 @Setter 註解修飾的類。
  2. 找到類上的所有成員變數。
  3. 為成員變數構造 setter 方法。

重點關注構造 setter 方法的邏輯:

/**
 * 為成員構造 setter 方法
 */
private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl) {
    ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
    // 生成表示式 this.value = value;
    JCTree.JCExpressionStatement aThis = makeAssignment(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName()));
    statements.append(aThis);
    // 加上大括號 { this.value = value; }
    JCTree.JCBlock block = treeMaker.Block(0, statements.toList());

    // 生成方法引數之前,指明當前語法節點在語法樹中的位置,避免出現異常 java.lang.AssertionError: Value of x -1
    treeMaker.pos = jcVariableDecl.pos;

    // 生成方法引數 String value
    JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), jcVariableDecl.getName(), jcVariableDecl.vartype, null);
    List<JCTree.JCVariableDecl> parameters = List.of(param);
    // 生成返回物件 void
    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);
}

/**
 * 賦值操作 lhs = rhs
 */
private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
    return treeMaker.Exec(
            treeMaker.Assign(lhs, rhs)
    );
}

/**
 * 駝峰命名法
 */
private Name getNewMethodName(Name name) {
    String s = name.toString();
    return names.fromString("set" + s.substring(0, 1).toUpperCase() + s.substring(1, name.length()));
}

3.2 app 專案

專案整體結構如下,這裡 IDEA 的標紅提示是由於無法識別自定義註解導致的,不影響專案執行。
lombok-app
在 maven 中引入 lombok-processor 專案:

<dependency>
    <groupId>com.sumkor</groupId>
    <artifactId>lombok-processor</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

編寫測試類,引入自定義的註解 @Getter 和 @Setter,可以看到 IDEA 雖然標紅提示找不到方法,但是可以正常通過編譯和執行。
 title=

4. 除錯

4.1 IDEA 配置

使用 Attach Remote JVM 的方式,對 lombok-app 專案的編譯過程進行除錯。

  1. 建立一個遠端除錯,指定埠為 5005。
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

run/debug configuration

  1. 在選單中選擇:Help -> Edit Custom VM Options,新增以下內容並重啟 IDEA。這裡的埠是上一步中選擇需要 attach 的埠。
-Dcompiler.process.debug.port=5005
  1. 開啟 IDEA 的 Debug Build Process。注意,這個選項在每次 IDEA 重啟後都會預設關閉。

Debug Build Process

4.2 開始除錯

  1. 在註解處理器中新增斷點,推薦斷點打在 AbstractProcessor#init 中。
  2. 執行 mvn clean 操作,清除掉上一次構建完成的內容。
  3. 執行 ctrl + F9 操作進行 lombok-app 專案構建。
    build project
    在狀態列可以看到構建過程在等待 debugger 連線:
    Build Process Pause
  4. 執行剛才建立的遠端除錯。
    Start Run Configuration
    可以看到進入了 AbstractProcessor#init 中的斷點。
    AbstractProcessor#init對自定義的註解處理器進行除錯,觀察 TreeMaker 對語法樹的修改結果。
    SetterProcessor

    4.3 問題解決

在編寫 SetterProcessor#process 方法的時候,如果缺少 treeMaker.pos = jcVariableDecl.pos; 這一行程式碼,在編譯過程會報錯提示:java.lang.AssertionError: Value of x -1,具體編譯資訊如下:

Executing pre-compile tasks...
Loading Ant configuration...
Running Ant tasks...
Running 'before' tasks
Checking sources
Copying resources... [lombok-app]
Parsing java... [lombok-app]
java: ========= GetterProcessor init =========
java: value has been processed
java: ========= SetterProcessor init =========
java: 編譯器 (1.8.0_91) 中出現異常錯誤。如果在 Bug Database (http://bugs.java.com) 中沒有找到該錯誤, 請通過 Java Bug 報告頁 (http://bugreport.java.com) 建立該 Java 編譯器 Bug。請在報告中附上您的程式和以下診斷資訊。謝謝。
java: java.lang.AssertionError: Value of x -1
java:     at com.sun.tools.javac.util.Assert.error(Assert.java:133)
java:     at com.sun.tools.javac.util.Assert.check(Assert.java:94)
java:     at com.sun.tools.javac.util.Bits.incl(Bits.java:186)
java:     at com.sun.tools.javac.comp.Flow$AssignAnalyzer.initParam(Flow.java:1858)
java:     at com.sun.tools.javac.comp.Flow$AssignAnalyzer.visitMethodDef(Flow.java:1807)
java:     at com.sun.tools.javac.tree.JCTree$JCMethodDecl.accept(JCTree.java:778)
java:     at com.sun.tools.javac.tree.TreeScanner.scan(TreeScanner.java:49)
java:     at com.sun.tools.javac.comp.Flow$BaseAnalyzer.scan(Flow.java:404)
java:     at com.sun.tools.javac.comp.Flow$AssignAnalyzer.scan(Flow.java:1382)
java:     at com.sun.tools.javac.comp.Flow$AssignAnalyzer.visitClassDef(Flow.java:1749)
java:     at com.sun.tools.javac.tree.JCTree$JCClassDecl.accept(JCTree.java:693)
java:     at com.sun.tools.javac.comp.Flow$AssignAnalyzer.analyzeTree(Flow.java:2446)
java:     at com.sun.tools.javac.comp.Flow$AssignAnalyzer.analyzeTree(Flow.java:2429)
java:     at com.sun.tools.javac.comp.Flow.analyzeTree(Flow.java:211)
java:     at com.sun.tools.javac.main.JavaCompiler.flow(JavaCompiler.java:1327)
java:     at com.sun.tools.javac.main.JavaCompiler.flow(JavaCompiler.java:1296)
java:     at com.sun.tools.javac.main.JavaCompiler.compile2(JavaCompiler.java:901)
java:     at com.sun.tools.javac.main.JavaCompiler.compile(JavaCompiler.java:860)
java:     at com.sun.tools.javac.main.Main.compile(Main.java:523)
java:     at com.sun.tools.javac.api.JavacTaskImpl.doCall(JavacTaskImpl.java:129)
java:     at com.sun.tools.javac.api.JavacTaskImpl.call(JavacTaskImpl.java:138)
java:     at org.jetbrains.jps.javac.JavacMain.compile(JavacMain.java:238)
java:     at org.jetbrains.jps.incremental.java.JavaBuilder.lambda$compileJava$2(JavaBuilder.java:514)
java:     at org.jetbrains.jps.incremental.java.JavaBuilder.invokeJavac(JavaBuilder.java:560)
java:     at org.jetbrains.jps.incremental.java.JavaBuilder.compileJava(JavaBuilder.java:512)
java:     at org.jetbrains.jps.incremental.java.JavaBuilder.compile(JavaBuilder.java:355)
java:     at org.jetbrains.jps.incremental.java.JavaBuilder.doBuild(JavaBuilder.java:280)
java:     at org.jetbrains.jps.incremental.java.JavaBuilder.build(JavaBuilder.java:234)
java:     at org.jetbrains.jps.incremental.IncProjectBuilder.runModuleLevelBuilders(IncProjectBuilder.java:1485)
java:     at org.jetbrains.jps.incremental.IncProjectBuilder.runBuildersForChunk(IncProjectBuilder.java:1123)
java:     at org.jetbrains.jps.incremental.IncProjectBuilder.buildTargetsChunk(IncProjectBuilder.java:1268)
java:     at org.jetbrains.jps.incremental.IncProjectBuilder.buildChunkIfAffected(IncProjectBuilder.java:1088)
java:     at org.jetbrains.jps.incremental.IncProjectBuilder.buildChunks(IncProjectBuilder.java:854)
java:     at org.jetbrains.jps.incremental.IncProjectBuilder.runBuild(IncProjectBuilder.java:441)
java:     at org.jetbrains.jps.incremental.IncProjectBuilder.build(IncProjectBuilder.java:190)
java:     at org.jetbrains.jps.cmdline.BuildRunner.runBuild(BuildRunner.java:132)
java:     at org.jetbrains.jps.cmdline.BuildSession.runBuild(BuildSession.java:318)
java:     at org.jetbrains.jps.cmdline.BuildSession.run(BuildSession.java:146)
java:     at org.jetbrains.jps.cmdline.BuildMain$MyMessageHandler.lambda$channelRead0$0(BuildMain.java:218)
java:     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
java:     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
java:     at java.lang.Thread.run(Thread.java:745)
java: Compilation failed: internal java compiler error
Checking dependencies... [lombok-app]
Dependency analysis found 0 affected files
Errors occurred while compiling module 'lombok-app'
javac 1.8.0_91 was used to compile java sources
Finished, saving caches...
Compilation failed: errors: 1; warnings: 0
Executing post-compile tasks...
Loading Ant configuration...
Running Ant tasks...
Synchronizing output directories...

從異常堆疊資訊可知是 com.sun.tools.javac.util.Bits.incl(Bits.java:186) 出現報錯,定位到異常位置:

com.sun.tools.javac.util.Bits#incl

public void incl(int var1) {
    Assert.check(this.currentState != Bits.BitsState.UNKNOWN);
    Assert.check(var1 >= 0, "Value of x " + var1); // 這一行報錯
    this.sizeTo((var1 >>> 5) + 1);
    this.bits[var1 >>> 5] |= 1 << (var1 & 31);
    this.currentState = Bits.BitsState.NORMAL;
}

打斷點分析異常鏈路,可以定位到是 SetterProcessor#process 方法導致的報錯。導致問題的核心值是 JCTree 的 pos 欄位,該欄位用於指明當前語法樹節點在語法樹中的位置,而使用 TreeMaker 生成的 pos 都為固定值,需要將此欄位設定為所解析元素的 pos 即可(修改後的 SetterProcessor#process 方法見上一節)。

5. 參考


作者:Sumkor
連結:https://segmentfault.com/a/11...

相關文章