在项目中使用hudson来做持续集成,使用cobertura来分析代码覆盖率的时候发现它会把一些本不应该算做覆盖分析的代码也会加入到最终的统计中,导致因为一些默认的构造函数或者一些没有被覆盖到的get,set方法使得整个统计数据无法达到比较完美的程度,比如我们的方法覆盖率一直到不了80%,感觉很不给力,鉴于我们会对方法覆盖率做一定的要求,我就下载了cobertura的代码,准备打个patch。
类似的代码覆盖率工具通常都离不开对原始文件的打点处理,也就是会分析代码并动态生成打点的字节码,cobertura也不例外,会在指定路径下生成instrumented文件夹,里面包含了被打点处理过的class,打点后的类类似:
- source by cobertura 1.9.3:
- public class XX extends ParentClass implements filter, HasBeenInstrumented
- {
- public static int getDefaultParams(someDto)
- {
- int i = 0;int __cobertura__branch__number == -1;
- int __cobertura_line__number__;
- ProjectData.getGlobalProjectData().getOrClassData(“com.xxx.web.utils.xxxUtil”).touch(40); StringBuffer result = new StringBuffer();
- ………
- }
- }
所有方法内部的行或者分支都被如上代码打点,使用了asm来实现打点逻辑的字节码生成(asm使用了visitor+adapter模式代代码),下面介绍一些cobertura中的核心结构: net.sourceforge.cobertura.coveragedata包里面定义了记录覆盖率数据的javabean,比如ClassData,LineData,JumpData,比如会记录当前代码的hit次数,某个分支是否被hit到等等,这些数据对象的定义都在这个包下面,另外这个包里面有个HasBeenInstrumented接口,这个接口没有实现,只是用来作为是否需要对当前类做打点的判断依据,比如已经打过点的类就不会再次打点,如果你有类不想被打点,也可以implements这个接口(不建议这么做,因为cobertura代码本身就不稳定),copy一段代码吧
- ClassInstrumenter.java
- // Do not attempt to instrument interfaces or classes that
- // have already been instrumented
- //如果该类实现了hasBeenInstrumented接口或本身就是接口,则跳过打点
- if (((access & Opcodes.ACC_INTERFACE) != 0)
- || arrayContains(interfaces, hasBeenInstrumented))
- {
- super.visit(version, access, name, signature, superName,
- interfaces);
- }
- else
- {
- instrument = true;
- // Flag this class as having been instrumented
- String[] newnewInterfaces = new String[interfaces.length + 1];
- System.arraycopy(interfaces, 0, newInterfaces, 0,
- interfaces.length);
- //对已经打点的类加上implements HasBeenInstrumented
- newInterfaces[newInterfaces.length - 1] = hasBeenInstrumented;
- super.visit(version, access, name, signature, superName,
- newInterfaces);
- }
net.sourceforge.cobertura.instrument 这个包顾名思义,包含的所有打点相关的逻辑,最核心的类有三个
- ClassInstrumenter 这个类前面有提到,它实现了asm中的ClassAdapter接口,asm的visitor加adapter模式,tree模式等等大家可以在asm官方的guide中看到介绍,ClassInstrumenter的作用就是作为对单个class文件字节码注入的入口,在visitMethod方法中调用了FirstPassMethodInstrumenter来处理对method的visit
- FirstPassMethodInstrumenter,SecondPassMethodInstrumenter。这两个东东完成了对方法内部的打点任务,第一个类做了采样的动作,分析了代码里有多少行,有多少个分支等等,在visitEnd方法里调用了SecondPassMethodInstrumenter,使用了asm中的Tree模式再次visit了一次方法的代码并添加了打点的逻辑代码:
- SecondPassMethodInstrumenter.java
- public void visitLineNumber(int line, Label start)
- {
- // Record initial information about this line of code
- currentLine = line;
- currentJump = 0;
- instrumentOwnerClass();
- // Mark the current line number as covered:
- // classData.touch(line)
- mv.visitIntInsn(SIPUSH, line);
- mv.visitMethodInsn(INVOKESTATIC,
- TOUCH_COLLECTOR_CLASS, “touch”,
- “(Ljava/lang/String;I)V”);
- super.visitLineNumber(line, start);
- }
剩下的一些包比如reporting,merge提供数据合并和报表展示,ant提供了对ant的集成,还有些main方法提供了命令行的功能,javancss包使用javacc生成代码来分析java语法,提供了对代码质量的计算和分析,话说。。。我重来没在hudson里找到相关的视图,另外这件事明显该交给分析静态代码的工具来干,比如pmd,checkstyle之类,我在pmd中看到了类似的代码,比如分析方法中存在的if,for等再根据方法和这些关键字出现的次数来计算代码的复杂度,Javancss.java 是整个代码分析的入口,用编写者的话说,这个是大脑,这部分代码我估计迟早得干掉。
说了半天了,都是废话,我的目标是对cobertura的ignore功能做改进,最开始我的假设是cobertura通过实现自定义的classadapter和methodadapter来实现对代码的打点工作,而visitor模式的最大缺点是你无法灵活的根据上下文来决定是否需要对一行代码判断是否需要ignore,而asm的tree模式可以作为一个合适的方式来补充实现非常灵活的ignore的规则。在看完cobertura和asm的部分代码后,我发现要在现有的代码结构中添加相关的逻辑还是有点复杂,不是一个patch就可以解决的问题。 如果实现一些通用的ignorePattern模式在现有的条件下也是可行的,比如定义ClassIgnorePattern,methodIgnorePattern,LineIgnorePattern,这种方式可以提供一些常用的实现,比如对默认构造函数的忽略,对代码中get,set方法忽略,对记录日志代码的忽略等等,当然在这种设计下,用户也可以自定义一些IgnorePattern,当然。。这些都是我最初的想法,后来我查询了sourceforge上cobertura的patch list中已经有人对类似的需求打了patch,虽然在实现上只是单纯的硬编码实现(可以忽略get,set方法,默认的构造函数),不过也算是实现了这个需求,也就是说在1.9.4.2或者1.9.5这个版本,如果大家配置相关参数为true的话,项目的覆盖率会有较大的提升。
不过话说回来,cobertura里面代码注释是相当的少,规范性也做的不好,代码有点乱,这个ignore的patch已经有人提出了一些异议,但还是被加到了trunk中,我个人觉得只要可配置应该也没太大的问题,只是这个功能的实现实在是太硬编码了,研究了asm代码也算小有收获吧,但是个人感觉如果采用asm的tree模式加上官方文档guide提到的transformer来实现对于定制化覆盖率这件事会比较轻松,缺点是效率会降低,但是覆盖率通常都是持续集成来做,时间应该不是我们关注的重点,最多也就是30%-40%的损失。 总在说要参与开源项目,想到什么都希望能有个结论,这次总算做的我自己满意了,留篇文章做纪念吧,有空我会试试把我的想法落实,练练手。(如果您看到这里,我感觉我们很对路了,如果你有什么好的想法或者建议请联系我:element8325@gmail.com,另外如果您想换个环境也可以通过邮件联系我,谢谢)