JavaParser使用指南

JavaParser 是一个功能强大的 Java 库,用于解析、分析和修改 Java 源代码。它通过将代码转换为抽象语法树(AST)来进行操作,广泛应用于自动化重构、代码生成和静态分析等场景。

下面将为你详细介绍它的核心用法。

💡 JavaParser 能做什么?

JavaParser 主要有三大应用场景:

  • 分析 (Analyse):遍历 Java 源码,查找你感兴趣的特定模式或结构,例如找出所有公共方法或未使用的变量。
  • 转换 (Transform):在识别出代码模式后,对代码进行修改,例如重命名方法、修改逻辑或 添加注解。
  • 生成 (Generate):以编程方式动态创建新的 Java 代码,例如生成实体类、接口或方法,避免重复的手动编写。

🚀 快速开始:引入依赖

首先,你需要在项目中引入 JavaParser 的核心库。在 pom.xml 中添加:

xml 复制代码
<dependency>
    <groupId>com.github.javaparser</groupId>
    <artifactId>javaparser-core</artifactId>
    <version>3.28.0</version>
    <scope>compile</scope>
</dependency>

🔧 核心操作指南

1. 分析代码

这是最基础的操作,目的是将源代码读入并转换为可分析的 AST 结构。

解析源代码

使用 StaticJavaParser 类可以方便地解析各种形式的 Java 代码。

java 复制代码
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;

import java.io.File;
import java.io.FileNotFoundException;

public class AnalyzeCode {
    public static void main(String[] args) throws FileNotFoundException {
        // 解析一个Java文件
        File file = new File("path/to/YourClass.java");
        CompilationUnit cu = StaticJavaParser.parse(file);

        System.out.println("解析完成");
    }
}

遍历与查找

得到 CompilationUnit(AST根节点)后,你可以查找特定的代码元素。

按类型查找:使用 findAll 方法获取所有指定类型的节点,例如所有方法声明。

java 复制代码
// 获取并打印所有方法的名称
cu.findAll(MethodDeclaration.class).forEach(method ->
    System.out.println("找到方法: " + method.getNameAsString())
);

使用访问者模式:对于更复杂的遍历逻辑,可以实现 VoidVisitorAdapter 来访问 AST 中的每个节点。

java 复制代码
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import com.github.javaparser.ast.body.MethodDeclaration;

// 自定义访问者,专门处理MethodDeclaration节点
public class MethodNamePrinter extends VoidVisitorAdapter<Void> {
    @Override
    public void visit(MethodDeclaration md, Void arg) {
        super.visit(md, arg); // 确保继续遍历子节点
        System.out.println("方法名: " + md.getNameAsString());
    }
}

// 在main方法中使用
// new MethodNamePrinter().visit(cu, null);

2. 修改代码

在 AST 的基础上,你可以直接修改节点属性,从而改变代码结构。

重命名方法

以下示例将所有方法名改为大写。

java 复制代码
cu.accept(new VoidVisitorAdapter<Void>() {
    @Override
    public void visit(MethodDeclaration n, Void arg) {
        super.visit(n, arg);
        String oldName = n.getNameAsString();
        n.setName(oldName.toUpperCase()); // 直接修改名称
    }
}, null);

修改或添加内容

也可以修改更复杂的结构,例如替换方法体或添加新语句。

java 复制代码
// 假设想找到变量名为 "oldName" 的声明并修改它
cu.findAll(VariableDeclarator.class).forEach(vd -> {
    if (vd.getNameAsString().equals("oldName")) {
        // 注意:直接修改名称可能不够,还需要处理使用到它的地方
        vd.setName("newName"); 
    }
});

3. 生成代码

你甚至可以不用解析现有文件,直接从零开始构建一个新的 CompilationUnit 来生成代码。

java 复制代码
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.stmt.BlockStmt;

public class GenerateCode {
    public static void main(String[] args) {
        CompilationUnit cu = new CompilationUnit();

        // 创建一个公共类 "MyGeneratedClass"
        ClassOrInterfaceDeclaration myClass = 
        cu.addClass("MyGeneratedClass").setPublic(true);

        // 添加一个私有字段: private String name;
        myClass.addField(String.class, "name", 
        com.github.javaparser.ast.Modifier.Keyword.PRIVATE);

        // 创建一个公共方法: public void sayHello() { System.out.println("Hello!"); }
        MethodDeclaration method = myClass.addMethod("sayHello", 
        com.github.javaparser.ast.Modifier.Keyword.PUBLIC);
        BlockStmt body = new BlockStmt();

        body.addStatement(StaticJavaParser.parseStatement("System.out.println(\"Hello!\");"));
        method.setBody(body);

        // 打印生成的代码
        System.out.println(cu.toString());
    }
}

4. 输出代码

对 AST 的任何修改,最终都可以通过调用 toString() 方法输出为格式化后的源码字符串。你也可以使用 DefaultPrettyPrinterVisitor 自定义输出格式,比如缩进空格数。

java 复制代码
// 输出修改后的代码
String modifiedCode = cu.toString();
System.out.println(modifiedCode);

5. 实践

需求:检查代码中的中文字符串是否已经被 I18nUtil 替换,并忽略log日志

