1. 引言
本文将深入探讨JavaCompiler API的使用。我们将了解这个API的用途、它能做什么,以及如何利用它来提取源文件中定义的方法细节。
2. JavaCompiler API详解
Java 6引入了ToolProvider机制,让我们能访问各种内置JVM工具。其中就包括JavaCompiler。 这与javac应用程序功能相同,但提供了编程方式访问。
通过它我们可以编译Java源代码,同时还能在编译过程中提取代码信息。
要获取JavaCompiler实例,需使用ToolProvider:
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
⚠️ 注意:JavaCompiler不一定总是可用,这取决于具体JVM实现及其提供的工具。
但要注意,解析Java代码而非简单编译是依赖于具体实现的。本文假设使用Oracle编译器,且tools.jar文件在类路径中可用。 自Java 9起,该文件默认不再提供,需确保有合适版本可用。
3. 处理Java代码
获取JavaCompiler实例后,就可以处理Java代码了。这需要合适的JavaFileManager实例和JavaFileObject集合。 具体实现取决于要处理的代码来源。
如果要处理磁盘上的文件,可以依赖JVM工具。特别是JavaCompiler提供的StandardJavaFileManager就是为此设计的:
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8);
获取后就能用它访问要处理的文件:
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(Arrays.asList(new File(filename)));
如果需要处理其他来源的代码(如内存中的字符串),可以使用其他实现。
准备好这些后,就可以处理文件了:
JavacTask javacTask =
(JavacTask) compiler.getTask(null, fileManager, null, null, null, compilationUnits);
Iterable<? extends CompilationUnitTree> compilationUnitTrees = javacTask.parse();
注意我们将compiler.getTask()的结果强制转换为JavacTask实例。这个类位于tools.jar中,是解析Java源代码的入口点。 然后用它将输入文件解析为CompilationUnitTree集合,每个元素代表我们提供给编译器的一个文件。
4. 编译单元细节
至此,我们已经获取了编译单元(即已处理的源文件)的解析信息。
首先可以获取顶层信息:
- 使用
getPackageName()
获取包名 - 使用
getImports()
获取导入列表 - 使用
getTypeDecls()
获取所有顶层声明列表(通常是类定义,也可能是Java语言支持的任何类型)
这里返回的所有内容都是Tree接口的实现。整个编译单元表示为树形结构,允许适当嵌套。例如,类定义可以嵌套在方法内,而该方法又嵌套在其他类中。
Tree结构实现了访问者模式,这让我们无需提前知道具体类型就能处理结构中的任何实例。这很有用,因为getTypeDecls()
返回的是任意Tree类型的集合:
for (Tree tree : compilationUnitTree.getTypeDecls()) {
tree.accept(new SimpleTreeVisitor() {
@Override
public Object visitClass(ClassTree classTree, Object o) {
System.out.println("Found class: " + classTree.getSimpleName());
return null;
}
}, null);
}
也可以直接查询Tree实例的类型。所有Tree实例都有getKind()
方法,返回Kind枚举中的相应值。例如类定义会返回Kind.CLASS
:
for (Tree tree : compilationUnitTree.getTypeDecls()) {
if (tree.getKind() == Tree.Kind.CLASS) {
ClassTree classTree = (ClassTree) tree;
System.out.println("Found class: " + classTree.getSimpleName());
}
}
5. 类细节
获取ClassTree实例后(无论通过哪种方式),就可以开始查询类定义的详细信息。 这包括类名、父类、接口列表等类级别信息。
还可以使用getMembers()
获取类成员信息。这包括任何可作为类成员的内容:方法、字段、嵌套类等。 所有能直接写在类体中的内容都会被返回。
这与前面CompilationUnitTree.getTypeDecls()
类似,返回的是混合类型集合。因此需要同样处理:使用访问者模式或getKind()
方法。
例如提取类中的所有方法:
for (Tree member : classTree.getMembers()) {
member.accept(new SimpleTreeVisitor(){
@Override
public Object visitMethod(MethodTree methodTree, Object o) {
System.out.println("Found method: " + methodTree.getName());
return null;
}
}, null);
}
6. 方法细节
如果需要,可以查询MethodTree实例获取更多方法本身的信息。 如预期那样,我们可以获取方法签名的所有细节:方法名、参数、返回类型、throws子句,还包括泛型类型参数、修饰符,甚至注解类方法的默认值。
所有返回内容都是Tree或其子类。例如方法参数总是VariableTree实例,因为这是该位置唯一合法的类型。可以像处理源文件其他部分一样处理它们。
例如打印方法的某些细节:
System.out.println("Found method: " + classTree.getSimpleName() + "." + methodTree.getName());
System.out.println("Return value: " + methodTree.getReturnType());
System.out.println("Parameters: " + methodTree.getParameters());
输出类似:
Found method: ExtractJavaLiveTest.visitClassMethods
Return value: void
Parameters: ClassTree classTree
7. 方法体
我们可以更深入:MethodTree实例提供了方法体的解析表示(语句集合)。
在API的这个部分,"万物皆Tree"的特性优势尽显。Java中有各种特殊语句,某些语句甚至可以包含其他语句。
例如以下Java代码是单个语句:
for (Tree statement : methodTree.getBody().getStatements()) {
System.out.println("Found statement: " + statement);
}
这是一个"增强for循环",包含:
- 变量声明(Tree statement)
- 表达式(methodTree.getBody().getStatements())
- 嵌套语句(包含System.out.println的代码块)
JavaCompiler将其表示为EnhancedForLoopTree实例,提供这些不同细节的访问。Java中每种可用的语句类型都有对应的StatementTree子类,让我们能提取相关细节。
8. 前向兼容性
Java非常重视向后兼容,但前向兼容性管理较弱。这意味着可能遇到使用未知语法的Java代码。例如Java 5引入了增强for循环,如果处理更早版本的代码就会遇到意外。
✅ 简单粗暴的解决方案:准备好处理未预期的Tree实例。根据具体需求,这可能是严重问题,也可能无关紧要。但通常,如果要解析比预期更新的Java代码,应该做好失败准备。
9. 总结
我们学习了如何使用JavaCompiler API解析Java源代码并提取信息。特别是了解了如何从源文件深入到构成方法体的各个语句。
这个API还能做更多事情,不妨亲自尝试一些功能?
本文所有代码可在GitHub上找到。