首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JDK 內置 Java Compiler API 解析源碼:從語法樹提取變量、方法、註解完整實戰

     編輯:Java 相關知識

前言:為什麼要在運行時解析 Java 源碼?

  • 靜態分析工具(如 Lombok、Error Prone、Checkstyle)核心技術。
  • 自動化代碼生成、重構、文檔生成、安全掃描。
  • JDK tools.jar (JDK 9+ 為 jdk.compiler 模塊) 提供標準化 Compiler Tree API,無需第三方依賴。

環境準備

  • JDK 8:需將 $JAVA_HOME/lib/tools.jar 加入類路徑。
  • JDK 9+:模塊化,需在 module-info.java 聲明 requires jdk.compiler; 或啟動參數 --add-modules jdk.compiler。
  • Maven 依賴(可選,避免硬編碼路徑):
    <dependency>
        <groupId>com.sun</groupId>
        <artifactId>tools</artifactId>
        <version>${java.version}</version>
        <scope>system</scope>
        <systemPath>${java.home}/../lib/tools.jar</systemPath>
    </dependency>

核心 API 入口類

類/接口作用
javax.tools.JavaCompiler編譯器入口,ToolProvider.getSystemJavaCompiler() 獲取
javax.tools.JavaFileObject源文件抽象(內存/磁盤/ZIP 均可)
com.sun.source.tree.CompilationUnitTree單個源文件的語法樹根節點
com.sun.source.util.TreeScanner訪問者模式遍歷語法樹的基類
com.sun.source.util.Trees工具類:節點 ↔ Element/Symbol 映射、位置信息

實戰一:提取所有變量聲明(含類型、註解、包名)

public class VariableExtractor {
    public static void main(String[] args) throws IOException {
        // 1. 構建編譯任務
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null);
        Iterable files = fm.getJavaFileObjects("src/main/java/com/example/User.java");

        JavaCompiler.CompilationTask task = compiler.getTask(null, fm, null, null, null, files);
        // 2. 解析獲取語法樹
        Iterable units = task.parse();
        
        // 3. 遍歷
        for (CompilationUnitTree unit : units) {
            unit.accept(new VariableVisitor(), null);
        }
    }

    static class VariableVisitor extends TreeScanner {
        private String currentPackage = "";

        @Override
        public Void visitCompilationUnit(CompilationUnitTree node, Void p) {
            currentPackage = node.getPackageName().toString();
            return super.visitCompilationUnit(node, p);
        }

        @Override
        public Void visitVariable(VariableTree node, Void p) {
            // 過濾局部變量:僅保留字段
            if (node.getKind() == ElementKind.FIELD) {
                List annos = node.getModifiers().getAnnotations();
                String annoStr = annos.isEmpty() ? "無" : annos.get(0).getAnnotationType().toString();
                System.out.printf("包:%s | 字段:%s | 類型:%s | 註解:%s%n",
                    currentPackage, node.getName(), node.getType(), annoStr);
            }
            return super.visitVariable(node, p);
        }
    }
}

測試源文件 User.java

package com.example;
import com.sun.istack.internal.Nullable;
public class User {
    @Nullable private String foo = "123";
    private Foo bar;
    public void setFoo(String foo) { this.foo = foo; }
    static class Foo { private String inner; }
}

輸出結果

包:com.example | 字段:foo | 類型:String | 註解:@Nullable
包:com.example | 字段:bar | 類型:Foo | 註解:無

實戰二:提取類結構、方法簽名、方法體語句

static class ClassStructureVisitor extends TreeScanner {
    @Override
    public Void visitClass(ClassTree node, Void p) {
        System.out.println("=== 類: " + node.getSimpleName() + " ===");
        for (Tree member : node.getMembers()) {
            switch (member.getKind()) {
                case VARIABLE -> {
                    VariableTree v = (VariableTree) member;
                    System.out.println("  字段: " + v.getName() + " : " + v.getType());
                }
                case METHOD -> {
                    MethodTree m = (MethodTree) member;
                    System.out.println("  方法: " + m.getName() + m.getParameters() + " -> " + m.getReturnType());
                    // 遞歸進入方法體
                    m.getBody().accept(new StatementPrinter(), null);
                }
                default -> {}
            }
        }
        return super.visitClass(node, p);
    }
}

static class StatementPrinter extends TreeScanner {
    @Override public Void visitIf(IfTree node, Void p) {
        System.out.println("    IF: " + node.getCondition());
        return super.visitIf(node, p);
    }
    @Override public Void visitVariable(VariableTree node, Void p) {
        System.out.println("    區域變量: " + node.getName());
        return super.visitVariable(node, p);
    }
    @Override public Void visitExpressionStatement(ExpressionStatementTree node, Void p) {
        System.out.println("    語句: " + node.getExpression());
        return super.visitExpressionStatement(node, p);
    }
}

進階話題:獲取完整類型信息(解決泛型擦除、導入別名)

Tree API 僅提供語法層面的類型名(如 List 只能拿到 List),要獲取 **語義級類型**(含泛型參數、全限定名),需結合 Trees 工具類與 Element 模型:

Trees trees = Trees.instance(task); // task 為 CompilationTask
for (CompilationUnitTree unit : units) {
    for (Tree typeDecl : unit.getTypeDecls()) {
        if (typeDecl instanceof ClassTree clazz) {
            Element classElem = trees.getElement(trees.getPath(unit, clazz));
            TypeMirror classType = classElem.asType(); // 完整類型
            System.out.println("類完整類型: " + classType);
            for (VariableTree field : clazz.getMembers().stream()
                    .filter(t -> t.getKind() == Tree.Kind.VARIABLE)
                    .map(t -> (VariableTree) t).toList()) {
                Element fieldElem = trees.getElement(trees.getPath(unit, field));
                System.out.println("  字段完整類型: " + fieldElem.asType()); // 含泛型參數
            }
        }
    }
}

避坑指南與最佳實踐

  1. 非標準 API:com.sun.source.* 屬於 JDK 內部 API,版本間可能變動,生產工具建議封裝適配層。
  2. 編碼問題:StandardJavaFileManager 默認平台編碼,中文項目請顯式指定 StandardCharsets.UTF_8。
  3. 依賴解析:被解析源碼引用的第三方類必須在類路徑中,否則會報 cannot find symbol 但不拋異常,需檢查 task.analyze() 或 diagnosticListener。
  4. 性能:解析大型項目較慢,建議增量解析或緩存 CompilationUnitTree。

總結

JDK Compiler Tree API 提供了 **純 Java、零依賴** 的源碼靜態分析能力。掌握 TreeScanner 訪問者模式與 Trees 語義映射,即可構建變量提取、方法調用鏈分析、註解處理器等各類元編程工具。Lombok、MapStruct、MyBatis-Plus 等知名庫均基於此原理實現編譯期增強。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved