能找到这里,说明对jacoco的原理和使用有了一定的了解,而我写这边文章主要是网络上基本没有完整文档加代码的jaocco增量覆盖说明,所以我想分享些东西让需要这方面的人快速去实现自己想要的功能,那么如果想实现增量代码覆盖率需要做到哪些工作呢?

大家在网络上找到的实现方式无外乎三种

  1. 获取到增量代码,在jacoco进行插桩时判断是否是增量代码后再进行插桩,这样需要两个步骤,一是获取增量代码,二是找到jacoco的插桩逻辑进行修改
  2. 获取增量代码,在report阶段去判断方法是否是增量,再去生成报告
  3. 获取差异代码,解析生成的report报告,再过滤出差异代码的报告

首先第一种需要对java字节码操作比较熟悉,难度较高,我们不谈,第三种去解析生成的报告,可能存在误差

所以我们一般选择第二种,而网络上所有的增量实现基本是基于第二种,我们先看看下面的图

jacoco如何集成到Kubeletes jacoco教程_List

上图说明了jacoco测试覆盖率的生成流程,而我们要做的是在report的时候加入我们的逻辑

根据我们的方案,我们需要三个动作

  • 计算出两个版本的差异代码(基于git)
  • 将差异代码在jacoco的report阶段传给jacoco
  • 修改jacoco源码,生成报告时判断代码是否是增量代码,只有增量代码才去生成报告

下面我们逐步讲解上述步骤

计算差异代码

计算差异代码我实现了一个简单的工程:差异代码获取

主要用到了两个工具类

1.  
2.  
<groupId>org.eclipse.jgit</groupId>
3.  
<artifactId>org.eclipse.jgit</artifactId>
4.  
</dependency>
5.   
6.  
<!-- https://mvnrepository.com/artifact/com.github.javaparser/javaparser-core -->
7.  
<dependency>
8.  
<groupId>com.github.javaparser</groupId>
9.  
<artifactId>javaparser-core</artifactId>
10.  
</dependency>
org.eclipse.jgit主要用于从git获取代码,并获取到存在变更的文件
javaparser-core是一个java解析类,能将class类文件解析成树状,方便我们去获取差异类
1.  
/**
2.  
 * 获取差异类
3.  
 *
4.  
@param diffMethodParams
5.  
@return
6.  
 */
7.  
public List<ClassInfoResult> diffMethods(DiffMethodParams diffMethodParams) {
8.  
try {
9.  
//原有代码git对象
10.  
Git baseGit = cloneRepository(diffMethodParams.getGitUrl(), localBaseRepoDir + diffMethodParams.getBaseVersion(), diffMethodParams.getBaseVersion());
11.  
//现有代码git对象
12.  
Git nowGit = cloneRepository(diffMethodParams.getGitUrl(), localBaseRepoDir + diffMethodParams.getNowVersion(), diffMethodParams.getNowVersion());
13.  
AbstractTreeIterator baseTree = prepareTreeParser(baseGit.getRepository(), diffMethodParams.getBaseVersion());
14.  
AbstractTreeIterator nowTree = prepareTreeParser(nowGit.getRepository(), diffMethodParams.getNowVersion());
15.  
//获取两个版本之间的差异代码
16.  
List<DiffEntry> diff = nowGit.diff().setOldTree(baseTree).setNewTree(nowTree).setShowNameAndStatusOnly(true).call();
17.  
//过滤出有效的差异代码
18.  
Collection<DiffEntry> validDiffList = diff.stream()
19.  
//只计算java文件
20.  
.filter(e -> e.getNewPath().endsWith(".java"))
21.  
//排除测试文件
22.  
.filter(e -> e.getNewPath().contains("src/main/java"))
23.  
//只计算新增和变更文件
24.  
.filter(e -> DiffEntry.ChangeType.ADD.equals(e.getChangeType()) || DiffEntry.ChangeType.MODIFY.equals(e.getChangeType()))
25.  
.collect(Collectors.toList());
26.  
if (CollectionUtils.isEmpty(validDiffList)) {
27.  
return null;
28.  
}
29.  
/**
30.  
 * 多线程获取旧代码和新代码的差异类及差异方法
31.  
 */
32.  
List<CompletableFuture<ClassInfoResult>> priceFuture = validDiffList.stream().map(item -> getClassMethods(getClassFile(baseGit, item.getNewPath()), getClassFile(nowGit, item.getNewPath()), item)).collect(Collectors.toList());
33.  
return priceFuture.stream().map(CompletableFuture::join).filter(Objects::nonNull).collect(Collectors.toList());
34.  
} catch (GitAPIException e) {
35.  
e.printStackTrace();
36.  
}
37.  
return null;
38.  
}
以上代码为获取差异类的核心代码
1.   
2.  
/**
3.  
 * 获取类的增量方法
4.  
 *
5.  
@param oldClassFile 旧类的本地地址
6.  
@param mewClassFile 新类的本地地址
7.  
@param diffEntry 差异类
8.  
@return
9.  
 */
