Jacoco項目首頁:http://www.eclemma.org/jacoco/
本文位址:JacocoAnalyse
Content
- 調用的開源架構
- Ant
- ASM
- 測試源碼對比
- 插入前源碼
- 插入後源碼
- 插入前位元組碼
- 插入後位元組碼
- 兩種插樁模式
- 插樁方式
- Offline
- On-the-fly
- 比較
- 關于switch插樁分析
- TableSwitch
- Lookupswitch
- Report生成
- Task report
- Element executiondata
- Element structure
- Element xml
- Element check
- 插樁政策
- 源碼分析
- 插樁
- 生成報告
- 參考
調用的開源架構
Ant
開發文檔:http://www.jacoco.org/jacoco/trunk/doc/ant.html
ASM
項目首頁:http://asm.ow2.org/
ASM分析:ASM Analyse
測試源碼對比
插入前源碼
public class Hello
{
public Hello()
{
int rand = (int)(Math.random() * D);
if (rand % == )
System.out.println("Hi,0");
else {
System.out.println("Hi,1");
}
System.out.println("End");
}
}
public class HelloTest
{
public static void main(String[] args)
{
Hello h = new Hello();
}
}
插入後源碼
public class Hello
{
public Hello()
{
arrayOfBoolean[] = true;
int rand = (int)(Math.random() * D);
if (rand % == ) { arrayOfBoolean[] = true;
System.out.println("Hi,0"); arrayOfBoolean[] = true;
} else {
System.out.println("Hi,1"); arrayOfBoolean[] = true;
}
System.out.println("End");
arrayOfBoolean[] = true;
}
}
public class HelloTest
{
public HelloTest()
{
arrayOfBoolean[] = true;
}
public static void main(String[] arg0) {
boolean[] arrayOfBoolean = $jacocoInit(); Hello h = new Hello();
arrayOfBoolean[] = true;
}
}
插入前位元組碼
F:\Jacoco\target\classes>javap -c Hello
Compiled from "Hello.java"
public class Hello {
public Hello();
Code:
: aload_0
: invokespecial #1 // Method java/lang/Object."<init>":()V
: invokestatic #2 // Method java/lang/Math.random:()D
: ldc2_w #3 // double 100.0d
: dmul
: d2i
: istore_1
: iload_1
: iconst_2
: irem
: ifne
: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc #6 // String Hi,0
: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: goto
: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc #8 // String Hi,1
: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc #9 // String End
: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: return
}
插入後位元組碼
F:\Jacoco\target\classes-instr>javap -c Hello
Compiled from "Hello.java"
public class Hello {
public Hello();
Code:
: invokestatic #49 // Method $jacocoInit:()[Z
: astore_1
: aload_0
: invokespecial #1 // Method java/lang/Object."<init>":()V
: aload_1
: iconst_0
: iconst_1
: bastore
: invokestatic #2 // Method java/lang/Math.random:()D
: ldc2_w #3 // double 100.0d
: dmul
: d2i
: istore_2
: iload_2
: iconst_2
: irem
: ifne
: aload_1
: iconst_1
: iconst_1
: bastore
: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc #6 // String Hi,0
: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: aload_1
: iconst_2
: iconst_1
: bastore
: goto
: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc #8 // String Hi,1
: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: aload_1
: iconst_3
: iconst_1
: bastore
: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc #9 // String End
: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: aload_1
: iconst_4
: iconst_1
: bastore
: return
}
兩種插樁模式
插樁方式
Offline
先對位元組碼檔案進行插樁,然後執行插樁後的位元組碼檔案,生成覆寫資訊并導出報告。
On-the-fly
JVM通過-javaagent參數指定特定的jar檔案啟動Instrumentation代理程式,代理程式在裝載class檔案前判斷是否已經轉換修改了該檔案,若沒有則将探針(統計代碼)插入class檔案,最後在JVM執行測試代碼的過程中完成對覆寫率的分析。
比較
- On-the-fly更加友善擷取代碼覆寫率
- 無需提前進行位元組碼插樁
- 無需停機(Offline需要停機),可以實時擷取覆寫率
- Offline無需額外開啟代理
- Offline使用場景(From Jacoco Documentation)
- 運作環境不支援Java Agent
- 部署環境不允許設定JVM參數
- 位元組碼需要被轉換成其他虛拟機位元組碼,如Android Dalvik Vm
- 動态修改位元組碼檔案和其他Agent沖突
- 無法自定義使用者加載類
關于switch插樁分析
JVM分析:JVM Analyse
TableSwitch
源碼:
public void testSwitch(int i){
switch(i) {
case :
System.out.println("1");
break;
case :
System.out.println("2");
break;
case :
System.out.println("3");
break;
case :
System.out.println("4");
break;
case :
System.out.println("10");
break;
default:
System.out.println("...");
break;
}//switch
}
插入後反編譯Java源碼:
public void testSwitch(int arg1) {
boolean[] arrayOfBoolean = $jacocoInit();
switch (i)
{
case :
System.out.println("1");
arrayOfBoolean[] = true; break;
case :
System.out.println("2");
arrayOfBoolean[] = true; break;
case :
System.out.println("3");
arrayOfBoolean[] = true; break;
case :
System.out.println("4");
arrayOfBoolean[] = true; break;
case :
System.out.println("10");
arrayOfBoolean[] = true; break;
case :
case :
case :
case :
case :
default:
System.out.println("...");
arrayOfBoolean[] = true;
}
arrayOfBoolean[] = true;
}
我們可以發現,每一處label處都插入了探針,以及最後的return處也插入了一個探針。
源碼位元組碼檔案:
public void testSwitch(int);
Code:
: iload_1
: tableswitch { // 1 to 10
:
:
:
:
:
:
:
:
:
:
default:
}
: getstatic # // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc # // String 1
: invokevirtual # // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: goto
: getstatic # // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc # // String 2
: invokevirtual # // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: goto
: getstatic # // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc # // String 3
: invokevirtual # // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: goto
: getstatic # // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc # // String 4
: invokevirtual # // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: goto
: getstatic # // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc # // String 10
: invokevirtual # // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: goto
: getstatic # // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc # // String ...
: invokevirtual # // Method java/io/PrintStream.println:(Ljava/lang/String;)V
: return
插入後class檔案位元組碼:
public void testSwitch(int);
Code:
: invokestatic # // Method $jacocoInit:()[Z
: astore_2
: iload_1
: tableswitch { // 1 to 10
:
:
:
:
:
:
:
:
:
:
default:
}
: getstatic # // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc # // String 1
: invokevirtual # // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//case 1探針
: aload_2
: iconst_4
: iconst_1
: bastore
: goto
: getstatic # // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc # // String 2
: invokevirtual # // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//case 2探針
: aload_2
: iconst_5
: iconst_1
: bastore
: goto
: getstatic # // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc # // String 3
: invokevirtual # // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//case 3 探針
: aload_2
: bipush
: iconst_1
: bastore
: goto
: getstatic # // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc # // String 4
: invokevirtual # // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//case 4 探針
: aload_2
: bipush
: iconst_1
: bastore
: goto
: getstatic # // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc # // String 10
: invokevirtual # // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//case 10探針
: aload_2
: bipush
: iconst_1
: bastore
: goto
: getstatic # // Field java/lang/System.out:Ljava/io/PrintStream;
: ldc # // String ...
: invokevirtual # // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//default 探針
: aload_2
: bipush
: iconst_1
: bastore
//return 探針
: aload_2
: bipush
: iconst_1
: bastore
: return
可以發現,在每一個label處都執行了探針插入(具體在goto指令前),return前也插入了一個探針。
LookupSwitch
lookupswitch與tableswitch類似,依舊是每一個label和return處插入探針。
其插樁後的class檔案反編譯:
public void testSwitch(int arg1) { boolean[] arrayOfBoolean = $jacocoInit(); switch (i)
{
case :
System.out.println("1");
arrayOfBoolean[] = true; break;
case :
System.out.println("2");
arrayOfBoolean[] = true; break;
case :
System.out.println("3");
arrayOfBoolean[] = true; break;
case :
System.out.println("4");
arrayOfBoolean[] = true; break;
case :
System.out.println("10");
arrayOfBoolean[] = true; break;
default:
System.out.println("...");
arrayOfBoolean[] = true;
}
arrayOfBoolean[] = true;
}
Report生成
在結束插樁後,再次運作class檔案,會産生一個Jacoco.exec的檔案,裡面存放了探針的執行資訊,顯示如下:
下面會用這個資訊來生成代碼覆寫率。
Task report
Ant Task Report
我們使用report task來生成不同格式的報告。report task聲明包含以下幾個部分,和指定輸入的資料;指定輸出的格式。
<!-- Step 4: Create coverage report -->
<jacoco:report>
<!-- This task needs the collected execution data and ... -->
<executiondata>
<!-- Jacoco.exec-->
<file file="${result.exec.file}" />
</executiondata>
<!-- the class files and optional source files ... -->
<structure name="JaCoCo Ant Example">
<classfiles>
<!-- ./target/classes:未插樁前的class檔案位元組碼-->
<fileset dir="${result.classes.dir}" />
</classfiles>
<sourcefiles encoding="UTF-8">
<!-- /src:Java源碼處-->
<fileset dir="${src.dir}" />
</sourcefiles>
</structure>
<!-- to produce reports in different formats. -->
<html destdir="${result.report.dir}" />
</jacoco:report>
如上所看到的的,report task是基于幾個嵌套的元素的。
Element executiondata
這個元素指定的Ant resource和resource collections,它們包含了Jacoco 的execution data files(如上圖所示)。如果指定了超過了一種execution data,那麼execution data将被合并。在輸入檔案的任何地方,代碼塊被标記成這樣,那麼就被認為是執行了。
Element structure
這個元素定義了報告結構,它包含以下嵌套元素。
- classfiles:容器元素,它指定了Java class files,archive (檔案檔案)files(jar,war,ear etc,或者Pack200)或者包含class檔案的檔案夾。在檔案或者檔案夾中的class檔案被遞歸查詢。
- sourcefiles:可選的容器元素,指定了相關的源檔案。如果源代碼被指定,報告将會包含高亮代碼。源檔案可以被指定為獨立檔案或者檔案目錄。
Sourcefiles元素可以有以下可選的屬性。
屬性 | 描述 | 預設 |
---|---|---|
encoding | 源碼的字元編碼 | 平台預設編碼 |
tabwidth | 一個tab字元占的空白字元的數量 | 4字元 |
structure可以被精煉為group子元素,這種方式的覆寫報告可以反映項目的不同的子產品。對每一個group元素相關的class檔案和源代碼可以被分别指定。例如:
<structure name="Example Project">
<group name="Server">
<classfiles>
<fileset dir="${workspace.dir}/org.jacoco.example.server/classes"/>
</classfiles>
<sourcefiles>
<fileset dir="${workspace.dir}/org.jacoco.example.server/src"/>
</sourcefiles>
</group>
<group name="Client">
<classfiles>
<fileset dir="${workspace.dir}/org.jacoco.example.client/classes"/>
</classfiles>
<sourcefiles>
<fileset dir="${workspace.dir}/org.jacoco.example.client/src"/>
</sourcefiles>
</group>
...
</structure>
Element xml
使用xml格式建立一個單獨的報告。
屬性 | 描述 | 預設 |
---|---|---|
destfile | 報告的放置位置 | 無 |
encoding | 報告的編碼 | UTF-8 |
Element check
這個元素并沒有在
build.xml
中出現,但是在
ReportTask.java
中有一個
CheckFormatterElement
,它并沒有建立獨立的報告(像html,xml和cvs格式的報告),該元素根據配置的rule檢查coverage counters以及報告的不合法處。每一個rule應用于給定的類型(class,package,bundle,etc)的元素,用于檢查每一個元素的rule有一系列的限制。下面的例子用于檢查每一個包中行覆寫率最少在80%并且沒有class被遺漏。
<check>
<rule element="PACKAGE">
<limit counter="LINE" value="COVEREDRATIO" minimum="80%"/>
<limit counter="CLASS" value="MISSEDCOUNT" maximum="0"/>
</rule>
</check>
check元素有以下的屬性
屬性 | 描述 | 預設 |
---|---|---|
rules | 用于檢查的 集合 | 無 |
failonviolation | 判定 rule violations情況下建立是否失敗 | true |
violationsproperty | 存在violation messages的ant property元素的名稱 | 無 |
我們可以發現,在元素中,有元素,元素中有,事實上,和元素都可以被嵌套。
- 在check元素中,任何數量的rule元素可以被嵌套
屬性 | 描述 | 預設 |
---|---|---|
element | rule應用的element,可以取值: , , , 和 | bundle |
includes | 應當被檢查的元素集合名 | * |
excludes | 不需要被檢查的元素 | empty |
limits | 用于檢查的 | none |
- 在rule元素中,任何數量的limit元素可以被嵌套
屬性 | 描述 | 預設 |
---|---|---|
counter | 被檢查的counter,可以是: , , , , and . | INSTRUCTION |
value | 需要被檢查的counter的值,可以是: , , , and . | COVEREDRATIO |
minimum | 期望的最小值。 | none |
maximum | 期望的最大值。 | none |
插樁政策
對于Java源碼:
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位元組碼定義的控制流圖有不同的類型,每個類型連接配接一個源指令和一個目标指令,當然有時候源指令和目标指令并不存在,或者無法被明确(異常)。不同類型的插入政策也是不一樣的。
下面說明如何在不同的邊緣去情況下具體的插入探針。
Probe探針是通過以下四個指令來設定的。
ALOAD probearray
xPUSH probeid
ICONST_1
BASTORE
注意到探針是線程安全的,它不會改變操作棧和本地數組。它也不會通過外部的調用而離開函數。先決條件僅僅是探針數組作為一個本地變量被擷取。在每個函數的開始,附加的指令代碼将會插入以獲得相應類的數組對象,避免代碼複制,這個初始化會在靜态私有方法
$jacocoinit()
中進行。
具體詳見:Control Flow Analysis for Java Methods
# 源碼解析
整個工具主要分為兩個部分,對編譯好的位元組碼檔案插樁以及根據探針的執行情況生成報告。在
build.xml
中,二者的代碼分别是。
插樁:
<target name="instrument" depends="compile">
<!-- Step 2: Instrument class files -->
<!-- jacoco:instrument見antlib.xml定義-->
<jacoco:instrument destdir="${result.classes.instr.dir}">
<fileset dir="${result.classes.dir}" />
</jacoco:instrument>
</target>
生成報告:
<target name="report" depends="test">
<!-- Step 4: Create coverage report -->
<jacoco:report>
<!-- This task needs the collected execution data and ... -->
<executiondata>
<file file="${result.exec.file}" />
</executiondata>
<!-- the class files and optional source files ... -->
<structure name="JaCoCo Ant Example">
<classfiles>
<!-- ./target/classes:未插樁前的class檔案位元組碼-->
<fileset dir="${result.classes.dir}" />
</classfiles>
<sourcefiles encoding="UTF-8">
<!-- /src:Java源碼處-->
<fileset dir="${src.dir}" />
</sourcefiles>
</structure>
<!-- to produce reports in different formats. -->
<html destdir="${result.report.dir}" />
<csv destfile="${result.report.dir}/report.csv" />
<xml destfile="${result.report.dir}/report.xml" />
</jacoco:report>
</target>
插樁
在插樁中,程式入口是
org.jacoco.ant.InstrumentTask
,向其傳入了兩個參數
destdir
和
fileset
,分别是存放插入後的位元組碼檔案位置以及位元組碼檔案。
在
InstrumentTask
類中,由于是自定義Ant Task,是以執行函數是
excute()
,在
instrument()
函數中調用
Instrumenter
類,在
instrument(final ClassReader reader)
函數中,有以下代碼:
final ClassWriter writer = new ClassWriter(reader, );
final IProbeArrayStrategy strategy = ProbeArrayStrategyFactory
.createFor(reader, accessorGenerator);
final ClassVisitor visitor = new ClassProbesAdapter(
new ClassInstrumenter(strategy, writer), true);
reader.accept(visitor, ClassReader.EXPAND_FRAMES);
可以看出來,
ClassProbesAdapter
應該是ASM架構中的擴充卡(即繼承自ClassVisitor,自定義對位元組碼檔案過濾的類),同時在
ClassInstrumenter
中,發現其
visitMethod()
函數傳回了
MethodInstrumenter
對象,在該類中,找到了具體的插樁方法。
首先在
MethodProbesAdapter
中,定義了插樁政策。示例:
public void visitInsn(final int opcode) {
switch (opcode) {
case Opcodes.IRETURN:
case Opcodes.LRETURN:
case Opcodes.FRETURN:
case Opcodes.DRETURN:
case Opcodes.ARETURN:
case Opcodes.RETURN:
case Opcodes.ATHROW:
probesVisitor.visitInsnWithProbe(opcode, idGenerator.nextId());
break;
default:
probesVisitor.visitInsn(opcode);
break;
}
}
然後在
MethodInstrumenter
中具體實作了各個政策。示例:
public void visitJumpInsnWithProbe(final int opcode, final Label label,
final int probeId, final IFrame frame) {
if (opcode == Opcodes.GOTO) {
probeInserter.insertProbe(probeId);
mv.visitJumpInsn(Opcodes.GOTO, label);
} else {
final Label intermediate = new Label();
mv.visitJumpInsn(getInverted(opcode), intermediate);
probeInserter.insertProbe(probeId);
mv.visitJumpInsn(Opcodes.GOTO, label);
mv.visitLabel(intermediate);
frame.accept(mv);
}
}
具體插入是
probeInserter.insertProbe(probeId);
,它在
ProbeInster
中被實作:
public void insertProbe(final int id) {
// For a probe we set the corresponding position in the boolean[] array
// to true.
mv.visitVarInsn(Opcodes.ALOAD, variable);
// Stack[0]: [Z
InstrSupport.push(mv, id);
// Stack[1]: I
// Stack[0]: [Z
mv.visitInsn(Opcodes.ICONST_1);
// Stack[2]: I
// Stack[1]: I
// Stack[0]: [Z
mv.visitInsn(Opcodes.BASTORE);
}
到這裡插樁實作。
生成報告
生成報告在程式入口在
ReportTask
中,傳入了executionData,sourcefiles和classfiles。其中executionData是再次運作被插樁的位元組碼問價獲得的探針執行情況,在
report生成
一節有介紹。
參考
Java位元組碼操縱架構ASM小試
使用 ASM 實作 Java 語言的“多重繼承” IBM
從Java代碼到位元組碼ImportNew翻譯
簡書ASM建立函數示例
伯樂線上,簡述ASM各種文檔
官方文檔的中文簡單版
中文簡單版2
ASM-Guide
AOP 的利器:ASM 3.0 介紹 IBM
ASM系列1-5 有廣告那個
ASM系列
擁有構造函數以及成員函數兩種示例
添加時間,輸出那個
美團部落格,關于Jacoco很深講的