1. Introduction
1.导言
In this article, we’re going to investigate the JavaCompiler API. We’ll see what this API is, what we can do with it, and how to use it to extract the details of methods defined in our source files.
在本文中,我们将研究 JavaCompiler API。我们将了解该 API 是什么,我们可以用它做什么,以及如何使用它来提取源文件中定义的方法的详细信息。
2. The JavaCompiler API
2. JavaCompiler API
Java 6 introduced the ToolProvider mechanism, which gives us access to various built-in JVM tools. Amongst other things, this includes the JavaCompiler. This is the same functionality as in the javac application, which is only programmatically available.
Java 6 引入了 ToolProvider 机制,该机制可让我们访问各种内置 JVM 工具。 这与 javac 应用程序中的功能相同,后者仅通过编程实现。
Using this, we can compile Java source code. However, we can also extract information from the code as part of the compilation process.
利用这一点,我们可以编译 Java 源代码。不过,作为编译过程的一部分,我们还可以从代码中提取信息。
To get access to the JavaCompiler, we need to use the ToolProvider, which will give us an instance if available:
要访问JavaCompiler,我们需要使用ToolProvider,如果可用,它将为我们提供一个实例:
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
Note that there’s no guarantee that the JavaCompiler will be available. It depends on the JVM being used and what tooling it makes available.
请注意,并不能保证 JavaCompiler 一定可用。这取决于所使用的 JVM 及其提供的工具。
However, interrogating the Java code instead of simply compiling it is implementation-dependent. In this article, we’re assuming the use of the Oracle compiler and that the tools.jar file is available on the classpath. Note that since Java 9, this file is no longer available by default, so we need to make sure an appropriate version is available for use.
不过,询问 Java 代码而不是简单地编译 Java 代码与实现有关。在本文中,我们假设使用 Oracle 编译器,并且 tools.jar 文件在类路径上可用。请注意,自 Java 9 起,该文件不再默认可用,因此我们需要确保有适当的版本可供使用。
3. Processing Java Code
3.处理 Java 代码
Once a JavaCompiler instance is available, we can process some Java code. We need an appropriate JavaFileManager instance and an appropriate collection of JavaFileObject instances to do this. Exactly how we do both of these things depends on the source of the code that we wish to process.
一旦 JavaCompiler 实例可用,我们就可以处理一些 Java 代码。我们需要一个适当的 JavaFileManager 实例和一个适当的 JavaFileObject 实例集合来完成这项工作。具体如何完成这两项工作取决于我们希望处理的代码源。
If we want to process code that exists as files on disk, we can rely on the JVM tooling. In particular, the StandardJavaFileManager that the JavaCompiler instance provides access to is intended precisely for this purpose:
如果我们要处理以文件形式存在于磁盘上的代码,我们可以依靠 JVM 工具。特别是,JavaCompiler 实例提供访问的 StandardJavaFileManager 正是用于此目的:
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8);
Once we’ve got this, we can then use it to access the files that we want to process:
有了这个,我们就可以用它来访问要处理的文件:
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(Arrays.asList(new File(filename)));
We can use other instances of these if we need to. For example, if we want to process code held in local variables,
如果需要,我们还可以使用其他实例。例如,如果我们要处理局部变量中的代码、
Once we have these, we can then process our files:
有了这些,我们就可以处理文件了:
JavacTask javacTask =
(JavacTask) compiler.getTask(null, fileManager, null, null, null, compilationUnits);
Iterable<? extends CompilationUnitTree> compilationUnitTrees = javacTask.parse();
Note that we’re casting the compiler’s result.getTask() into a JavacTask instance. This class exists in the tools.jar file and is the entry point to interrogating the processed Java source. We then use this to parse our input files into a collection of CompilationUnitTree types. Each of these represents on file that we provided to the compiler.
请注意,我们将编译器的结果.getTask()转换为一个 JavacTask 实例。该类存在于 tools.jar 文件中,是查询已处理 Java 源代码的入口点。然后,我们使用该类将输入文件解析为 CompilationUnitTree 类型的集合。其中每个类型都代表我们提供给编译器的文件。
4. Compilation Unit Details
4.汇编单位详情
Once we’ve got this far, we have the parsed details of the compilation unit – that is, the source files that we’ve processed – available to us.
一旦我们走到这一步,我们就可以获得编译单元的解析细节,也就是我们处理过的源文件。
The first thing that we can do is to interrogate the top-level details. For example, we can see what package it represents using getPackageName() and get the list of imports using getImports(). We can also use getTypeDecls() to get the list of all top-level declarations – which typically means the class definitions but could be anything the Java language supports.
我们可以做的第一件事是查询顶层细节。例如,我们可以使用 getPackageName() 查看它所代表的软件包,并使用 getImports() 获取导入列表。我们还可以使用 getTypeDecls() 获取所有顶层声明的列表–这通常是指类定义,但也可以是 Java 语言支持的任何内容。
We’ll notice here that everything returned is an implementation of the Tree interface. The entire compilation unit is represented as a tree structure, allowing things to be appropriately nested. For example, it’s legal to have class definitions nested inside methods where the method is already nested inside another class.
我们会注意到,这里返回的所有内容都是 Tree 接口的实现。整个编译单元以树形结构表示,允许适当嵌套。例如,类定义嵌套在方法中是合法的,而方法已经嵌套在另一个类中。
One advantage this gives us is that the Tree structure implements the visitor pattern. This allows us to have code that can interrogate any instance of the structure without knowing ahead of time what it is.
这样做的一个好处是,树结构实现了访问者模式。这样,我们就可以编写代码,询问结构的任何实例,而无需提前知道它是什么。
This is very useful since getTypeDecls() returns a collection of arbitrary Tree types, so we don’t know at this point what we’re dealing with:
这一点非常有用,因为 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);
}
We can also determine the type of our Tree instances by querying it directly. All of our Tree instances have a getKind() method that returns an appropriate value from the Kind enumeration. For example, class definitions will return Kind.CLASS to indicate that that’s what they are.
我们还可以通过直接查询来确定 Tree 实例的类型。我们的所有 Tree 实例都有一个 getKind() 方法,该方法会从 Kind 枚举中返回一个适当的值。例如,类定义将返回 Kind.CLASS 以表明它们是类。
We can then use this and cast the value ourselves if we don’t want to use the visitor pattern:
如果不想使用访问者模式,我们就可以使用它,然后自己转换值:
for (Tree tree : compilationUnitTree.getTypeDecls()) {
if (tree.getKind() == Tree.Kind.CLASS) {
ClassTree classTree = (ClassTree) tree;
System.out.println("Found class: " + classTree.getSimpleName());
}
}
5. Class Details
5.班级详情
Once we’ve got access to a ClassTree instance – however we manage that – we can start to interrogate this for details about the class definition. This includes class-level details such as the class name, the superclass, the list of interfaces, and so on.
一旦我们访问了ClassTree实例(无论我们如何管理它),我们就可以开始查询有关类定义的详细信息。这包括类级的详细信息,例如类名、超类、接口列表等。
We can also get the class members’ details – using getMembers(). This includes anything that can be a class member, such as methods, fields, nested classes, etc. Anything that you’re allowed to write directly into the body of a class will be returned by this.
我们还可以使用 getMembers() 获取类成员的详细信息。这包括任何可以成为类成员的内容,例如方法、字段、嵌套类等。任何允许直接写入类主体的内容都将通过此方法返回。
This is the same as we saw with CompilationUnitTree.getTypeDecls(), where we can get a mixture of different types. As such, we need to treat it similarly, using the visitor pattern or the getKind() method.
这与我们在 CompilationUnitTree.getTypeDecls() 中看到的情况相同,我们可以得到不同类型的混合物。因此,我们需要使用访问者模式或 getKind() 方法进行类似处理。
For example, we can extract all of the methods out of a class:
例如,我们可以从一个类中提取所有方法:
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. Method Details
6.方法细节
If we wish, we can interrogate the MethodTree instance to get more information about the method itself. As we’d expect, we can get all the details about the method signature. This includes the method name, parameters, return type, and throws clause, but also details like generic type parameters, modifiers, and even – in the case of methods present in annotation classes – the default value.
如果我们愿意,我们可以询问 MethodTree 实例以获取有关方法本身的更多信息。正如我们所期望的,我们可以获取有关方法签名的所有详细信息。这包括方法名称、参数、返回类型和抛出子句,但也包括诸如泛型类型参数、修饰符等详细信息,甚至还包括 注解类中的方法的默认值。
As always, everything we’re given here is a Tree or some subclass. For example, the method parameters are always VariableTree instances because that’s the only legal thing in that position. We can then treat these as any other part of the source file.
和往常一样,这里给我们提供的所有东西都是 Tree 或某个子类。例如,方法参数始终是 VariableTree 实例,因为这是该位置上唯一合法的东西。然后,我们就可以像处理源文件的其他部分一样处理这些参数。
For example, we can print out some of the details of a method:
例如,我们可以打印出一个方法的部分细节:
System.out.println("Found method: " + classTree.getSimpleName() + "." + methodTree.getName());
System.out.println("Return value: " + methodTree.getReturnType());
System.out.println("Parameters: " + methodTree.getParameters());
Which will produce output such as:
其输出结果如下
Found method: ExtractJavaLiveTest.visitClassMethods
Return value: void
Parameters: ClassTree classTree
7. Method Body
7.方法身体
We can go even further than this, though. The MethodTree instance gives us access to the parsed body of the method as a collection of statements.
不过,我们还可以更进一步。MethodTree 实例允许我们访问作为语句集合的方法解析后的主体。
This, more than anywhere else in the API, is where the fact that everything is a Tree really benefits us. In Java, there are a variety of statements that have special details about them, which can even include some statements containing other statements.
在 API 中,这一点比其他任何地方都要重要,因为所有东西都是一棵树,这一点让我们真正受益匪浅。在 Java 中,各种语句都有其特殊的细节,其中甚至包括一些包含其他语句的语句。
For example, the following Java code is a single statement:
例如,以下 Java 代码是一条单一语句:
for (Tree statement : methodTree.getBody().getStatements()) {
System.out.println("Found statement: " + statement);
}
This statement is an “Enhanced for loop” and consists of:
该语句是”增强 for 循环“,由以下内容组成:
- A variable declaration – Tree statement
- An expression – methodTree.getBody().getStatements()
- A nested statement – The block containing System.out.println(“Found statement: ” + statement);
Our JavaCompiler represents this as an EnhancedForLoopTree instance, which gives us access to these different details. Every different type of statement that can be used in Java is represented by a subclass of StatementTree, allowing us to get the pertinent details back out again.
我们的 JavaCompiler 将其表示为 EnhancedForLoopTree 实例,从而使我们能够访问这些不同的详细信息。Java 中可以使用的每种不同类型的语句都由 StatementTree 的子类表示,这样我们就可以再次获得相关的详细信息。
8. Future Proofing
8.面向未来
Java pays a lot of attention to backward compatibility. However, forward compatibility is less well managed. This means it’s possible to have Java code that uses syntax our program doesn’t expect. For example, Java 5 introduced the enhanced for loop. We’d be surprised to see one of these if we were expecting code older than that.
Java 非常重视向后兼容性。但是,前向兼容性的管理却不那么完善。这意味着 Java 代码有可能会使用我们的程序所不期望的语法。例如,Java 5 引入了增强型 for 循环。如果我们期待的是比它更早的代码,那么看到这样的代码会让我们大吃一惊。
However, all this means is that we must be prepared for Tree instances that we might not expect. Depending on exactly what we’re doing, this might be a serious concern, or it might not even be an issue. In general, though, we should be prepared to fail if we’re trying to parse Java code from a version newer than we’re expecting.
然而,这意味着我们必须为我们可能意想不到的树情况做好准备。根据我们正在做的事情,这可能是一个严重的问题,也可能根本不是问题。但总的来说,如果我们试图解析的 Java 代码版本比我们预期的要新,我们就应该做好失败的准备。
9. Conclusion
9.结论
We’ve seen how to use the JavaCompiler API to parse some Java source code and get information from it. In particular, we’ve seen how to get from the source file to the individual statements that make up method bodies.
我们已经了解了如何使用 JavaCompiler API 来解析一些 Java 源代码并从中获取信息。特别是,我们了解了如何从源文件获取构成方法体的单个语句。
You can do much more with this API, so why not try some of it out yourself?
使用该应用程序接口,您还可以做更多的事情,为什么不亲自尝试一下呢?
As always, all of the code from this article can be found over on GitHub.
与往常一样,本文的所有代码都可以在 GitHub 上找到。