1-Java代码覆盖率 Java Code Coverage
JaCoCo是一个开源的覆盖率工具(官网地址:http://www.eclemma.org/JaCoCo/),它针对的开发语言是java,其使用方法很灵活,可以嵌入到Ant、Maven中;可以作为Eclipse插件,可以使用其JavaAgent技术监控Java程序等等。
很多第三方的工具提供了对JaCoCo的集成,如sonar、Jenkins等。其他语言也基本都有覆盖率工具,例如python的coverage。
2-jacoco接入 2.1-maven工程接入jacoco 1-IDEA新建Maven工程 IDEA-File-New-Project-Maven直接Next
Groupid=cn.youzan.jacoco
Artifactid=jacoco
Version=默认
2-pom.xml设置 - 在<configuration>中配置具体生成的jacoco.exec的目录和报告的目录,设置includes/excludes;
- 在<rules>中配置对应的覆盖率检测规则;覆盖率规则未达到时,mvn install会失败;
<element>BUNDLE</element>
在check元素中,任何数量的rule元素可以被嵌套
属性 描述 默认 element rule应用的element,可以取值: bundle
, package
, class
, sourcefile
和 method
bundle includes 应当被检查的元素集合名 * excludes 不需要被检查的元素 empty limits 用于检查的 limits
none
<limit implementation="org.jacoco.report.check.Limit">
<counter>METHOD</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
在rule元素中,任何数量的limit元素可以被嵌套
属性 描述 默认 counter 被检查的counter,可以是: INSTRUCTION
, LINE
, BRANCH
, COMPLEXITY
, METHOD
and CLASS
. INSTRUCTION value 需要被检查的counter的值,可以是: TOTALCOUNT
, MISSEDCOUNT
, COVEREDCOUNT
, MISSEDRATIO
and COVEREDRATIO
. COVEREDRATIO minimum 期望的最小值。 none maximum 期望的最大值。 none
- 在<executions>中配置执行步骤:
1)prepare-agent(即构建jacoco-unit.exec);
2)check(即根据在<rules>定义的规矩进行检测);
3)package(生成覆盖率报告,默认生成在target/site/index.html)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.youzan.ycm</groupId>
<artifactId>jacoco_test</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<compiler.source>1.8</compiler.source>
<compiler.target>1.8</compiler.target>
<junit.version>4.12</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.9</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>freewill</finalName>
<plugins>
<plugin>
<inherited>true</inherited>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${compiler.source}</source>
<target>${compiler.target}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.9</version>
<configuration>
<!-- rules里面指定覆盖规则 -->
<rules>
<rule implementation="org.jacoco.maven.RuleConfiguration">
<element>BUNDLE</element>
<limits>
<!-- 指定方法覆盖最低 -->
<limit implementation="org.jacoco.report.check.Limit">
<counter>METHOD</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
<!-- 指定分支覆盖最低 -->
<limit implementation="org.jacoco.report.check.Limit">
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
<!-- 指定类覆盖到,最多Missed 0 -->
<limit implementation="org.jacoco.report.check.Limit">
<counter>CLASS</counter>
<value>MISSEDCOUNT</value>
<maximum>0</maximum>
</limit>
</limits>
</rule>
</rules>
</configuration>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>target/jacoco.exec</dataFile>
<outputDirectory>target/jacoco-wl</outputDirectory>
<includes>
<include>**/Func1**</include>
<include>**/Func2**</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
3-新建类 PS:测试类的命名一定按照如下命名方式来
Func1.java
public class Func1 {
public int add(int a, int b) {
return a + b;
}
public int sub(int a, int b) {
return a - b;
}
}
Func2.java
Func3.java
2个内容一样也可以
public class Func2 {
public int multi(int a, int b) {
return a*b;
}
public int div(int a, int b) {
while(b!=0){
return a/b;
}
if(b<0){
return a/b;
}else{
return -a/b;
}
}
}
4-新建单测类 PS:测试类的命名一定按照如下命名方式来
ATest3.java
import org.junit.Assert;
import org.junit.Test;
public class ATest3 {
private Func2 func2 = new Func2();
@Test
public void testAdd1() {
int a = 10;
int b = 20;
int expected = 200;
Assert.assertEquals(expected, func2.multi(a, b));
}
@Test
public void testSub2() {
int a = 40;
int b = 20;
int expected =2;
Assert.assertEquals(expected, func2.div(a, b));
}
}
BTest.java
import org.junit.Assert;
import org.junit.Test;
public class BTest {
private Func2 func2 = new Func2();
@Test
public void testAdd1() {
int a = 10;
int b = 20;
int expected = 200;
Assert.assertEquals(expected, func2.multi(a, b));
}
@Test
public void testSub2() {
int a = 40;
int b = 20;
int expected =2;
Assert.assertEquals(expected, func2.div(a, b));
}
}
Test1.java
import org.junit.Assert;
import org.junit.Test;
public class Test1 {
private Func1 func1 = new Func1();
@Test
public void testAdd() {
int a = 10;
int b = 20;
int expected = 30;
Assert.assertEquals(expected, func1.add(a, b));
}
@Test
public void testSub() {
int a = 10;
int b = 20;
int expected = -10;
Assert.assertEquals(expected, func1.sub(a, b));
}
}
工程的组织如下:
5-MVN Test 几点说明:
说明1:测试类的命名规范
maven 的测试类需要遵循相应的规范命名,否则无法运行测试类,无法生成测试报告以及覆盖率报告。
jacoco 使用的是 maven-surefire-plugin 插件,它的默认测试类名规范是:
Test*.java:以 Test 开头的 Java 类;
*Test.java:以 Test 结尾的 Java 类;
*TestCase.java:以 TestCase 结尾的 Java 类;
或者可以在pom中自定义测试类:
说明2:includes/excludes设置
同理:excludes的设置,完全参考includes即可
说明3:rules指定覆盖规则
当设置了覆盖率规则,但是实际结果未达标时,mvn test命令正常执行,但是mvn install 失败:
mvn test 成功
mvn install失败,覆盖率规则不达标
例子2:现有开发工程接入情况:ycm-perfrom
举例子
3-如何看懂jacoco报告 Jacoco报告层级:包>类>方法
Jacoco报告纬度:
字段 名称 描述 Instructions 代码指令 字节码中的指令覆盖:后面原理部分会详解 Branches 分支 for,if,while,switch 语句代表分支,报告里面用菱形标识分支,Assert,Boolean也会被定义为分支,判断语句都会被定义为分支 Cyclomatic Complexity 圈复杂度 Jacoco为每个非抽象方法计算圈复杂度,并也会计算每个类,包,组的复杂度。
圈复杂度可以理解为覆盖所有的可能情况最少使用的测试用例数。后面详解
Lines 行 绿色+红色+黄色背景的行才是jacoco统计的行,else不统计行,else里面的代码会计入 Methods/CLasses 方法/类 非抽象方法/类被执行1条指令,覆盖率就统计
3.1-Instructions-代码指令 红色代表未覆盖,绿色代表已覆盖,Cov 为总体覆盖率。
275/357 22%
未覆盖275条指令/总357条指令 覆盖率22%
Jacoco 计算的最小单位就是字节码指令。指令覆盖率表明了在所有的指令中,哪些被执行过以及哪些没有被执行。
3.2-Branches-分支 for,if,while,switch 语句代表分支,Assert,Boolean也会被定义为分支,判断语句都会被定义为分支
分支用菱形标示:
红色菱形:无覆盖,该分支指令均无执行。
黄色菱形:部分覆盖,该分支部分指令被执行。
绿色菱形:全覆盖,该分支所有指令被执行。
PS:指令全覆盖,不代表分支全覆盖!
Jacoco详解 Jacoco详解 Missed Instructions覆盖率100%,但分支覆盖率为75%; 原因:所有代码行都覆盖并不代表所有分支都覆盖完整。
分析:urls!=null这个条件已覆盖,但urls=null这个条件还没有覆盖 ;所有的代码行都有覆盖到、但分支还没有覆盖完整、所以Instructions的覆盖率100%、Braches的覆盖率75%。
3.3-Cyclomatic Complexity-圈复杂度 1-什么是圈复杂度 圈复杂度(Cyclomatic Complexity)是一种代码复杂度的衡量标准,由 Thomas McCabe 于 1976年定义。它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖所有的可能情况最少使用的测试用例数。圈复杂度大说明程序代码的判断逻辑复杂,可能质量低且难于测试和维护。程序的可能错误和高的圈复杂度有着很大关系。
圈复杂度主要与分支语句(if、else、,switch 等)的个数成正相关。可以在图1中看到常用到的几种语句的控制流图(表示程序执行流程的有向图)。当一段代码中含有较多的分支语句,其逻辑复杂程度就会增加。在计算圈复杂度时,可以通过程序控制流图方便的计算出来。
2-采用圈复杂度去衡量代码的好处
1.指出极复杂模块或方法,这样的模块或方法也许可以进一步细化。
2.限制程序逻辑过长。
McCabe&Associates 公司建议尽可能使 V(G) <= 10。NIST(国家标准技术研究所)认为在一些特定情形下,模块圈复杂度上限放宽到 15 会比较合适。
因此圈复杂度 V(G)与代码质量的关系如下:
V(G) ∈ [ 0 , 10 ]:代码质量不错;
V(G) ∈ [ 11 , 15 ]:可能存在需要拆分的代码,应当尽可能想措施重构;
V(G) ∈ [ 16 , ∞ ):必须进行重构;
3.方便做测试计划,确定测试重点。
许多研究指出一模块及方法的圈复杂度和其中的缺陷个数有相关性,许多这类研究发现圈复杂度和模块或者方法的缺陷个数有正相关的关系:圈复杂度最高的模块及方法,其中的缺陷个数也最多,做测试时做重点测试。
3-计算圈复杂度的方法 通常使用的计算公式是V(G) = e – n + 2 , e 代表在控制流图中的边的数量(对应代码中顺序结构的部分),n 代表在控制流图中的节点数量,包括起点和终点(1、所有终点只计算一次,即便有多个return或者throw;2、节点对应代码中的分支语句)。
增加圈复杂度的语句:在代码中的表现形式:在一段代码中含有很多的 if / else 语句或者其他的判定语句(if / else , switch / case , for , while , | | , ? , …)。
代码示例-控制流图
根据公式 V(G) = e – n + 2 = 12 – 8 + 2 = 6 ,上图的圈复杂段为6。
说明一下为什么n = 8,虽然图上的真正节点有12个,但是其中有5个节点为throw、return,这样的节点为end节点,只能记做一个。
4-Jacoco圈复杂度计算 Jacoco 基于下面的方程来计算复杂度,B是分支的数量,D是决策点的数量:
v(G) = B – D + 1
基于每个分支的被覆盖情况,Jacoco也为每个方法计算覆盖和缺失的复杂度。缺失的复杂度同样表示测试案例没有完全覆盖到这个模块。注意Jacoco不将异常处理作为分支,try/catch块也同样不增加复杂度。
例子1:
报告可以看出:圈=3,Missed=2(if,else)
同理可以计算:
multi方法的圈复杂度=0(无分支)-0(无决策点)+1
类的圈复杂度:2(2个方法)-2(2个决策点)+1
例子2:
圈复杂度=?
5-降低圈复杂度的重构技术 1.Extract Method(提炼函数)
2.Substitute Algorithm(替换你的算法)
3.Decompose Conditional(分解条件式)
4.Consolidate Conditional Expression(合并条件式)
5.Consolidate Duplicate Conditional Fragments(合并重复的条件片断)
6.Remove Control Flag(移除控制标记)
7.Parameterize Method(令函数携带参数)
8.异常逻辑处理型重构方法
9.状态处理型重构方法(1)
10.状态处理型重构方法(2)
11.case语句重构方法(1)
参考文档:https://blog.csdn.net/u010684134/article/details/94412483
3.4-Lines-行 绿色+红色+黄色背景的行才是jacoco实际统计的代码行,
红色背景代表Missed行
黄色+绿色代表覆盖行;
无背景的都不统计(变量的定义,引用,else定义等)
上面的行覆盖结果:
3.5-方法/类 方法,类里面有一行指令被执行,代表覆盖
4-Jacoco的原理 JaCoCo使用ASM技术修改字节码方法,可以修改Jar文件、class文件字节码文件。
1-ASM简介 ASM是一个Java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。ASM可以直接产生二进制class文件,也可以在类被加载入Java虚拟机之前动态改变类行为。Java class被存储在严格格式定义的.class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。简单使用
2-插桩方式 Jacoco详解 上图包含了几种不同的收集覆盖率信息的方法,每个方法的实现都不太一样,这里主要关心字节码注入这种方式(Byte Code)。Byte Code包含Offline和On-The-Fly两种注入方式
On-the-fly更加方便获取代码覆盖率
无需提前进行字节码插桩
无需停机(Offline需要停机),可以实时获取覆盖率
Offline无需额外开启代理
2.1-Offline 对Class文件进行插桩(探针),生成最终的目标文件,执行目标文件以后得到覆盖执行结果,最终生成覆盖率报告。
Offline使用场景(From Jacoco Documentation)
运行环境不支持Java Agent
部署环境不允许设置JVM参数
字节码需要被转换成其他虚拟机字节码,如Android Dalvik Vm
动态修改字节码文件和其他Agent冲突
无法自定义用户加载类
【主要用于单测,集成测试等静态场景】
offline大致流程:
Jacoco详解 2.2-On The Fly JVM通过-javaagent参数指定特定的jar文件启动Instrumentation代理程序,代理程序在装载class文件前判断是否已经转换修改了该文件,若没有,则将探针(统计代码)插入class文件,最后在JVM执行测试代码的过程中完成对覆盖率的分析。
【主要用于服务化系统的代码动态覆盖率获取】
JaCoCo代理收集执行信息并根据请求或在JVM退出时将其转储。有三种不同的执行数据输出模式:
文件系统:在JVM终止时,执行数据被写入本地文件。
TCP套接字服务器:外部工具可以连接到JVM,并通过套接字连接检索执行数据。可以在VM退出时进行可选的执行数据重置和执行数据转储。
TCP套接字客户端:启动时,JaCoCo代理连接到给定的TCP端点。执行数据根据请求写入套接字连接。可以在VM退出时进行可选的执行数据重置和执行数据转储。
该代理jacocoagent.jar是JaCoCo发行版的一部分,包括所有必需的依赖项。可以使用以下JVM选项激活Java代理:
-javaagent:[yourpath /] jacocoagent.jar = [option1] = [value1],[option2] = [value2]
通过这种方式进行服务的agent 启动时,一般需要在容器的启动脚本配置,下面是一个参考的配置:
#!/usr/bin/env bash
MAIN_CLASS="me.ele.marketing.hongbao.Application"
SCRIPTS_DIR=`dirname "$0"`
DIR_PROJECT=`cd $SCRIPTS_DIR && pwd`
DIR_TMP="${DIR_PROJECT}/tmp"
DIR_LOG="${DIR_PROJECT}/log"
DATETIME=`date +"%Y%m%d_%H%M%S"`
mkdir -p ${DIR_TMP} ${DIR_LOG}
if [ -z ${ELEAPPOS_OFFER_MEM+x} ]; then
echo "Cannot get system mem from system var, because mem var ELEAPPOS_OFFER_MEM not set."
MEM_OPTS="${JVM_MEMORY}"
else
echo "ELEAPPOS_OFFER_MEM is set: ${ELEAPPOS_OFFER_MEM}, so generate jvm memory by system mem var..."
JVM_MEM=$(($ELEAPPOS_OFFER_MEM*700))
MEM_OPTS="-Xms${JVM_MEM}m -Xmx${JVM_MEM}m"
echo "jvm mem is set to: ${MEM_OPTS}"
fi
#步骤1:下载包
wget -O jacocoagent.jar http://repo1.maven.org/maven2/org/jacoco/org.jacoco.agent/0.8.1/org.jacoco.agent-0.8.1-runtime.jar
#步骤2:确定本机IP
LOCAL_IP=$(/sbin/ifconfig -a|grep inet|grep -v 127.0.0.1|grep -v 172.17.0.1|grep -v inet6|awk \'{print $2}\'|tr -d "addr:"|head -1)
MEM_OPTS=${MEM_OPTS}
GC_OPTS="-XX:+UseG1GC"
GC_OPTS="${GC_OPTS} -XX:MaxGCPauseMillis=20"
GC_OPTS="${GC_OPTS} -XX:+UnlockExperimentalVMOptions"
GC_OPTS="${GC_OPTS} -XX:InitiatingHeapOccupancyPercent=56"
GC_OPTS="${GC_OPTS} -Xloggc:${DIR_LOG}/gc_${DATETIME}.log"
GC_OPTS="${GC_OPTS} -XX:+PrintGCDateStamps"
GC_OPTS="${GC_OPTS} -XX:+PrintGCDetails"
GC_OPTS="${GC_OPTS} -XX:+HeapDumpOnOutOfMemoryError"
GC_OPTS="${GC_OPTS} -XX:HeapDumpPath=${DIR_LOG}/heapdump_${DATETIME}.hprof"
PARAMS="-Dfile.encoding=UTF-8"
PARAMS="${PARAMS} -DAPPID=${APP_ID}"
PARAMS="${PARAMS} -Duser.dir=${DIR_PROJECT}"
PARAMS="${PARAMS} -Djava.io.tmpdir=${DIR_TMP}"
PARAMS="${PARAMS} -DTEAM=${APP_TEAM}"
#步骤3:添加启动agent的参数,主要注意 IP+端口,因为这个是jacoco agent的通信接口(tcp)
PARAMS="${PARAMS} -javaagent:/data/marketing.hongbao/jacocoagent.jar=includes=me.ele.marketing.hongbao.*,output=tcpserver,address=${LOCAL_IP},port=8335,classdumpdir=/data/marketing.hongbao/eship/jacoco"
CLASS_PATH="$PROJECT_DIR/conf:"
CLASS_PATH="${CLASS_PATH}:$PROJECT_DIR/conf:"
CLASS_PATH="${CLASS_PATH}:$PROJECT_DIR/lib/*:"
CLASS_PATH="${CLASS_PATH}:/data/marketing.hongbao/marketing.hongbao/*"
#verify_codes
echo `pwd`
echo "##########################################################"
echo "exec java -server ${MEM_OPTS} ${GC_OPTS} ${PARAMS} -classpath ${CLASS_PATH} ${MAIN_CLASS}"
echo "##########################################################"
exec java -server ${MEM_OPTS} ${GC_OPTS} ${PARAMS} -classpath ${CLASS_PATH} ${MAIN_CLASS}
重点看下步骤3:
-javaagent:/data/marketing.test/jacocoagent.jar=includes=me.test.maketing.*,output=tcpserver,address=${LOCAL_IP},port=8335,classdumpdir=/data/marketing.hongbao/eship/jacoco"
当服务启动的时候,容器的8335/默认6330端口会开启TCP Server,如何生成覆盖率结果:
// dump结果数据
java -jar jacococli.jar dump --port 6300 --destfile data/jacoco-it.exec
// 生成覆盖率结果
java -jar jacococli.jar report data/jacoco-it.exec --classfiles ***/classes --html html
就可以获取覆盖率的结果数据,这种方法获取的是全量的代码覆盖率。
远程代理控制的安全注意事项
在tcpserver和 tcpclient模式下打开的端口和连接以及JMX接口不提供任何身份验证机制。如果在生产系统上运行JaCoCo,请确保没有不受信任的源可以访问TCP服务器端口,或者JaCoCo TCP客户端仅连接到受信任的目标。否则,可能会泄露应用程序的内部信息,或者可能发生DOS攻击。
3-增量覆盖率 增量覆盖率的思想:
1. 获取测试完成后的 exec 文件(二进制文件,里面有探针的覆盖执行信息);
2. 获取基线提交与被测提交之间的差异代码;
3. 对差异代码进行解析,切割为更小的颗粒度,我们选择方法作为最小纬度;
4. 改造 JaCoCo ,使它支持仅对差异代码生成覆盖率报告;
3.1-获取exec数据 参考On The Fly方式获取dump数据,或者通过JaCoCo 开放出来的 API 进行 exec 文件获取:
public void dumpData(String localRepoDir, List<IcovRequest> icovRequestList) throws IOException {
icovRequestList.forEach(req -> req.validate());
icovRequestList.parallelStream().map(icovRequest -> {
String destFileDir = ...;
String address = icovRequest.getAddress();
try {
final FileOutputStream localFile = new FileOutputStream(destFileDir + "/" + DEST_FILE_NAME);
final ExecutionDataWriter localWriter = new ExecutionDataWriter(localFile);
final Socket socket = new Socket(InetAddress.getByName(address), PORT);
final RemoteControlWriter writer = new RemoteControlWriter(socket.getOutputStream());
final RemoteControlReader reader = new RemoteControlReader(socket.getInputStream());
reader.setSessionInfoVisitor(localWriter);
reader.setExecutionDataVisitor(localWriter);
writer.visitDumpCommand(true, false);
if (!reader.read()) {
throw new IOException("Socket closed unexpectedly.");
}
...
} ...
return null;
}).count();
}
3.2-获取代码差异 JGit 是一个用 Java 写成的功能比较健全的 Git 的实现,它在 Java 社区中被广泛使用。在这一步的主要流程是获取基线提交与被测提交之间的差异代码,然后过滤一些需要排除的文件(比如非 Java 文件、测试文件等等),对剩余文件进行解析,将变更代码解析到方法纬度,部分代码片段如下:
private List<AnalyzeRequest> findDiffClasses(IcovRequest request) throws GitAPIException, IOException {
String gitAppName = DiffService.extractAppNameFrom(request.getRepoURL());
String gitDir = workDirFor(localRepoDir,request) + File.separator + gitAppName;
DiffService.cloneBranch(request.getRepoURL(),gitDir,branchName);
String masterCommit = DiffService.getCommitId(gitDir);
List<DiffEntry> diffs = diffService.diffList(request.getRepoURL(),gitDir,request.getNowCommit(),masterCommit);
List<AnalyzeRequest> diffClasses = new ArrayList<>();
String classPath;
for (DiffEntry diff : diffs) {
if(diff.getChangeType() == DiffEntry.ChangeType.DELETE){
continue;
}
AnalyzeRequest analyzeRequest = new AnalyzeRequest();
if(diff.getChangeType() == DiffEntry.ChangeType.ADD){
...
}else {
HashSet<String> changedMethods = MethodDiff.methodDiffInClass(oldPath, newPath);
analyzeRequest.setMethodnames(changedMethods);
}
classPath = gitDir + File.separator + diff.getNewPath().replace("src/main/java","target/classes").replace(".java",".class");
analyzeRequest.setClassesPath(classPath);
diffClasses.add(analyzeRequest);
}
return diffClasses;
}
3.3-差异代码解析 JaCoCo默认的注入方式为全量注入。通过阅读源码,发现注入的逻辑主要在ClassProbesAdapter中。ASM在遍历字节码时,每次访问一个方法定义,都会回调这个类的visitMethod方法 ,在visitMethod方法中再调用ClassProbeVisitor的visitMethod方法,并最终调用MethodInstrumenter完成注入。部分代码片段如下:
@Override
public 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方法,只对变化了方法进行注入:
@Override
public 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);
}
}
3.4-差异代码覆盖率 生成增量代码的覆盖率报告和增量注入的原理类似,通过阅读源码,分别需要修改Analyzer(只对变化的类做处理):
和ReportClassProbesAdapter(只对变化的方法做处理):
4-探针 JaCoCo通过ASM在字节码中插入Probe指针(探测指针),每个探测指针都是一个BOOL变量(true表示执行、false表示没有执行),程序运行时通过改变指针的结果来检测代码的执行情况(不会改变原代码的行为)。
1-插入探针的源码: boolean[] arrayOfBoolean = $jacocoInit();
arrayOfBoolean[4] = true;
2-插入探针的字节码指令: 例子1:
aload_2 # 从局部变量2中装载引用类型值入栈
iconst_4 # 4(int)值入栈
iconst_1 # 1(int)值入栈
bastore # 将栈顶boolean类型值或byte类型值保存到指定boolean类型数组或byte类型数组的指定项。
例子2:
aload_2 # 从局部变量2中装载引用类型值入栈
bipush 6 #valuebyte值带符号扩展成int值入栈
iconst_1 #1(int)值入栈
bastore #将栈顶boolean类型值或byte类型值保存到指定boolean类型数组或byte类型数组的指定项。
探测代码的大小取决于探测阵列变量的位置和探测标识符的值,因为可以使用不同的操作码。如下表所示,每个探测器的开销介于4到7个字节的附加字节码之间:
3-Java字节码指令大全: https://www.cnblogs.com/longjee/p/8675771.html
4-关于switch插桩分析 源码:
public void testSwitch(int i){
switch(i) {
case 1:
System.out.println("1");
break;
case 2:
System.out.println("2");
break;
case 3:
System.out.println("3");
break;
case 4:
System.out.println("4");
break;
case 10:
System.out.println("10");
break;
default:
System.out.println("...");
break;
}//switch
}
插桩后的源码:
public void testSwitch(int arg1) {
boolean[] arrayOfBoolean = $jacocoInit();
switch (i)
{
case 1:
System.out.println("1");
arrayOfBoolean[4] = true; break;
case 2:
System.out.println("2");
arrayOfBoolean[5] = true; break;
case 3:
System.out.println("3");
arrayOfBoolean[6] = true; break;
case 4:
System.out.println("4");
arrayOfBoolean[7] = true; break;
case 10:
System.out.println("10");
arrayOfBoolean[8] = true; break;
case 5:
case 6:
case 7:
case 8:
case 9:
default:
System.out.println("...");
arrayOfBoolean[9] = true;
}
arrayOfBoolean[10] = true;
}
我们可以发现,每一处label处都插入了探针,以及最后的return处也插入了一个探针。
源码-字节码(未插桩):
public void testSwitch(int);
Code:
0: iload_1
1: tableswitch { // 1 to 10
1: 56
2: 67
3: 78
4: 89
5: 111
6: 111
7: 111
8: 111
9: 111
10: 100
default: 111
}
56: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
59: ldc #8 // String 1
61: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
64: goto 119
67: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
70: ldc #10 // String 2
72: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
75: goto 119
78: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
81: ldc #11 // String 3
83: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
86: goto 119
89: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
92: ldc #12 // String 4
94: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
97: goto 119
100: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
103: ldc #13 // String 10
105: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
108: goto 119
111: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
114: ldc #14 // String ...
116: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
119: return
源码-字节码(插桩后):
public void testSwitch(int);
Code:
0: invokestatic #65 // Method $jacocoInit:()[Z
3: astore_2
4: iload_1
5: tableswitch { // 1 to 10
1: 60
2: 75
3: 90
4: 106
5: 138
6: 138
7: 138
8: 138
9: 138
10: 122
default: 138
}
60: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
63: ldc #8 // String 1
65: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//case 1探针
68: aload_2
69: iconst_4
70: iconst_1
71: bastore
72: goto 151
75: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
78: ldc #10 // String 2
80: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//case 2探针
83: aload_2
84: iconst_5
85: iconst_1
86: bastore
87: goto 151
90: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
93: ldc #11 // String 3
95: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//case 3 探针
98: aload_2
99: bipush 6
101: iconst_1
102: bastore
103: goto 151
106: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
109: ldc #12 // String 4
111: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//case 4 探针
114: aload_2
115: bipush 7
117: iconst_1
118: bastore
119: goto 151
122: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
125: ldc #13 // String 10
127: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//case 10探针
130: aload_2
131: bipush 8
133: iconst_1
134: bastore
135: goto 151
138: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
141: ldc #14 // String ...
143: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//default 探针
146: aload_2
147: bipush 9
149: iconst_1
150: bastore
//return 探针
151: aload_2
152: bipush 10
154: iconst_1
155: bastore
156: return
4-插桩策略 源码:
public static void example() {
a();
if (cond()) {
b();
} else {
c();
}
d();
}
字节码:
public static example()V
INVOKESTATIC a()V
INVOKESTATIC cond()Z
IFEQ L1
INVOKESTATIC b()V
GOTO L2
L1: INVOKESTATIC c()V
L2: INVOKESTATIC d()V
RETURN
这样我们可以使用ASM框架在字节码文件中进行插桩操作,具体的是插入探针probe,一般是Boolean数组,下面是原始的控制流图,以及插桩完成的控制流图。
Jacoco详解 可以看出,探针的位置位于分支后
由Java字节码定义的控制流图有不同的类型,每个类型连接一个源指令和一个目标指令,当然有时候源指令和目标指令并不存在,或者无法被明确(异常)。不同类型的插入策略也是不一样的。
Type Source Target Remarks ENTRY - First instruction in method SEQUENCE Instruction, except GOTO
, xRETURN
, THROW
, TABLESWITCH
and LOOKUPSWITCH
Subsequent instruction JUMP GOTO
, IFx
, TABLESWITCH
or LOOKUPSWITCH
instruction Target instruction TABLESWITCH
and LOOKUPSWITCH
will define multiple edges. EXHANDLER Any instruction in handler scope Target instruction EXIT xRETURN
or THROW
instruction - EXEXIT Any instruction - Unhandled exception.
下面是具体的插入探针的策略:
Type Before After Remarks SEQUENCE Jacoco详解 Jacoco详解 如果是简单序列,则将探针简单地插入两个指令之间。
JUMP (unconditional) Jacoco详解 Jacoco详解 由于在任何情况下都执行无条件跳转,因此我们也可以在GOTO指令之前插入探针。 JUMP (conditional) Jacoco详解 Jacoco详解 向条件跳转添加探针会比较棘手。我们反转操作码的语义,并在条件跳转之后立即添加探测。 EXIT Jacoco详解 Jacoco详解 正如RETURN和THROW语句的本质一样,实际上是将方法留在了我们在这些语句之前添加探针的位置。
注意到探针是线程安全的,它不会改变操作栈和本地数组。它也不会通过外部的调用而离开函数。先决条件仅仅是探针数组作为一个本地变量被获取。在每个函数的开始,附加的指令代码将会插入以获得相应类的数组对象,避免代码复制,这个初始化会在静态私有方法$jacocoinit()中进行。
6-Jacoco与Jenkins http://shangyehua-jenkins.cd-qa.qima-inc.com/job/Jacoco-Bit-Commerce/
1-安装Jacoco plugin
2-新建任务
当 “新建”一个任务时,在 构建后操作中点击 “增加构建后操作步骤”下来框中选择“Record JaCoCo coverag report”
3-查看报告
7-关于一个jacoco的专利 Jacoco详解