您可曾想过像 Checkstyle 或 FindBugs 这样的工具如何执行静态代码分析吗,或者像 NetBeans 或 Eclipse 这样的集成开发环境(Integrated Development Environments IDE)如何执行快速代码修复或查找在代码中声明的字段的完全引用吗?在许多情况下,IDE 具有自己的 API 来解析源码并生成标准树结构,称为 抽象语法树(Abstract Syntax Tree AST) 或“解析树”,此树可用于对源码元素的进一步分析。好消息是,借助于在 Java 中作为 Java Standard Edition 6 发行版的一部分引入的三个新 API,现在可以实现上述任务以及其他更多任务。可能与需要执行源码分析的 Java 应用程序开发人员相关的 API 有 Java Compiler API (JSR 199)、可插入注解处理(Pluggable Annotation Processing)API (JSR 269) 和 Compiler Tree API。
在本文中,我们探讨了其中每个 API 的功能,并继续开发一个简单的演示应用程序,来在作为输入提供的一套源码文件上验证特定的 Java 编码规则。此实用程序还显示了编码违规消息以及作为输出的违规源码的位置。考虑一个简单的 Java 类,它覆盖 Object 类的 equals() 方法。要验证的编码规则是实现 equals() 方法的每个类也应该覆盖具有合适签名的 hashcode() 方法。您可以看到下面的 TestClass 类没有定义 hashcode() 方法,即使它具有 equals() 方法。
public class TestClass implements Serializable {
int num;
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if ((obj == null) || (obj.getClass() != this.getClass()))
return false;
TestClass test = (TestClass) obj;
return num == test.num;
}
}
让我们继续借助这三个 API 将此类作为构建过程的一部分进行分析。
从代码中调用编译器:Java Compiler API
我们全部使用 javac 命令行工具来将 Java 源文件编译为类文件。那么我们为什么需要 API 来编译 Java 文件呢?好的,答案极其简单:正如名称所示,这个新的标准 API 告诉我们从自己的 Java 应用程序中调用编译器;比如,可以通过编程方式与编译器交互,从而进行应用程序级别服务的编译部分。此 API 的一些典型使用如下。
Compiler API 帮助应用服务器最小化部署应用程序的时间,例如,避免了使用外部编译器来编译从 JSP 页面中生成的 servlet 源码的开销
IDE 等开发人员工具和代码分析器可以从编辑器或构建工具中调用编译器,从而显著降低编译时间。
Java Compiler 类包装在 javax.tools 包中。此包的 ToolProvider 类提供了一个名为 getSystemJavaCompiler() 的方法,此方法返回某个实现了 JavaCompiler 接口的类的实例。此编译器实例可用于创建一个将执行实际编译的编译任务。然后,要编译的 Java 源文件将传递给此编译任务。为此,编译器 API 提供了一个名为 JavaFileManager 的文件管理器抽象,它允许从各种来源中检索 Java 文件,比如从文件系统、数据库、内存等。在此示例中,我们使用 StandardFileManager,一个基于 java.io.File 的文件管理器。此标准文件管理器可以通过调用 JavaCompiler 的 getStandardFileManager() 方法来获得。上述步骤的代码段如下所示:
//Get an instance of java compiler
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//Get a new instance of the standard file manager implementation
StandardJavaFileManager fileManager = compiler.
getStandardFileManager(null, null, null);
// Get the list of java file objects, in this case we have only
// one file, TestClass.java
Iterable<? extends JavaFileObject> compilationUnits1 =
fileManager.getJavaFileObjectsFromFiles("TestClass.java");
诊断监听器可以传递给 getStandardFileManager() 方法来生成任何非致命问题的诊断报告。在此代码段中,我们传递 null 值,因为我们准备从此工具中收集诊断。有关传递给这些方法的其他参数的详细信息,请参阅 Java 6 API。StandardJavaFileManager 的 getJavaFileObjectsfromFiles() 方法返回与所提供的 Java 源文件相java JavaFileObject 实例。
下一步是创建 Java 编译任务,这可以使用 JavaCompiler 的 getTask() 方法来获得。这时,编译任务尚未启动。此任务可以通过调用 CompilationTask 的 call() 方法来触发。创建和触发编译任务的代码段如下所示。
// Create the compilation task
CompilationTask task = compiler.getTask(null, fileManager, null,
null, null, compilationUnits1);
// Perform the compilation task.
task.call();
假设没有任何编译错误,这将在目标目录中生成 TestClass.class 文件。