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