10.  
private CompletableFuture<ClassInfoResult> getClassMethods(String oldClassFile, String mewClassFile, DiffEntry diffEntry) {
11.  
//多线程获取差异方法,此处只要考虑增量代码太多的情况下,每个类都需要遍历所有方法,采用多线程方式加快速度
12.  
return CompletableFuture.supplyAsync(() -> {
13.  
String className = diffEntry.getNewPath().split("\\.")[0].split("src/main/java/")[1];
14.  
//新增类直接标记,不用计算方法
15.  
if (DiffEntry.ChangeType.ADD.equals(diffEntry.getChangeType())) {
16.  
return ClassInfoResult.builder()
17.  
.classFile(className)
18.  
.type(DiffEntry.ChangeType.ADD.name())
19.  
.build();
20.  
}
21.  
List<MethodInfoResult> diffMethods;
22.  
//获取新类的所有方法
23.  
List<MethodInfoResult> newMethodInfoResults = MethodParserUtils.parseMethods(mewClassFile);
24.  
//如果新类为空,没必要比较
25.  
if (CollectionUtils.isEmpty(newMethodInfoResults)) {
26.  
return null;
27.  
}
28.  
//获取旧类的所有方法
29.  
List<MethodInfoResult> oldMethodInfoResults = MethodParserUtils.parseMethods(oldClassFile);
30.  
//如果旧类为空,新类的方法所有为增量
31.  
if (CollectionUtils.isEmpty(oldMethodInfoResults)) {
32.  
diffMethods = newMethodInfoResults;
33.  
} else { //否则,计算增量方法
34.  
List<String> md5s = oldMethodInfoResults.stream().map(MethodInfoResult::getMd5).collect(Collectors.toList());
35.  
diffMethods = newMethodInfoResults.stream().filter(m -> !md5s.contains(m.getMd5())).collect(Collectors.toList());
36.  
}
37.  
//没有增量方法,过滤掉
38.  
if (CollectionUtils.isEmpty(diffMethods)) {
39.  
return null;
40.  
}
41.  
ClassInfoResult result = ClassInfoResult.builder()
42.  
.classFile(className)
43.  
.methodInfos(diffMethods)
44.  
.type(DiffEntry.ChangeType.MODIFY.name())
45.  
.build();
46.  
return result;
47.  
}, executor);
48.  
}
以上代码为获取差异方法的核心代码
大家可以下载代码后运行,下面我们展示下,运行代码后获取到的差异代码内容(参数可以是两次commitId,也可以是两个分支,按自己的业务场景来)
1.  
{
2.  
"code": 10000,
3.  
"msg": "业务处理成功",
4.  
"data": [
5.  
{
6.  
"classFile": "com/dr/application/InstallCert",
7.  
"methodInfos": null,
8.  
"type": "ADD"
9.  
},
10.  
{
11.  
"classFile": "com/dr/application/app/controller/Calculable",
12.  
"methodInfos": null,
13.  
"type": "ADD"
14.  
},
15.  
{
16.  
"classFile": "com/dr/application/app/controller/JenkinsPluginController",
17.  
"methodInfos": null,
18.  
"type": "ADD"
19.  
},
20.  
{
21.  
"classFile": "com/dr/application/app/controller/LoginController",
22.  
"methodInfos": [
23.  
{
24.  
"md5": "2C9D2AE2B1864A2FCDDC6D47CEBEBD4C",
25.  
"methodName": "captcha",
26.  
"parameters": "HttpServletRequest request,HttpServletResponse response"
27.  
},
28.  
{
29.  
"md5": "3D6DFADD2171E893D99D3D6B335B22EA",
30.  
"methodName": "login",
31.  
"parameters": "@RequestBody LoginUserParam loginUserParam,HttpServletRequest request"
32.  
},
33.  
{
34.  
"md5": "90842DFA5372DCB74335F22098B36A53",
35.  
"methodName": "logout",
36.  
"parameters": ""
37.  
},
38.  
{
39.  
"md5": "D0B2397D04624D2D60E96AB97F679779",
40.  
"methodName": "testInt",
41.  
"parameters": "int a,char b"
42.  
},
43.  
{
44.  
"md5": "34219E0141BAB497DCB5FB71BAE1BDAE",
45.  
"methodName": "testInt",
46.  
"parameters": "String a,int b"
47.  
},
48.  
{
49.  
"md5": "F9BF585A4F6E158CD4475700847336A6",
50.  
"methodName": "testInt",
51.  
"parameters": "short a,int b"
52.  
},
53.  
{
54.  
"md5": "0F2508A33F719493FFA66C5118B41D77",
55.  
"methodName": "testInt",
56.  
"parameters": "int[] a"
57.  
},
58.  
{
59.  
"md5": "381C8CBF1F381A58E1E93774AE1AF4EC",
60.  
"methodName": "testInt",
61.  
"parameters": "AddUserParam param"
62.  
},
63.  
{
64.  
"md5": "64BF62C11839F45030198A8D8D7821C5",
65.  
"methodName": "testInt",
66.  
"parameters": "T[] a"
67.  
},
68.  
{
69.  
"md5": "D091AB0AD9160407AED4182259200B9B",
70.  
"methodName": "testInt",
71.  
"parameters": "Calculable calc,int n1,int n2"
72.  
},
73.  
{
74.  
"md5": "693BBA0A8A57F2FD19F61BA06F23365C",
75.  
"methodName": "display",
76.  
"parameters": ""
77.  
},
78.  
{
79.  
"md5": "F9DFE0E75C78A31AFB6A8FD46BDA2B81",
80.  
"methodName": "a",
81.  
"parameters": "InnerClass a"
82.  
}
83.  
],
84.  
"type": "MODIFY"
85.  
},
86.  
{
87.  
"classFile": "com/dr/application/app/controller/RoleController",
88.  
"methodInfos": null,
89.  
"type": "ADD"
90.  
},
91.  
{
92.  
"classFile": "com/dr/application/app/controller/TestController",
93.  
"methodInfos": [
94.  
{
95.  
"md5": "B1840C873BF0BA74CB6749E1CEE93ED7",
96.  
"methodName": "getPom",
97.  
"parameters": "HttpServletResponse response"
98.  
},
99.  
{
100.  
"md5": "9CEE68771972EAD613AF237099CD2349",
101.  
"methodName": "getDeList",
102.  
"parameters": ""
103.  
}
104.  
],
105.  
"type": "MODIFY"
106.  
},
107.  
{
108.  
"classFile": "com/dr/application/app/controller/UserController",
109.  
"methodInfos": [
110.  
{
111.  
"md5": "7F2AD08CE732ADDFC902C46D238A9EB3",
112.  
"methodName": "add",
113.  
"parameters": "@RequestBody AddUserParam addUserParam"
114.  
},
115.  
{
116.  
"md5": "D41D8CD98F00B204E9800998ECF8427E",
117.  
"methodName": "get",
118.  
"parameters": ""
119.  
},
120.  
{
121.  
"md5": "2B35EA4FB5054C6EF13D557C2ACBB581",
122.  
"methodName": "list",
123.  
"parameters": "@ApiParam(required = true, name = \"page\", defaultValue = \"1\", value = \"当前页码\") @RequestParam(name = \"page\") Integer page,@ApiParam(required = true, name = \"pageSize\", defaultValue = \"10\", value = \"每页数量\") @RequestParam(name = \"pageSize\") Integer pageSize,@ApiParam(name = \"userId\", value = \"用户id\") @RequestParam(name = \"userId\", required = false) Long userId,@ApiParam(name = \"username\", value = \"用户名\") @RequestParam(name = \"username\", required = false) String username,@ApiParam(name = \"userSex\", value = \"性别\") @RequestParam(name = \"userSex\", required = false) Integer userSex,@ApiParam(name = \"mobile\", value = \"手机号\") @RequestParam(name = \"mobile\", required = false) String mobile"
124.  
}
125.  
],
126.  
"type": "MODIFY"
127.  
},
128.  
{
129.  
"classFile": "com/dr/application/app/controller/view/RoleViewController",
130.  
"methodInfos": null,
131.  
"type": "ADD"
132.  
},
133.  
{
134.  
"classFile": "com/dr/application/app/controller/view/UserViewController",
135.  
"methodInfos": [
136.  
{
137.  
"md5": "9A1DDA3F41B36026FC2F3ACDAE85C1DB",
138.  
"methodName": "user",
139.  
"parameters": ""
140.  
}
141.  
],
142.  
"type": "MODIFY"
143.  
},
144.  
{
145.  
"classFile": "com/dr/application/app/param/AddRoleParam",
146.  
"methodInfos": null,
147.  
"type": "ADD"
148.  
},
149.  
{
150.  
"classFile": "com/dr/application/app/vo/DependencyVO",
151.  
"methodInfos": null,
152.  
"type": "ADD"
153.  
},
154.  
{
155.  
"classFile": "com/dr/application/app/vo/JenkinsPluginsVO",
156.  
"methodInfos": null,
157.  
"type": "ADD"
158.  
},
159.  
{
160.  
"classFile": "com/dr/jenkins/vo/DeviceVo",
161.  
"methodInfos": null,
162.  
"type": "ADD"
163.  
},
164.  
{
165.  
"classFile": "com/dr/jenkins/vo/GoodsVO",
166.  
"methodInfos": null,
167.  
"type": "ADD"
168.  
},
169.  
{
170.  
"classFile": "com/dr/jenkins/vo/JobAddVo",
171.  
"methodInfos": null,
172.  
"type": "ADD"
173.  
},
174.  
{
175.  
"classFile": "com/dr/repository/user/dto/query/RoleQueryDto",
176.  
"methodInfos": null,
177.  
"type": "ADD"
178.  
},
179.  
{
180.  
"classFile": "com/dr/repository/user/dto/query/UserQueryDto",
181.  
"methodInfos": null,
182.  
"type": "ADD"
183.  
},
184.  
{
185.  
"classFile": "com/dr/repository/user/dto/result/MenuDTO",
186.  
"methodInfos": null,
187.  
"type": "ADD"
188.  
},
189.  
{
190.  
"classFile": "com/dr/repository/user/dto/result/RoleResultDto",
191.  
"methodInfos": null,
192.  
"type": "ADD"
193.  
},
194.  
{
195.  
"classFile": "com/dr/repository/user/dto/result/UserResultDto",
196.  
"methodInfos": null,
197.  
"type": "ADD"
198.  
},
199.  
{
200.  
"classFile": "com/dr/user/service/impl/RoleServiceImpl",
201.  
"methodInfos": [
202.  
{
203.  
"md5": "D2AAADF53B501AE6D2206B2951256329",
204.  
"methodName": "getRoleCodeByUserId",
205.  
"parameters": "Long id"
206.  
},
207.  
{
208.  
"md5": "47405162B3397D02156DE636059049F2",
209.  
"methodName": "getListByPage",
210.  
"parameters": "RoleQueryDto roleQueryDto"
211.  
}
212.  
],
213.  
"type": "MODIFY"
214.  
},
215.  
{
216.  
"classFile": "com/dr/user/service/impl/UserServiceImpl",
217.  
"methodInfos": [
218.  
{
219.  
"md5": "D41D8CD989ABCDEFFEDCBA98ECF8427E",
220.  
"methodName": "selectListByPage",
221.  
"parameters": "UserQueryDto userQueryDto"
222.  
}
223.  
],
224.  
"type": "MODIFY"
225.  
}
226.  
]
227.

  1. }

