使用Java 6 API分析源代码

原文: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实例。

下一步,使用JavaCompilergetTask()创建编译任务。此时任务还没开始,而要通过调用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;
    }
}


 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值