正文
生成代码的覆盖率报告,首先想到的就是
JaCoCo
,下面分别介绍一下JaCoCo的原理和我们所做的改造。
JaCoCo概述
JaCoCo包含了多种维度的
覆盖率计数器
:指令级计数器(C0 coverage)、分支级计数器(C1 coverage)、圈复杂度、行覆盖、方法覆盖、类覆盖。其覆盖率报告的示例如下:
-
绿色:表示行覆盖充分。
-
红色:表示未覆盖的行。
-
黄色棱形:表示分支覆盖不全。
-
绿色棱形:表示分支覆盖完全。
注入原理
JaCoCo主要通过代码注入的方式来实现上面覆盖率的功能。JaCoCo支持的注入方式如下图(图片出自
这里
)所示:
包含了几种不同的收集覆盖率信息的方法,每个方法的实现都不太一样,这里主要关心字节码注入这种方式(Byte Code)。Byte Code包含Offline和On-The-Fly两种注入方式:
-
Offline:在生成最终的目标文件之前,对Class文件进行插桩,生成最终的目标文件,执行目标文件以后得到覆盖执行结果,最终生成覆盖率报告。
-
On-The-Fly:JVM通过-javaagent指定特定的Jar来启动Instrumentation代理程序,代理程序在ClassLoader装载一个class前先判断是否需要对class进行注入,对于需要注入的class进行注入。覆盖率结果可以在JVM执行代码的过程中完成。
可以看到,On-The-Fly因为要修改JVM参数,所以对环境的要求比较高,为了屏蔽工具对虚拟机环境的依赖,我们的代码注入主要选择Offline这种方式。
Offline的工作流程:
-
在生成最终目标文件之前对字节码进行插桩。
-
运行测试代码,得到运行时数据。
-
根据运行时数据、生成的class文件、源码生成覆盖率报告。
通过一张图来形象地表示一下:
如何实现代码注入呢?举个例子说明一下:
JaCoCo通过ASM在字节码中插入Probe指针(探测指针),每个探测指针都是一个BOOL变量(true表示执行、false表示没有执行),程序运行时通过改变指针的结果来检测代码的执行情况(不会改变原代码的行为)。探测指针完整插入策略请参考
Probe Insertion Strategy
。
增量注入
介绍完JaCoCo注入原理以后,我们来看看如何做到增量注入:
JaCoCo默认的注入方式为全量注入。通过阅读源码,发现注入的逻辑主要在ClassProbesAdapter中。ASM在遍历字节码时,每次访问一个方法定义,都会回调这个类的visitMethod方法,在visitMethod方法中再调用ClassProbeVisitor的visitMethod方法,并最终调用MethodInstrumenter完成注入。部分代码片段如下:
@Overridepublic final MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { final MethodProbesVisitor methodProbes; final MethodProbesVisitor mv = cv.visitMethod(access, name, desc,
signature, exceptions); if (mv == null) {
methodProbes = EMPTY_METHOD_PROBES_VISITOR;
} else {
methodProbes = mv;
} return new MethodSanitizer(null, access, name, desc, signature,
exceptions) { @Override
public void visitEnd() { super.visitEnd();
LabelFlowAnalyzer.markLabels(this); final MethodProbesAdapter probesAdapter = new MethodProbesAdapter(
methodProbes, ClassProbesAdapter.this); if (trackFrames) { final AnalyzerAdapter analyzer = new AnalyzerAdapter(
ClassProbesAdapter.this.name, access, name, desc,
probesAdapter);
probesAdapter.setAnalyzer(analyzer); this.accept(analyzer);
} else { this.accept(probesAdapter);
}
}
};
}
看到这里基本上已经知道如何去修改JaCoCo的源码了。继承原有的ClassInstrumenter和ClassProbesAdapter,修改其中的visitMethod方法,只对变化了方法进行注入:
@Overridepublic final MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { if (Utils.shoudHackMethod(name,desc,signature,changedMethods,cv.getClassName())) {
...
} else { return cv.getCv().visitMethod(access, name, desc, signature, exceptions);
}
}
生成增量代码的覆盖率报告
和增量注入的原理类似,通过阅读源码,分别需要修改Analyzer(只对变化的类做处理):
@Overridepublic void analyzeClass(final ClassReader reader) { if (Utils.shoudHackMethod(reader.getClassName(),changedMethods)) {
...
}
}
和ReportClassProbesAdapter(只对变化的方法做处理):
@Override