data部分为差异代码的具体内容

将差异代码传递到jaocco

大家可以参考:jacoco增量代码改造

我们只需要找到Report类,加入可选参数

@Option(name = "--diffCode", usage = "input file for diff", metaVar = "<file>") String diffCode;

jacoco如何集成到Kubeletes jacoco教程_java_02

这样,我们就可以在jacoco内部接受到传递的参数了,如果report命令加上--diffCode就计算增量,不加则计算全量,不影响正常功能,灵活性高

我们这里改造了analyze方法,将增量代码塞给CoverageBuilder对象,我们需要用时直接去获取

1.  
2.  
final PrintWriter out) throws IOException {
3.  
CoverageBuilder builder;
4.  
// 如果有增量参数将其设置进去
5.  
if (null != this.diffCode) {
6.  
builder = new CoverageBuilder(this.diffCode);
7.  
} else {
8.  
builder = new CoverageBuilder();
9.  
}
10.  
final Analyzer analyzer = new Analyzer(data, builder);
11.  
for (final File f : classfiles) {
12.  
analyzer.analyzeAll(f);
13.  
}
14.  
printNoMatchWarning(builder.getNoMatchClasses(), out);
15.  
return builder.getBundle(name);
16.  
}

差异代码匹配

jacoco采用AMS类去解析class类,我们需要去修改org.jacoco.core包下面的Analyzer类

