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:指令全覆蓋,不代表分支全覆蓋!
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-插樁方式
上圖包含了幾種不同的收集覆蓋率信息的方法,每個方法的實現都不太一樣,這里主要關心字節碼注入這種方式(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大致流程:
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 |
重點看下步驟3:
當服務啟動的時候,容器的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 { |
3.2-獲取代碼差異
JGit 是一個用 Java 寫成的功能比較健全的 Git 的實現,它在 Java 社區中被廣泛使用。在這一步的主要流程是獲取基線提交與被測提交之間的差異代碼,然后過濾一些需要排除的文件(比如非 Java 文件、測試文件等等),對剩余文件進行解析,將變更代碼解析到方法緯度,部分代碼片段如下:
private List<AnalyzeRequest> findDiffClasses(IcovRequest request) throws GitAPIException, IOException { |
3.3-差異代碼解析
JaCoCo默認的注入方式為全量注入。通過閱讀源碼,發現注入的邏輯主要在ClassProbesAdapter中。ASM在遍歷字節碼時,每次訪問一個方法定義,都會回調這個類的visitMethod方法 ,在visitMethod方法中再調用ClassProbeVisitor的visitMethod方法,並最終調用MethodInstrumenter完成注入。部分代碼片段如下:
@Override |
如何去修改JaCoCo的源碼?繼承原有的ClassInstrumenter和ClassProbesAdapter,修改其中的visitMethod方法,只對變化了方法進行注入:
@Override |
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數組,下面是原始的控制流圖,以及插樁完成的控制流圖。
可以看出,探針的位置位於分支后
由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 |
![]() |
![]() |
如果是簡單序列,則將探針簡單地插入兩個指令之間。 |
JUMP (unconditional) |
![]() |
![]() |
由於在任何情況下都執行無條件跳轉,因此我們也可以在GOTO指令之前插入探針。 |
JUMP (conditional) |
![]() |
![]() |
向條件跳轉添加探針會比較棘手。我們反轉操作碼的語義,並在條件跳轉之后立即添加探測。 |
EXIT |
![]() |
![]() |
正如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的專利