JavaParser 簡介

banq發表於2024-05-13

在本文中,我們將瞭解JavaParser庫。我們將瞭解它是什麼、我們可以用它做什麼以及如何使用它。

什麼是JavaParser?
JavaParser 是一個用於處理 Java 原始碼的開源庫。它允許我們將 Java 原始碼解析為抽象語法樹(AST)。完成此操作後,我們可以分析解析的程式碼、操作它,甚至編寫新程式碼。

使用 JavaParser,我們可以解析用 Java 編寫的原始碼,最高可達 Java 18。這包括所有穩定語言功能,但可能不包括任何預覽功能。

依賴關係
在使用 JavaParser 之前,我們需要在構建中包含最新版本,在撰寫本文時為3.25.10 。

我們需要包含的主要依賴項是javaparser-core。如果我們使用 Maven,我們可以在pom.xml檔案中包含此依賴項:

<dependency>
    <groupId>com.github.javaparser</groupId>
    <artifactId>javaparser-core</artifactId>
    <version>3.25.10</version>
</dependency>

或者,如果我們使用 Gradle,我們可以將其包含在build.gradle檔案中:

implementation("com.github.javaparser:javaparser-core:3.25.10")


此時,我們已準備好開始在我們的應用程式中使用它。

還有兩個附加依賴項可用。依賴項com.github.javaparser:javaparser-symbol-solver-core提供了一種分析解析後的 AST 以查詢 Java 元素及其宣告之間關係的方法。依賴項com.github.javaparser:javaparser-core-serialization提供了一種將解析後的 AST 與 JSON 進行序列化的方法。

解析Java程式碼
一旦我們在應用程式中設定了依賴項,我們就可以開始了。Java 程式碼的解析始終從StaticJavaParser類開始。這為我們提供了幾種不同的解析程式碼的機制,具體取決於我們解析的內容以及它的來源。

1.解析原始檔
我們首先要分析的是整個原始檔。我們可以使用StaticJavaParser.parse()方法來做到這一點。幾種過載的替代方案允許我們以不同的方式提供原始碼——直接作為字串、作為本地檔案系統上的檔案、或者作為某些資源的輸入流或讀取器。所有這些都以相同的方式工作,並且只是提供要解析的程式碼的便捷方法。

讓我們看看它的實際效果。在這裡,我們將嘗試解析提供的原始碼並生成一個CompilationUnit作為結果:

CompilationUnit parsed = StaticJavaParser.parse("class TestClass {}");

這代表了我們的 AST,讓我們可以檢查和操作解析後的程式碼。

2.解析語句
各個語句位於我們可以解析的程式碼範圍的另一端。我們使用StaticJavaParser.parseStatement()方法來完成此操作。與原始檔不同,只有一個版本,它採用包含要解析的語句的單個字串。

此方法返回一個Statement物件,該物件表示已解析的語句:

Statement parsed = StaticJavaParser.parseStatement("final int answer = 42;");

3.解析其他結構
JavaParser 還可以解析許多其他構造,涵蓋直至 Java 18 的整個 Java 語言。每個構造都有一個單獨的專用解析方法,並返回表示解析程式碼的適當型別。例如,我們可以使用parseAnnotation()來解析註釋,parseImport()來解析匯入語句,parseBlock()來解析語句塊,等等。

在內部,JavaParser 將使用完全相同的程式碼來解析程式碼的各個部分。例如,當使用parseBlock()解析塊時,JavaParser 最終將得到與parseStatement()直接呼叫的程式碼相同的程式碼。這意味著我們可以依賴這些不同的解析方法,對相同的程式碼子集發揮相同的作用。

我們確實需要確切地知道我們正在解析什麼型別的程式碼,以便選擇正確的解析方法。例如,使用parseStatement()方法解析類定義將會失敗。

4.格式錯誤的程式碼
如果解析失敗,JavaParser 將丟擲一個ParseProblemException 異常,準確指出程式碼出了什麼問題。例如,如果我們嘗試解析格式錯誤的類定義,那麼我們將得到類似以下內容的資訊:

