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上找到。


原始标题:Parse Java Source Code and Extract Methods