JavaParser简介

754 阅读6分钟

翻译自:www.baeldung.com/javaparser

1、javaParser 是什么

JavaParser它允许我们将Java源代码解析为抽象语法树(AST)。完成这些之后,我们就可以分析解析后的代码,对其进行操作,甚至编写新代码并输出。

GitHub地址:github.com/javaparser/…

2、maven依赖

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

3、解析源文件

3.1、解析源代码

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

3.2、解析语句

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

3.3、解析其他结构

JavaParser还可以解析许多其他结构,覆盖到Java 18之前的整个Java语言。每个构造都有一个单独的专用解析方法,并返回表示已解析代码的适当类型。例如,我们可以使用parseAnnotation()来解析注释,parseImport()来解析导入语句,parseBlock()来解析语句块,等等。为了选择正确的解析方法,我们确实需要确切地知道要解析的代码类型。例如,使用parseStatement()方法解析类定义将会失败

4、分析解析代码

4.1、访问已解析元素

一旦我们解析了一些源代码,我们就可以查询AST来访问各个元素。具体如何做取决于我们想要访问的元素和我们解析的内容。

例如,如果我们已经将源文件解析为CompilationUnit,那么我们可以使用getClassByName()访问我们期望出现的类:

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

在每个阶段,我们只能直接访问我们当前处理的最外层的元素。例如,如果我们从解析源文件中得到一个CompilationUnit,那么我们可以访问包声明、导入语句和顶级类型,但是我们不能访问这些类型中的成员。但是,一旦访问了其中一个类型,就可以访问其中的成员。

4.2、 迭代解析元素

在某些情况下,我们可能不知道解析代码中出现了哪些元素,或者我们只是想处理所有特定类型的元素,而不仅仅是一个元素。

我们的每个AST类型都可以访问整个合适的嵌套元素范围。具体如何运作取决于我们想要处理什么。例如,我们可以使用以下命令从CompilationUnit中提取所有的import语句:

NodeList<ImportDeclaration> imports = compilationUnit.getImports();

4.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("Method: " + n.getName());
    }
}, null);

这将为源文件中的每个方法名输出一条日志消息,而不管它们在哪里。这在整个树结构上递归的事实意味着这些方法可以位于顶级类、内部类,甚至是其他方法中的匿名类中。

另一种选择是GenericVisitor<R, A> 它的工作原理类似于VoidVisitor,只不过它的visit()方法有一个返回值。这里我们还有适配器类,这取决于我们希望如何从每个方法收集返回值。例如,GenericListVisitorAdaptor将强制我们让每个方法的返回类型为List而是将所有这些列表合并在一起:

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);

这将返回一个列表,其中包含整个树中每个方法的名称。

5、输出解析代码

除了解析和分析代码之外,我们还可以再次将其作为字符串输出。这在很多情况下都很有用——例如,如果我们只想提取和输出代码的特定部分。

实现这一点的最简单方法是使用标准的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();

6、操作已解析代码

一旦我们将一些代码解析为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将被就地更新。然后,我们可以按照自己的意愿对其进行格式化,新格式化的代码将反映我们的更改。