1.  
2.  
final long classId = CRC64.classId(source);
3.  
final ClassReader reader = InstrSupport.classReaderFor(source);
4.  
if ((reader.getAccess() & Opcodes.ACC_MODULE) != 0) {
5.  
return;
6.  
}
7.  
if ((reader.getAccess() & Opcodes.ACC_SYNTHETIC) != 0) {
8.  
return;
9.  
}
10.  
// 字段不为空说明是增量覆盖
11.  
if (null != CoverageBuilder.classInfos
12.  
&& !CoverageBuilder.classInfos.isEmpty()) {
13.  
// 如果没有匹配到增量代码就无需解析类
14.  
if (!CodeDiffUtil.checkClassIn(reader.getClassName())) {
15.  
return;
16.  
}
17.  
}
18.  
final ClassVisitor visitor = createAnalyzingVisitor(classId,
19.  
reader.getClassName());
20.  
reader.accept(visitor, 0);
21.   
22.  
}

主要是判断如果需要的是增量代码覆盖率,则匹配类是否是增量的(这里是jacoco遍历解析每个类的地方)

然后修改ClassProbesAdapter类的visitMethod方法(这个是遍历类里面每个方法的地方)

jacoco如何集成到Kubeletes jacoco教程_java_03

整个比较的代码逻辑在这里,注释写的你叫详细了

jacoco如何集成到Kubeletes jacoco教程_java_04

修改完成后,大家只要构建出org.jacoco.cli-0.8.7-SNAPSHOT-nodeps.jar包,然后report时传入增量代码即可

全量报告

jacoco如何集成到Kubeletes jacoco教程_List_05

增量报告

jacoco如何集成到Kubeletes jacoco教程_java_06

所遇到问题


  • 差异方法的参数匹配

由于我们使用javaparser解析出的参数格式为String a,int b

而ASM解析出的 为Ljava/lang/String,I;在匹配参数的时候遇到了问题,最终我找到了Type类的方法

Type.getArgumentTypes(desc)

然后

argumentTypes[i].getClassName()

将AmS的参数解析成String,int(做了截取),然后再去匹配,就能正确匹配到参数的格式了

  • 为什么不将整个生成报告做成一个平台

jacoco生成报告的时候,需要传入源码,编译后的class文件,而编译这些东西我们一般都有自己的ci平台去做,我们可以将我们的覆盖率功能集成到我们的devops平台,从那边去获取源码或编译出的class文件,而且可以做业务上的整合,所以没有像supper-jacoco那样做成一个平台