原文:http://today.java.net/pub/a/today/2008/04/10/source-code-analysis-using-java-6-compiler-apis.html
静态代码分析工具Checkstyle, FindBugs,以及IDE如NetBeans, Eclipse能快速进行代码关联,它们使用了API解析代码,生成AST,深入分析代码元素。JAVA 6 提供了3种新API来完成这样的任务:
http://www.jcp.org/en/jsr/detail?id=199">Java Compiler API(JSR 199),
http://www.jcp.org/en/jsr/detail?id=269">Pluggable AnnotationProcessing API (JSR 269)
http://java.sun.com/javase/6/docs/jdk/api/javac/tree/index.html">CompilerTree API.
探索API在代码规则上的应用。考虑下面例子,TestClass类重载了Object类的equals方法,编码规则规定实现equals方法必须也重载hashcode方法进行签名,但TestClass类有equals方法却没有hashcode方法。
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;
}
}
下面使用3种API分析代码。
从代码调用用编译器:Java Compiler API
使用API而不用javac编译,是因为使用API能从我们自己的应用中调用编译器,如在程序中和编译器交互从而提供一些应用层服务。这个API典型使用情形有:
1.编译API减少应用服务器部署应用的时间,如避免用外部编译器编译jsp页面生成的servlet
2.IDEs和代码分析工具能从编辑器调用编译器,减少编译时间。
Java编译器的类在javax.tools里,ToolProvider提供了getSystemJavaCompiler()返回实现了JavaCompiler接口的类的实例。要编译的代码文件被传到编译任务,由实例开始执行这些任务。API提供了文件管理的抽象JavaFileManager,能从文件系统、数据库、内存里恢复java文件。StandardFileManager基于java.io.File,可以从getStandardFileManager()方法获得StandardFileManager实例。示例如下:
//获得Java Compiler的实例
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
//获得实现标准文件管理器的新实例
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
//获得java文件对象的列表,这里我们只有一个类:TestClass.java
Iterable<? extends JavaFileObject> compilationUnits1 = fileManager.getJavaFileObjectsFromFiles("TestClass.java");
诊断监听器可以可选地被传到getStandardFileManager()方法来产生诊断报告。StandardJavaFileManager的
getJavaFileObjectsfromFiles()方法返回所有对应java文件的JavaFileObject实例。
下一步,使用JavaCompiler
的getTask()创建编译任务。此时任务还没开始,而要通过调用CompilationTask的call()方法触发。示例如下:
// 创建编译任务
CompilationTask task = compiler.getTask(null, fileManager, null,null, null, compilationUnits1);
// 执行编译任务.
task.call();
如果没有编译错误,会在目标路径下形成class文件。
注释处理:插接式注释处理API
注释被编译工具或运行环境处理,执行一些有用的任务,像控制应用行为、形成代码等。注释处理器是动态插入编译器分析源代码文件的工具。能包括而不限于处理以下任务:
1.注释可用于形成部署描述文件,如persistence.xml或ejb-jar.xml
2.注释处理器能用元数据信息来形成代码,如形成被正确注释的enterprise bean的Home和Remote接口。
3.注释可被用于核查代码和部署文件的有效性
Java 5 提供Annotation Processing Tool (APT)和依赖的API(com.sun.mirror.*
)来处理注释和构建注释信息模型。缺点是APT不是标准化的,只限定于sun jdk。
Java 6 的Pluggable Annotation Processing框架提供自定义注释处理器的标准化支持。可插接是因为它可以动态插入javac和操作出现在java文件里的注释。框架有两部分,声明和与注释处理器交互的API(javax.annotation.processing),Java语言建模的API(javax.lang.model)
编写自定义注释处理器
自定义注释处理器扩展AbstractProcessor(默认实现Processor
接口)和重载process()方法。
注释处理器有两种注释:@SupportedAnnotationTypes
和@SupportedSourceVersion
,SupportedSourceVersion指明最近支持的版本,@SupportedAnnotationTypes指明注释处理器对哪种注释感兴趣,例如,
@SupportedAnnotationTypes
("javax.persistence.*")
处理器只需要处理JPA(Java Persistence API)注释,如果被指定@SupportedAnnotationTypes("*")
,即使没有表示出注释,也会调用处理器。
@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes("*")
public class CodeAnalyzerProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnvironment) {
for (Element e : roundEnvironment.getRootElements()) {
System.out.println("Element is "+ e.getSimpleName());
// Add code here to analyze each root element
}
return true;
}
}
处理器的调用,依赖于源代码上的注释、处理器配置和处理器处理的注释类型。注释处理过程可以循环发生,如第一次处理源文件的注释,第二次同时考虑到第一次处理时产生的文件。自定义处理器要重载AbstractProcessor的process(),它有两个参数:1.源文件中的类型和注释集合;2.封装了处理循环信息的运行环境。如果处理器声明了它支持的注释类型,process会返回true,而不会调用别的处理器,否则返回false,如果有下一个处理器则调用下一个处理器处理。
注释处理器的插装
我们可以将处理器引入编译过程。
javac提供了-process接受自定义注释处理器的插装。语法如下:
javac -processor demo.codeanalyzer.CodeAnalyzerProcessor TestClass.java
CodeAnalyzerProcessor是处理注释的类,TestClass是被处理的类。工具在classpath里寻找CodeAnalyzerProcessor,所以要将类放到classpath中。
编程中的插装。CompilationTask的setProcessors()方法允许在编译任务中插入多个处理器,这个方法要在call()之前。
CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits1);
// Create a list to hold annotation processors
LinkedList<AbstractProcessor> processors = new LinkedList<AbstractProcessor>();
// Add an annotation processor to the list
processors.add(new CodeAnalyzerProcessor());
// Set the annotation processor to the compiler task
task.setProcessors(processors);
// Perform the compilation task.
task.call();
执行后,处理器会在TestClass.java的编译过程中打印TestClass
访问AST:the Complier Tree API
AST以树节点表现代码,如类是ClassTree,方法是MethodTree,变量是VariableTree,注释是AnnotationTree
API提供TreeVisitor、TreeScanner等对AST的操作。使用TreeVisitor可以深入分析源码内容,它访问所有孩子节点抽象出方法、注释、类等元素的信息。TreeVisitor是以访问者模式来实现的。
API提供了3种TreeVisitor的实现:SimpleTreeVisitor, TreePathScanner
, 和TreeScanner。
例子用TreePathScanner访问java文件,需要调用TreePathScanner的scan()方法,访问特定类型,只需要重载visitXYZ方法,在方法中调用super.visitXYZ访问子孙节点
public class CodeAnalyzerTreeVisitor extends TreePathScanner<Object, Trees> {
@Override
public Object visitClass(ClassTree classTree, Trees trees) {
---- some code ----
return super.visitClass(classTree, trees);
}
@Override
public Object visitMethod(MethodTree methodTree, Trees trees) {
---- some code ----
return super.visitMethod(methodTree, trees);
}
}
ClassTree是类节点,MethodTree是方法节点,Trees提供了树的元素路径。例子中trees只有一个根元素,就是TestClass本身。
CodeAnalyzerTreeVisitor visitor = new CodeAnalyzerTreeVisitor();
@Override
public void init(ProcessingEnvironment pe) {
super.init(pe);
trees = Trees.instance(pe);
}
for (Element e : roundEnvironment.getRootElements()) {
TreePath tp = trees.getPath(e);
// invoke the scanner
visitor.scan(tp, trees);
}
@Override
public Object visitClass(ClassTree classTree, Trees trees) {
//Storing the details of the visiting class into a model
JavaClassInfo clazzInfo = new JavaClassInfo();
// Get the current path of the node
TreePath path = getCurrentPath();
//Get the type element corresponding to the class
TypeElement e = (TypeElement) trees.getElement(path);
//Set qualified class name into model
clazzInfo.setName(e.getQualifiedName().toString());
//Set extending class info
clazzInfo.setNameOfSuperClass(e.getSuperclass().toString());
//Set implementing interface details
for (TypeMirror mirror : e.getInterfaces()) {
clazzInfo.addNameOfInterface(mirror.toString());
}
return super.visitClass(classTree, trees);
}
JavaClassInfo用来存储Java信息的自定义模型。
在代码违反规则时,将违反规则的代码位置提供给用户。使用了Compiler Tree API 的 SourcePositions
public static LocationInfo getLocationInfo(Trees trees, TreePath path, Tree tree) {
LocationInfo locationInfo = new LocationInfo();
SourcePositions sourcePosition = trees.getSourcePositions();
long startPosition = sourcePosition.getStartPosition(path.getCompilationUnit(), tree);
locationInfo.setStartOffset((int) startPosition);
return locationInfo;
}
获得更充足的信息,包括此位置代码所在的类和方法。一种可选方法是在代码文件中搜索字符。
//Get the compilation unit tree from the tree path
CompilationUnitTree compileTree = treePath.getCompilationUnit();
//Get the java source file which is being processed
JavaFileObject file = compileTree.getSourceFile();
// Extract the char content of the file into a string
String javaFile = file.getCharContent(true).toString();
//Convert the java file content to a character buffer
CharBuffer charBuffer = CharBuffer.wrap (javaFile.toCharArray());
LocationInfo clazzNameLoc = (LocationInfo) clazzInfo.getLocationInfo();
int startIndex = clazzNameLoc.getStartOffset();
int endIndex = -1;
if (startIndex >= 0) {
String strToSearch = buffer.subSequence(startIndex,
buffer.length()).toString();
Pattern p = Pattern.compile(clazzName);
Matcher matcher = p.matcher(strToSearch);
matcher.find();
startIndex = matcher.start() + startIndex;
endIndex = startIndex + clazzName.length();
}
clazzNameLoc.setStartOffset(startIndex);
clazzNameLoc.setEndOffset(endIndex);
clazzNameLoc.setLineNumber(compileTree.getLineMap().getLineNumber(startIndex));
LineMap提供了CompilationUnitTree字符位置和行号的映射。可通过CompilationUnitTree的getLineMap()获得行号。
核查代码规则
代码规则被配置在xml文件中,由自定义类RuleEngine管理,不满足规则会返回ErrorDescription
ClassFile clazzInfo = ClassModelMap.getInstance().getClassInfo(className);
for (JavaCodeRule rule : getRules()) {
// apply rules one by one
Collection<ErrorDescription> problems = rule.execute(clazzInfo);
if (problems != null) {
problemsFound.addAll(problems);
}
}
每个规则由一个java类实现,封装了验证逻辑。下面是一个规则的实现,规则规定重载equals方法必须也重载hashcode方法:
public class OverrideEqualsHashCode extends JavaClassRule {
@Override
protected Collection<ErrorDescription> apply(ClassFile clazzInfo) {
boolean hasEquals = false;
boolean hasHashCode = false;
Location errorLoc = null;
for (Method method : clazzInfo.getMethods()) {
String methodName = method.getName();
ArrayList paramList = (ArrayList) method.getParameters();
if ("equals".equals(methodName) && paramList.size() == 1) {
if ("java.lang.Object".equals(paramList.get(0))) {
hasEquals = true;
errorLoc = method.getLocationInfo();
}
} else if ("hashCode".equals(methodName) &&
method.getParameters().size() == 0) {
hasHashCode = true;
}
}
if (hasEquals) {
if (hasHashCode) {
return null;
} else {
StringBuffer errrMsg = new StringBuffer();
errrMsg.append(CodeAnalyzerUtil.getSimpleNameFromQualifiedName(clazzInfo.getName()));
errrMsg.append(" : The class that overrides equals() should ");
errrMsg.append("override hashcode()");
Collection<ErrorDescription> errorList = new ArrayList<ErrorDescription>();
errorList.add(setErrorDetails(errrMsg.toString(), errorLoc));
return errorList;
}
}
return null;
}
}