java 复制代码
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.expr.VariableDeclarationExpr;
import com.github.javaparser.ast.stmt.BlockStmt;
import com.github.javaparser.ast.stmt.Statement;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.io.FileNotFoundException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class JavaParserTest {

    private static final List<String> strings = new ArrayList<>();

    public static void main(String[] args) {
        // 目录或文件路径
        File dir = new File("path/to/YourClass.java");
        // 获取所有导入的包
        // cu.getImports().forEach(System.out::println);

        if (dir.isDirectory()) {
            traverseDirectory(dir);
        } else {
            checkFile(dir);
        }

        if (strings.isEmpty()) {
            System.out.println("改造完成");
        } else {
            strings.forEach(System.out::println);
        }
    }

    /**
     * 检查单个文件
     * @param file Java 源文件
     */
    public static void checkFile(File file) {
        try {
            CompilationUnit cu = StaticJavaParser.parse(file);
            // 方式1
            cu.accept(new MethodVisitor(), file.toPath());
            // 方式2
            // new MethodVisitor().visit(cu, file.toPath());
        } catch (FileNotFoundException e) {
            System.err.println("文件不存在或无法读取: " + file.getAbsolutePath());
        }
    }

    /**
     * 递归遍历目录,处理所有 Java 文件
     * @param directory 目录
     */
    private static void traverseDirectory(File directory) {
        File[] files = directory.listFiles();
        if (files == null) {
            return;
        }
        for (File file : files) {
            if (file.isDirectory()) {
                traverseDirectory(file);
            }
            if (file.isFile() && file.getName().endsWith(".java")) {
                checkFile(file);
            }
        }
    }

    // 自定义访问者,专注于处理方法声明
    static class MethodVisitor extends VoidVisitorAdapter<Path> {
        @Override
        public void visit(MethodDeclaration method, Path filePath) {
            super.visit(method, filePath);
            // 在这里可以进一步处理方法体
            method.getBody().ifPresent(body -> {
                //查找所有字符串字面量
                body.findAll(StringLiteralExpr.class).forEach(str -> {
                    String value = str.getValue();
                    if (containsChinese(value)) {
                        if (!isI18nUtilGetArgument(str)) {
                            String[] split = filePath.toString().split("/");
                            String className = split[split.length - 1];
                            // 输出警告
                            String s = String.format("[警告] 文件: %s, 方法: %s, 行号: %d, 字符串: \"%s\"%n",
                                    className, method.getNameAsString(),
                                    str.getBegin().map(p -> p.line).orElse(-1),
                                    value);
                            strings.add(s);
                        }
                    }
                });
            });
        }
    }

    /**
     * 检查字符串是否包含中文字符
     */
    private static boolean containsChinese(String s) {
        for (char c : s.toCharArray()) {
            if (c >= '\u4e00' && c <= '\u9fff') {
                return true;
            }
        }
        return false;
    }

    /**
     * 判断一个字符串字面量节点是否作为 I18nUtil方法的参数
     */
    private static boolean isI18nUtilGetArgument(StringLiteralExpr strLiteral) {
        Node parent = strLiteral.getParentNode().orElse(null);
        // 寻找最近的 MethodCallExpr 祖先节点
        while (parent != null && !(parent instanceof MethodCallExpr)) {
            parent = parent.getParentNode().orElse(null);
        }
        if (parent instanceof MethodCallExpr) {
            MethodCallExpr call = (MethodCallExpr) parent;
            // 检查调用者是否为 "I18nUtil" (可以是标识符或静态引用)
            Optional<Expression> scope = call.getScope();
            if (scope.isPresent()) {
                Expression scopeExpr = scope.get();
                // 忽略log日志
                if (StringUtils.equals(scopeExpr.toString(), "log")) {
                    return true;
                }
                // 判断 I18nUtil
                if (StringUtils.equals(scopeExpr.toString(), "I18nUtil")) {
                    // 确认该字符串字面量是调用的第一个参数(或者某个参数,通常 key 是第一个)
                    // 这里简单判断是否在参数列表中
                    return call.getArguments().stream()
                            .anyMatch(arg -> arg == strLiteral);
                }
            }
        }
        return false;
    }

    /**
     * parseBlock 解析代码块
     * 注意:解析的代码块必须包含花括号,否则会报错
     */
    private static void parseBlock() {
        // 注意最外层添加了 { 和 },内部语句保持原样
        String methodBody =
                "{\n"
                        + " int a = 10;\n" +
                        " String b = \"hello\";\n" +
                        " System.out.println(a + b);\n" +
                        " // 使用 lambda 表达式\n" +
                        " List<String> list = Arrays.asList(\"a\", \"b\", \"c\");\n" +
                        " list.forEach(item -> System.out.println(item));\n" +
                        " if (a > 5) {\n" +
                        " System.out.println(\"a is greater than 5\");\n" +
                        " }\n"
                        + "}";

        try {
            // 使用 parseBlock 解析代码块
            BlockStmt block = StaticJavaParser.parseBlock(methodBody);

            System.out.println("成功解析代码块!共包含 " + block.getStatements().size() + " 条语句。\n");

            // 遍历并分析每条语句
            for (Statement stmt : block.getStatements()) {
                System.out.println("语句类型: " + stmt.getClass().getSimpleName());
                System.out.println("语句内容: " + stmt);
                System.out.println("---");
            }

            // 示例:查找并修改变量声明
            block.findAll(VariableDeclarationExpr.class).forEach(varDecl -> {
                varDecl.getVariables().forEach(variable -> {
                    if (variable.getNameAsString().equals("a")) {
                        variable.setName("number");
                        System.out.println("已将变量 'a' 重命名为 'number'");
                    }
                });
            });

            // 输出修改后的代码
            System.out.println("\n修改后的代码块:");
            System.out.println(block);
        } catch (Exception e) {
            System.err.println("解析失败: " + e.getMessage());
        }
    }
}