使用jacoco+ant实现服务器上spring boot项目的代码覆盖率
目录
四、通过jacoco+ant为部署在服务器上的项目生成覆盖率报告
一、前言
由于公司的项目需要用到代码覆盖率,这两天花时间研究了一下,参考了很多篇博客,现在整理一下把我的理解分享给大家。
二、概述
看一圈下来,目前比较使用较多的代码覆盖率工具为jacoco,它包含了多种尺度的覆盖率计数器,可以计算出指令、分支、圈复杂度、类、方法、行的覆盖率(下文分析报告时会详细介绍),非常全面。根据公司需求需要实现本地的代码覆盖率以及服务器上正在运行的项目的代码覆盖率,接下来本文将会详细介绍两种方式的实现步骤。
三、本地实现代码覆盖率
(一)、IDEA上的coverage插件
点击idea编辑器Debug按钮旁边的Run ‘xxx’ with Coverage按钮启动项目。
如果没有该按钮,进入settings查看插件是否已经安装
测试用例执行完后,结束运行,IDEA会弹出这么一个小框框用来查看代码覆盖率报告,也可以通过旁边的按钮生成网页版的代码覆盖率报告。
特别说明:此方式使用的是IDEA默认的coverage工具,并不是jacoco,因此只能显示类、方法以及行覆盖率。可以通过Run按钮左边的下拉菜单中进入Run/Debug Configurations设置,点击Code Coverage面板选择以jacoco 的方式启动,我选择jacoco的时候启动报错了,后续没有深入研究这个问题,找时间再解决,如果觉得这个覆盖率不够全面的大家可以选择下一种方式。
另外,经过测试发现只有启动单元测试才能生成覆盖率数据,启动主类然后使用postman调用接口并不能得到覆盖率结果。
(二)、使用maven工程配置jacoco
此种方式本人没有试验过,附上一篇比较好理解的博客,传送门:https://my.oschina.net/wangmengjun/blog/974067
四、通过jacoco+ant为部署在服务器上的项目生成覆盖率报告
(一)、安装软件
- jacoco
- ant
安装步骤:
1.1 jacoco安装
jacoco需安装在部署项目的服务器上,我是在官网下载的0.8.2最新版zip包。官网下载地址:https://www.eclemma.org/jacoco/
把下载的zip包上传到linux服务器上(路径自定),使用unzip工具将zip解压,解压完毕后就算安装成功了。解压命令(先进到存放jacoco-0.8.2.zip的目录下):unzip jacoco-0.8.2.zip
注:如果提示unzip命令不存在,请使用yum命令自行安装。
1.2 ant安装
ant是基于java的,因此需要先安装好java环境。使用java -version命令查看java是否安装,没安装的童靴自己百度安装把,本文就不介绍了。ant也是在官网下载的1.10.5最新版,地址:https://ant.apache.org/bindownload.cgi
ant可安装在部署项目的服务器上,也可安装在其他机器上,只要后面参数中ip地址配置正确就行,本人此处是安装在同一服务器上,因此还是使用unzip工具解压安装,解压完毕后还需要配置环境变量。
任意目录下输入:vim ~/.bashrc命令打开文件设置用户环境变量,按i或者insert键进入编辑模式,在文件最后一行插入以下环境变量信息,注意修改ANT_HOME为自己对应的ant安装目录。
ANT_HOME=/root/ant/apache-ant-1.10.5
PATH=$JAVA_HOME/bin:$ANT_HOME/bin:$PATH
export JAVA_HOME JAVA_BIN ANT_HOME PATH CLASSPATH
输入之后按esc键退出编辑模式,shift+:开启命令行,输入wq回车保存退出。单独输入q回车为退出不保存。
任意目录下输入:source ~/.bashrc并执行命令,以启用刚才配置的环境变量。
(二)、使用javaagent方式启动项目
接下来启动spring boot项目,启动命令和平时使用的有所不同,命令如下:
nohup java -javaagent:/root/jacoco/lib/jacocoagent.jar=includes=*,output=tcpserver,port=8091,address=* -jar xxxx.jar&
可以看出命令中比平时多出了一串以-javaagent开头的配置,注意这中间不能有空格,否则会启动失败。后面跟着的/root/jacoco/lib/jacocoagent.jar是jacoco的安装路径中的jacocoagent.jar包的路径,includes配置包含在执行分析中的类名列表,我分析全部,因此使用*通配符。可以只分析一个包下的代码,也可以使用excludes排除不想要的包。output指的是用于写入覆盖数据的输出方法,使用tcpserver代理侦听由address和
port
属性指定的TCP端口,并将执行的数据写入此TCP连接,从而实现不停止项目运行实时生成代码覆盖率报告。
port为端口号,需要明确的一点就是这个端口号不是项目的yml配置文件中配置的server.port端口号,不要搞混淆了,选择一个未被占用的即可,Linux查看端口号是否被占用命令:lsof -i:端口号 如果提示-bash: lsof: command not found,老规矩,yum命令安装:yum install -y lsof。address配置连接的ip地址,可以简单理解为之前安装ant的机器的ip地址。*号则表示此代理接受任何本地的连接,也就是说同一局域网下都可以。
其他属性配置请参照官方文档:https://www.eclemma.org/jacoco/trunk/doc/agent.html
注意:在tcpserver
和 tcpclient
模式下打开的端口和连接以及JMX接口不提供任何身份验证机制。如果在生产环境上运行jacoco,请确保没有不受信任的源可以访问TCP服务器端口,或者jacoco TCP客户端仅连接到受信任的目标。否则可能会泄露应用程序的内部信息或者可能发生DOS攻击。
(三)、上传源码到服务器上
将项目的源代码以及编译好的.class文件上传到服务器上,待会配置ant任务的时候需要配置他们的路径。
此处有个问题,我在官网的常见问题处看到
重点是——“请确保在运行时使用与报告生成完全相同的类文件”。也就是说,如果代码有更改并且重新发布了项目,要及时更新一下源代码,不然有可能出现数据不正确的情况(有待验证,最好按照官网说的做)。
(四)、配置ant任务的xml文件
在本地创建jacocoant.xml文件,配置以下信息:
-
<?xml version="1.0" encoding="UTF-8"?>
-
<project name="JaCoCo" default="run" xmlns:jacoco="antlib:org.jacoco.ant">
-
<!--Jacoco的安装路径-->
-
<property name="jacocoAntPath" value="/root/jacoco/lib/jacocoant.jar"/>
-
<!--生成.exec文件的路径,Jacoco就是根据这个文件生成HTML报告的-->
-
<property name="jacocoExecPath" value="/home/jacocodata/jacocoExec"/>
-
<!--生成覆盖率报告的路径-->
-
<property name="jacocoReportPath" value="/home/jacocodata/jacocoReport"/>
-
<!--远程服务的ip地址 -->
-
<property name="server_ip" value="127.0.0.1"/>
-
<!--前面javaagent配置的远程服务打开的端口,要跟上面配置的一样-->
-
<property name="server_port_teaching" value="8457"/>
-
<property name="server_port_user" value="8456"/>
-
-
<!--源代码路径-->
-
<!--teaching项目-->
-
<property name="teaching_webapiSrcPath" value="/home/jacocodata/jacocoSrc/domain-teaching/domain-teaching-webapi/src/main/java"/>
-
<property name="teaching_serviceSrcPath" value="/home/jacocodata/jacocoSrc/domain-teaching/domain-teaching-service/src/main/java"/>
-
<property name="teaching_daoSrcPath" value="/home/jacocodata/jacocoSrc/domain-teaching/domain-teaching-dao/src/main/java"/>
-
-
<!--user项目-->
-
<property name="user_webapiSrcPath" value="/home/jacocodata/jacocoSrc/domain-user/domain-user-webapi/src/main/java"/>
-
<property name="user_serviceSrcPath" value="/home/jacocodata/jacocoSrc/domain-user/domain-user-service/src/main/java"/>
-
<property name="user_daoSrcPath" value="/home/jacocodata/jacocoSrc/domain-user/domain-user-dao/src/main/java"/>
-
-
<!--.class文件路径-->
-
<!--teaching项目-->
-
<property name="teaching_webapiClassesPath" value="/home/jacocodata/jacocoSrc/domain-teaching/domain-teaching-webapi/target/classes"/>
-
<property name="teaching_serviceClassesPath" value="/home/jacocodata/jacocoSrc/domain-teaching/domain-teaching-service/target/classes"/>
-
<property name="teaching_daoClassesPath" value="/home/jacocodata/jacocoSrc/domain-teaching/domain-teaching-dao/target/classes"/>
-
-
<!--user项目-->
-
<property name="user_webapiClassesPath" value="/home/jacocodata/jacocoSrc/domain-user/domain-user-webapi/target/classes"/>
-
<property name="user_serviceClassesPath" value="/home/jacocodata/jacocoSrc/domain-user/domain-user-service/target/classes"/>
-
<property name="user_daoClassesPath" value="/home/jacocodata/jacocoSrc/domain-user/domain-user-dao/target/classes"/>
-
-
<!--让ant知道去哪儿找Jacoco-->
-
<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
-
<classpath path="${jacocoAntPath}"/>
-
</taskdef>
-
-
<target name="run">
-
<echo message="start..."/>
-
<echo message="dump..."/>
-
<antcall target="dump"/>
-
<echo message="merge..."/>
-
<antcall target="merge"/>
-
<echo message="report..."/>
-
<antcall target="report"/>
-
<echo message="end..."/>
-
</target>
-
-
<!--dump任务:
-
根据前面配置的ip地址,和端口号,访问目标服务,并生成.exec文件。
-
reset=true时,会在dump出exec文件后,清空覆盖率数据;
-
append=false时,dump出的exec文件会覆盖原有的exec文件;append=true时,dump出的exec文件
-
追加至原有的exec文件;
-
-->
-
<target name="dump">
-
<jacoco:dump address="${server_ip}" reset="false" destfile="${jacocoExecPath}/jacoco_teaching_webapi.exec" port="${server_port_teaching}" append="true"/>
-
<jacoco:dump address="${server_ip}" reset="false" destfile="${jacocoExecPath}/jacoco_teaching_service.exec" port="${server_port_teaching}" append="true"/>
-
<jacoco:dump address="${server_ip}" reset="false" destfile="${jacocoExecPath}/jacoco_teaching_dao.exec" port="${server_port_teaching}" append="true"/>
-
<jacoco:dump address="${server_ip}" reset="false" destfile="${jacocoExecPath}/jacoco_user_webapi.exec" port="${server_port_user}" append="true"/>
-
<jacoco:dump address="${server_ip}" reset="false" destfile="${jacocoExecPath}/jacoco_user_service.exec" port="${server_port_user}" append="true"/>
-
<jacoco:dump address="${server_ip}" reset="false" destfile="${jacocoExecPath}/jacoco_user_dao.exec" port="${server_port_user}" append="true"/>
-
</target>
-
-
<target name="merge">
-
<jacoco:merge destfile="${jacocoExecPath}/merged.exec">
-
<fileset dir="${jacocoExecPath}" includes="*.exec"/>
-
</jacoco:merge>
-
</target>
-
-
<!--jacoco任务:
-
根据前面配置的源代码路径和.class文件路径,
-
dump后生成的.exec文件,生成最终的html覆盖率报告。-->
-
<target name="report">
-
<jacoco:report>
-
<executiondata>
-
<file file="${jacocoExecPath}/merged.exec"/>
-
</executiondata>
-
-
<structure name="JaCoCo Report">
-
<!--group name 对应生成的报告中的列表名-->
-
<group name="teaching webapi">
-
<sourcefiles encoding="UTF-8">
-
<fileset dir="${teaching_webapiSrcPath}"/>
-
</sourcefiles>
-
<classfiles>
-
<fileset dir="${teaching_webapiClassesPath}"/>
-
</classfiles>
-
</group>
-
<group name="teaching dao">
-
<sourcefiles encoding="UTF-8">
-
<fileset dir="${teaching_daoSrcPath}"/>
-
</sourcefiles>
-
<classfiles>
-
<fileset dir="${teaching_daoClassesPath}"/>
-
</classfiles>
-
</group>
-
<group name="teaching service">
-
<sourcefiles encoding="UTF-8">
-
<fileset dir="${teaching_serviceSrcPath}"/>
-
</sourcefiles>
-
<classfiles>
-
<fileset dir="${teaching_serviceClassesPath}"/>
-
</classfiles>
-
</group>
-
-
<group name="user webapi">
-
<sourcefiles encoding="UTF-8">
-
<fileset dir="${user_webapiSrcPath}"/>
-
</sourcefiles>
-
<classfiles>
-
<fileset dir="${user_webapiClassesPath}"/>
-
</classfiles>
-
</group>
-
<group name="user service">
-
<sourcefiles encoding="UTF-8">
-
<fileset dir="${user_serviceSrcPath}"/>
-
</sourcefiles>
-
<classfiles>
-
<fileset dir="${user_serviceClassesPath}"/>
-
</classfiles>
-
</group>
-
<group name="user dao">
-
<sourcefiles encoding="UTF-8">
-
<fileset dir="${user_daoSrcPath}"/>
-
</sourcefiles>
-
<classfiles>
-
<fileset dir="${user_daoClassesPath}"/>
-
</classfiles>
-
</group>
-
</structure>
-
-
<html destdir="${jacocoReportPath}" encoding="utf-8"/>
-
<csv destfile="${jacocoReportPath}/report.csv"/>
-
<xml destfile="${jacocoReportPath}/report.xml"/>
-
</jacoco:report>
-
</target>
-
</project>
由于公司使用了spring cloud,目前有两个微服务项目teaching和user,所以需要打开不同的两个tcp端口,而每个项目都有4个子项目(简单来说就是有5个pom.xml文件),需要生成代码覆盖率报告的每个项目有3个子项目需要(也就是domain那层不需要配置)。配置文件修改完毕后,上传至ant所在的服务器上,此时一切准备就绪,可以执行ant任务了。
执行命令:ant -f jacocoant.xml 出现BUILD SUCCESSFUL表示执行成功,报告已经生成在配置的路径中了。
如果提示ant命令找不到,请检查设置环境变量是ant安装路径是否填写正确。如果dump失败,请检查源码和class文件路径是否在jacocoant.xml中配置正确。
五、jacoco report报告解读
将生成的报告文件夹从服务器上复制下来,下图是jacoco report文件夹中的所有内容。
双击打开index.xml文件,就可以在浏览器中查看代码覆盖率报告啦。
一堆数字加一堆英文,看起来很是头疼,那么它们分别代表什么含义呢,接下来就详细的和大家说说怎么看懂这份覆盖率报告。
Elment对应jacocoant.xml文件中配置的group name属性,也就是我项目中的一个子项目,点进去可以看到所有的包,再点击包可以看到里面的类文件,点击类进入到方法列表,方法还可以点,点了会直接进入到代码当中,代码会有颜色并高亮显示,分支代码的左边还会有一颗带颜色的钻石(具体颜色含义在下文描述),如下图所示:
Instructions、Branches、Cxty、Lines、Methods、Classes分别为指令、分支、圈复杂度、行、方法、类。
Instructions:
jacoco计算的最小单位就是字节码指令。指令覆盖率表明了在所有的指令中,哪些被指令过以及哪些没有被执行。这项指数完全独立于源码格式并且在任何情况下有效,不需要类文件的调试信息。
Branches:
jacoco对所有的if和switch指令计算了分支覆盖率。这项指标会统计所有的分支数量,并同时支出哪些分支被执行,哪些分支没有被执行。这项指标也在任何情况都有效。异常处理不考虑在分支范围内。
在有调试信息的情况下,代码分支的左边会有一个带颜色的钻石,绿色全部分支被执行,红色全部分支未执行,黄色钻石部分分支被执行。
Cyclomatic Complexity(Cxty):
jacoco会计算每个非抽象方法的圈复杂度,并且也会计算类,包,组的复杂度。根据McCabe1996的定义,圈复杂度可以理解为覆盖所有的可能情况最少使用的测试用例数。这项参数也在任何情况下有效。
Lines:
该项指数在有调试信息的情况下计算。因为每一行代码可能会产生若干条字节码指令,因此使用红色背景表示该行指令未被执行,黄色背景表示该行部分指令未被执行,绿色背景则表示该行指令全部执行。
Medhods:
每一个非抽象方法都至少有一条指令。若一个方法至少被执行了一条指令,就认为它被执行过。因为jacoco直接对字节码进行操作,所以有些方法没有在源码显示(比如某些构造方法和由编译器自动生成的方法)也会被计入在内。
Classes:
每个类中只要有一个方法被执行,这个类就被认定为被执行。同medhods一样,有些没有在源码声明的方法被执行,也认定该类被执行。
Missed表示未被覆盖到,Cov是coverage的缩写,意思是代码覆盖率。第二栏和第三栏的指令覆盖率和分支覆盖率都是进度条的显示形式,红色未覆盖绿色已覆盖,后面跟着覆盖率百分比,从total那一栏可以看出,总共有88924条指令,86919条未执行,因此覆盖率为2%。后面的以此类推,jacoco代码覆盖率报告就可以完全看懂了。
六、总结
此次研究代码覆盖率主要是为了实现功能,应用到公司的项目上,因此很多地方以及原理方面理解的不够深入,一些属性的不同配置也没有去尝试。之所以写这篇博客,是因为在看其他博主写的博客的时候总有一两个疑惑的点,让我感到无从下手,不同的博客看多越多,疑惑就越多,于是产生了写一篇尽量将实现步骤和配置过程描述得较为清楚的博客,让后来人少走弯路,同时也能加深自己的印象。
如果大家有发现什么错误的地方,欢迎评论留言指出,谢谢。