一、Jacoco简介
Jacoco是专门用来统计单元测试覆盖率和集成测试覆盖率的十分常用的工具。
二、Jacoco插桩
主流代码覆盖率工具都采用字节码插桩模式,通过钩子的方式来记录代码执行轨迹信息。其中字节码插桩又分为编译时插桩和运行时插桩,分别对应Offline模式和On-the-fly模式 。On-The-Fly模式优点在于无需修改源代码,可以在系统不停机的情况下,实时收集代码覆盖率信息。Offine模式优点在于系统启动不需要额外开启代理,但是只能在系统停机的情况下才能获取代码覆盖率
On-The-Fly插桩:Java Agent
(1)JVM中通过 -javaagent 参数,指定特定的jar文件启动Instrumentation的代理程序
(2)代理程序在每装载一个class文件前,判断是否已经转换修改了该文件,如果没有则需要将探针插入class文件中
(3)代码覆盖率就可以在JVM执行代码的时候实时获取
(4)典型代表:Jacoco
On-The-Fly插桩:Class Loader
(1)自定义classloader实现自己的类装载策略,在类加载之前将探针插入class文件中
(2)典型代表:Emma
Offine插桩
(1)在测试之前先对文件进行插桩,生成插过桩的class文件或者jar包。执行插过桩的class文件或者jar包之后,会生成覆盖率信息到文件。最后统一对覆盖率信息进行处理,并生成报告。
(2)Offline插桩又分为两种:
1》Replace:修改字节码生成新的class文件
2》Inject:在原有字节码文件上进行修改
(3)典型代表:Cobertura
On-The-Fly和Offine比较
(1)On-The-Fly模式更加方便的获取代码覆盖率,无需提前进行字节码插桩,可以实时获取代码覆盖率信息
(2)Offline模式适用于以下场景:
运行环境不支持java agent
部署环境不允许设置JVM参数
字节码需要被转换成其他虚拟机字节码,如Android Dalvik VM
动态修改字节码过程中和其他agent冲突
无法自定义用户加载类
2.1 插桩前准备工作
- 下载jacoco.jar,官网地址https://www.eclemma.org/jacoco/
- 将jacoco.jar解压,cd到jacoco项目的lib目录下。jacocoagent.jar 就是启动应用时主要用来插桩的jar包
⚠️:请注意不要写错名称,里面有个很像的jacocoant.jar,这个jar包是用ant xml方式操作jacoco时使用的,不要混淆
2.2 不同部署方式实现插桩
包括:Ant插件启动、Maven插件启动、java -jar启动、tomcat启动war包
方式一:java -jar启动
java -javaagent:/Users/sundongping/Downloads/jacoco-0.8.5/lib/jacocoagent.jar=includes=*,output=tcpserver,port=2014,address=127.0.0.1 -jar ./target/jacocotest-1.0-SNAPSHOT.jar
后台启动需要使用 nohup … & 命令
nohup java -javaagent:/Users/sundongping/Downloads/jacoco-0.8.5/lib/jacocoagent.jar=includes=*,output=tcpserver,port=2014,address=127.0.0.1 -jar ./target/jacocotestmaven-1.0-SNAPSHOT.jar&
-
javaagent
jdk5之后新增的参数,主要用来在运行jar包的时候,以一种方式介入字节码加载过程,如有兴趣自行百度。注意后面有个冒号: -
/Users/sundongping/Downloads/jacoco-0.8.5/lib/jacocoagent.jar
需要用来介入class文件加载过程的jar包。是jacocoagent.jar包的绝对路径 -
includes=*
代表启动时需要进行字节码插桩的包的过滤,*代表所有的class文件加载都需要进行插桩。假如你们公司内部代码都有相同的包缀:com.mycompany
你可以写成:includes=com.mycompany.*
-
output=tcpserver
支持file、tcpserver、tcpclient等参数值 -
port=2014
这是jacoco开启的tcpserver的端口,请注意这个端口不能被占用 -
address=127.0.0.1
这是对外开放的tcpserver的访问地址。可以配置127.0.0.1,也可以配置为实际访问ip。配置为127.0.0.1的时候,dump数据只能在这台服务器上进行dump,就不能通过远程方式dump数据。配置为实际的ip地址的时候,就可以在任意一台机器上(前提是ip要通,不通都白瞎),通过ant xml或者api方式dump数据。举个栗子:
我如果配置了192.168.110.1:2014作为jacoco的tcpserver启动服务,那么可以在任意一台机器上进行数据的dump,比如在我本机mac上用api或者xml方式调用dump;如果配置了127.0.0.1:2014作为启动服务器,那么只能在这台测试机上进行dump,其他的机器都无法连接到这个tcpserver进行dump
总结:
格式是固定的,只有括号内的东西方可改变,其它尽量不要动,连空格都不要多
-javaagent:(/home/admin/jacoco/jacocoagent.jar)=includes=(*),output=tcpserver,port=(2014),address=(127.0.0.1)
方式二:外部tomcat启动war包
修改项目的pom.xml文件
<?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>jacocotesttomcat</groupId>
<artifactId>jacocotesttomcat</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- packaging是打包标签,默认是打成jar包,打war包需要将标签值改成war-->
<packaging>war</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 不使用内嵌tomcat的方式一:移除嵌入式tomcat插件 springboot内嵌tomcat服务器,所以当要使用外部的tomcat时需要先去除内置tomcat-->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 不使用内嵌tomcat的方式二:<scope>provided</scope>表示在编译和测试时使用(不加它,打的包中会指定tomcat,用tomcat部署时会因tomcat版本报错;而加上它,打包时不会把内置的tomcat打进去)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!--使用jacoco对web工程生成全部的覆盖率报告-->
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>org.jacoco.core</artifactId>
<version>0.8.5</version>
</dependency>
<dependency>
<groupId>org.jacoco</groupId>
<artifactId>org.jacoco.report</artifactId>
<version>0.8.5</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在服务器tomcat安装路径的bin目录下,找到catalina.sh。打开catalina.sh,找到合适的地方修改JAVA_OPTS参数
将项目war包放到tomcat的webapps目录下(tomcat会自动将war包解压)
启动tomcat sudo sh ./startup.sh
,使用命令 ps aux|grep tomcat
查看tomcat进程。若进程信息中包含javaagent的内容表示插桩成功
方式三:Maven插件启动
maven项目启动的命令
mvn clean install
mvn tomcat7:run -Dport=xxx
或
mvn clean install
mvn spring-boot:run -Dport=xxx
这两套命令,本质上没什么差别,只是运行插件不一样。在当前代码的pom文件层级运行,意思是通过maven的tomcat插件启动这个服务。这个服务启动在端口xxxx上,注意这个端口是应用的访问端口,和jacoco的那个端口不是一回事
maven实现jacoco插桩的命令
export MAVEN_OPTS="-javaagent:$jacocoJarPath=includes=*,output=tcpserver,port=2014,address=127.0.0.1"
这句命令加在哪里呢?就是run之前。因为这样一改,你的所有的mvn命令都会生效,但其实我们只想介入启动过程
因此,前面提到的两套启动命令,就可以改成如下方式:
mvn clean install
export MAVEN_OPTS="-javaagent:$jacocoJarPath=includes=*,output=tcpserver,port=2014,address=127.0.0.1"
mvn tomcat7:run -Dport=xxx
export MAVEN_OPTS=""
或
mvn clean install
export MAVEN_OPTS="-javaagent:$jacocoJarPath=includes=*,output=tcpserver,port=2014,address=127.0.0.1"
mvn spring-boot:run -Dport=xxx
export MAVEN_OPTS=""
最后修改为"",是因为担心对后续的mvn命令产生影响。其实如果你切换了terminal窗口,这个临时变量就会失效,不会对环境造成污染。如果应用启动成功了,可以使用netstat判别一下tcp服务是否真的启动
方式四:Ant插件启动
配置build.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project name="Jacoco" xmlns:jacoco="antlib:org.jacoco.ant" default="jacoco">
<property name="jacocoantPath" value="/Users/sundongping/Downloads/jacoco-0.8.5/lib/jacocoant.jar"/>
<property name="integrationJacocoexecPath" value="./jacoco-integration.exec"/>
<property name="reportfolderPath" value="/opt/app/mskyprocess/jacoco/file/jacocoReport/"/>
<property name="checkOrderSrcpath" value="/Users/sundongping/IdeaProjects/jacocotestmaven/dirtest/src/main/java" />
<property name="checkOrderClasspath" value="/Users/sundongping/IdeaProjects/jacocotestmaven/dirtest/target/classes" />
<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
<classpath path="${jacocoantPath}" />
</taskdef>
<target name="dump">
<jacoco:dump address="127.0.0.1" port="2014" reset="true" destfile="${integrationJacocoexecPath}" append="false"/>
</target>
<target name="clean">
<!-- 清空文件 -->
<delete dir="/opt/app/mskyprocess/jacoco/file/jacocoReport/it_coverage" />
</target>
<target name="report">
<jacoco:report>
<executiondata>
<file file="${integrationJacocoexecPath}" />
</executiondata>
<structure name="JaCoCo Report">
<group name="Check qaportal related">
<classfiles>
<fileset dir="${checkOrderClasspath}"/>
</classfiles>
<sourcefiles encoding="gbk">
<fileset dir="${checkOrderSrcpath}"/>
</sourcefiles>
</group>
</structure>
<html destdir="${reportfolderPath}" encoding="utf-8" />
</jacoco:report>
</target>
</project>
参考的其他人的build.xml(很多注释掉的生成报告的路径,是不用jenkins的时候可以本地生产html使用的)
<?xml version="1.0" encoding="UTF-8"?>
<project name="Jacoco" default="jacoco" xmlns:jacoco="antlib:org.jacoco.ant">
<!--Jacoco的安装路径-->
<property name="jacocoantPath" value="/opt/app/ant/apache-ant-1.10.5/jacocoant.jar"/>
<!-- <property name="integrationJacocoexecPath" value="./jacoco-integration.exec"/>-->
<!--最终生成.exec文件的路径,Jacoco就是根据这个文件生成最终的报告的-->
<property name="jacocoexecPath" value="/opt/app/jenkins/workspace/Jacoco_umegateway02/gateway/target/jacoco.exec"/>
<!--生成覆盖率报告的路径-->
<!-- <property name="reportfolderPath" value="/opt/app/apache-ant-1.10.5/coverage_ant_task/report/"/>-->
<!--远程tomcat服务的ip地址-->
<property name="server_ip" value="10.221.159.8"/>
<!--前面配置的远程tomcat服务打开的端口,要跟上面配置的一样-->
<property name="server_port" value="8044"/>
<!--源代码路径-->
<!-- <property name="checkOrderSrcpath" value="/data/Ume/umegateway/gateway/src/main/java" />-->
<!--.class文件路径-->
<!-- <property name="checkOrderClasspath" value="/data/Ume/umegateway/gateway/target/classes" />-->
<!--让ant知道去哪儿找Jacoco-->
<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
<classpath path="${jacocoantPath}" />
</taskdef>
<!--dump任务:
根据前面配置的ip地址,和端口号,
访问目标tomcat服务,并生成.exec文件。-->
<target name="dump">
<jacoco:dump address="${server_ip}" reset="false" destfile="${jacocoexecPath}" port="${server_port}" append="true"/>
</target>
<!--jacoco任务:
根据前面配置的源代码路径和.class文件路径,
根据dump后,生成的.exec文件,生成最终的html覆盖率报告。-->
<target name="report">
<!-- <delete dir="${reportfolderPath}" />-->
<!-- <mkdir dir="${reportfolderPath}" />-->
<jacoco:report>
<executiondata>
<file file="${jacocoexecPath}" />
</executiondata>
<!-- <structure name="JaCoCo Report">-->
<!-- <group name="Check qaportal related">-->
<!-- <classfiles>-->
<!-- <fileset dir="${checkOrderClasspath}" />-->
<!-- </classfiles>-->
<!-- <sourcefiles encoding="gbk">-->
<!-- <fileset dir="${checkOrderSrcpath}" />-->
<!-- </sourcefiles>-->
<!-- </group>-->
<!-- </structure>-->
<html destdir="${reportfolderPath}" encoding="utf-8" />
</jacoco:report>
</target>
</project>
参考博文:
关于Jacoco的小结和踩坑记录
Jacoco Code Coverage
JaCoCo在Tomcat服务器上监控代码覆盖率的使用方法
https://www.cnblogs.com/dingtian/p/7754079.html