ParseProblemException parseProblemException = assertThrows(ParseProblemException.class,
    () -> StaticJavaParser.parse(<font>"class TestClass"));
assertEquals(1, parseProblemException.getProblems().size());
assertEquals(
"Parse error. Found <EOF>, expected one of  \&#34<\&#34 \&#34extends\&#34 \&#34implements\&#34 \&#34permits\&#34 \&#34{\&#34"
    parseProblemException.getProblems().get(0).getMessage());

從這個錯誤資訊我們可以看出問題是類定義錯誤。在 Java 中,這樣的語句後面必須跟一個“ <”(泛型定義、extends或Implements關鍵字),或者跟一個“ {”來啟動類的實際主體。

5. 分析解析程式碼
一旦我們解析了一些程式碼,我們就可以開始分析它並從中學習。這類似於正在執行的應用程式中的反射,僅針對已解析的原始碼而不是當前正在執行的程式碼。

1.訪問已解析的元素
一旦我們解析了一些原始碼,我們就可以查詢 AST 來訪問各個元素。我們具體如何做到這一點取決於我們想要訪問的元素和我們解析的內容。

例如,如果我們已將原始檔解析為 CompilationUnit ,那麼我們可以使用getClassByName()訪問我們期望存在的類:

Optional<ClassOrInterfaceDeclaration> cls = compilationUnit.getClassByName("TestClass");

請注意,這會返回一個Optional<ClassOrInterfaceDeclaration>。使用可選是因為我們不能保證該型別存在於該編譯單元中。在其他情況下,我們也許能夠保證元素的存在。例如,類總是有一個名稱,因此ClassOrInterfaceDeclaration.getName()不需要返回Optional。

在每個階段,我們只能直接訪問當前正在使用的最外層的元素。例如,如果我們透過解析原始檔獲得了CompilationUnit ,那麼我們可以訪問包宣告、匯入語句和頂級型別,但無法訪問這些型別中的成員。但是,一旦我們訪問其中一種型別,我們就可以訪問其中的成員。

2.迭代解析的元素
在某些情況下,我們可能不確切知道解析的程式碼中存在哪些元素,或者我們只是想使用某種型別的所有元素而不是僅使用一個。

我們的每個 AST 型別都可以訪問整個範圍的適當巢狀元素。具體如何工作取決於我們想要處理什麼。例如,我們可以使用以下命令從CompilationUnit中提取所有匯入語句:

NodeList<ImportDeclaration> imports = compilationUnit.getImports();
不需要Optional,因為這保證返回結果。但是,如果不存在匯入,則此結果可能是空列表。

完成此操作後,我們可以將其視為任何集合。 NodeList型別正確實現了java.util.List ,因此我們可以像任何其他列表一樣使用它。

3.迭代整個 AST
除了從解析的程式碼中提取一種型別的元素之外,我們還可以迭代整個解析樹。JavaParser 中的所有 AST 型別都實現了訪問者模式,允許我們使用自定義訪問者訪問已解析原始碼中的每個元素:

compilationUnit.accept(visitor, arg);

我們可以使用兩種標準型別的訪問者。這兩個方法對於每種可能的 AST 型別都有一個Visit()方法,該方法採用傳遞到accept()呼叫中的狀態引數。

其中最簡單的是VoidVisitor<A>。每個 AST 型別都有一個方法,並且沒有返回值。然後,我們有一個介面卡型別 - VoidVisitorAdapter - 它為我們提供了一個標準實現,以幫助確保整個樹被正確呼叫。

然後我們只需要實現我們感興趣的方法 - 例如:

compilationUnit.accept(new VoidVisitorAdapter<Object>() {
    @Override
    public void visit(MethodDeclaration n, Object arg) {
        super.visit(n, arg);
        System.out.println(<font>"Method: " + n.getName());
    }
}, null);

這將為原始檔中的每個方法名稱輸出一條日誌訊息,無論它們位於何處。事實上,這會在整個樹結構上遞迴,這意味著這些方法可以位於頂級類、內部類、甚至其他方法中的匿名類中。

另一種選擇是GenericVisitor<R, A>。它的工作原理與VoidVisitor 類似,只是它的Visit()方法有一個返回值。我們這裡還有介面卡類,具體取決於我們想要如何從每個方法收集返回值。例如,GenericListVisitorAdaptor將強制我們將每個方法的返回型別改為List<R>並將所有這些列表合併在一起:

List<String> allMethods = compilationUnit.accept(new GenericListVisitorAdapter<String, Object>() {
    @Override
    public List<String> visit(MethodDeclaration n, Object arg) {
        List<String> result = super.visit(n, arg);
        result.add(n.getName().asString());
        return result;
    }
}, null);

這將返回一個列表,其中包含整個樹中每個方法的名稱。

6. 輸出解析後的程式碼
除了解析和分析我們的程式碼之外,我們還可以將其再次以字串的形式輸出。這在很多方面都很有用——例如,如果我們只想提取和輸出程式碼的特定部分。

實現此目的的最簡單方法是使用標準toString()方法。我們所有的 AST 型別都正確實現了這一點,並將生成格式化程式碼。請注意,這可能與我們解析程式碼時的格式不完全相同,但它仍然遵循相對標準的約定。

例如,如果我們解析以下程式碼:

package com.baeldung.javaparser;
import java.util.List;
class TestClass {
private List<String> doSomething()  {}
private class Inner {
private String other() {}
}
}

當我們格式化它時,我們將得到以下輸出:

package com.baeldung.javaparser;
import java.util.List;
class TestClass {
    private List<String> doSomething() {
    }
    private class Inner {
        private String other() {
        }
    }
}

我們可以用於格式化程式碼的另一種方法是使用DefaultPrettyPrinterVisitor。這是一個將處理格式化的標準訪問者類。這為我們提供了配置輸出格式化方式的某些方面的優勢。例如,如果我們想縮排兩個空格而不是四個空格,我們可以這樣寫:

DefaultPrinterConfiguration printerConfiguration = new DefaultPrinterConfiguration();
printerConfiguration.addOption(new DefaultConfigurationOption(DefaultPrinterConfiguration.ConfigOption.INDENTATION,
    new Indentation(Indentation.IndentType.SPACES, 2)));
DefaultPrettyPrinterVisitor visitor = new DefaultPrettyPrinterVisitor(printerConfiguration);
compilationUnit.accept(visitor, null);
String formatted = visitor.toString();

7. 操作解析的程式碼
一旦我們將一些程式碼解析為 AST,我們就可以對其進行更改。由於這現在只是一個 Java 物件模型,因此我們可以將其視為任何其他物件模型,並且 JavaParser 使我們能夠自由更改它的大部分方面。

將其與將 AST 作為工作原始碼輸出的能力相結合,意味著我們可以操作解析後的程式碼,對其進行更改,並以某種形式提供輸出。這對於 IDE 外掛、程式碼編譯步驟等非常有用。

只要我們能夠訪問適當的 AST 元素,就可以以任何方式使用它——無論是直接訪問它們、使用訪問者迭代還是任何有意義的方式。

例如,如果我們想將一段程式碼中的每個方法名稱都大寫,那麼我們可以這樣做:

compilationUnit.accept(new VoidVisitorAdapter<Object>() {
    @Override
    public void visit(MethodDeclaration n, Object arg) {
        super.visit(n, arg);
        
        String oldName = n.getName().asString();
        n.setName(oldName.toUpperCase());
    }
}, null);

這使用一個簡單的訪問者來訪問源樹中的每個方法宣告,並使用setName()方法為每個方法指定一個新名稱。新名稱就是舊名稱的大寫形式。

完成此操作後,AST 就會就地更新。然後我們可以按照自己的意願對其進行格式化,新格式化的程式碼將反映我們的更改。