天天看点

代码覆盖工具Jacoco使用示例及源码分析Content调用的开源框架测试源码对比两种插桩模式关于switch插桩分析Report生成插桩策略参考

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
}
           

两种插桩模式

插桩方式

代码覆盖工具Jacoco使用示例及源码分析Content调用的开源框架测试源码对比两种插桩模式关于switch插桩分析Report生成插桩策略参考

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的文件,里面存放了探针的执行信息,显示如下:

代码覆盖工具Jacoco使用示例及源码分析Content调用的开源框架测试源码对比两种插桩模式关于switch插桩分析Report生成插桩策略参考
代码覆盖工具Jacoco使用示例及源码分析Content调用的开源框架测试源码对比两种插桩模式关于switch插桩分析Report生成插桩策略参考

下面会用这个信息来生成代码覆盖率。

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 用于检查的

rules

集合
failonviolation 判定 rule violations情况下创建是否失败 true
violationsproperty 存在violation messages的ant property元素的名称

我们可以发现,在元素中,有元素,元素中有,事实上,和元素都可以被嵌套。

  • 在check元素中,任何数量的rule元素可以被嵌套
属性 描述 默认
element rule应用的element,可以取值:

bundle

,

package

,

class

,

sourcefile

method

bundle
includes 应当被检查的元素集合名 *
excludes 不需要被检查的元素 empty
limits 用于检查的

limits

none

- 在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

插桩策略

对于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数组,下面是原始的控制流图,以及插桩完成的控制流图。

代码覆盖工具Jacoco使用示例及源码分析Content调用的开源框架测试源码对比两种插桩模式关于switch插桩分析Report生成插桩策略参考

由Java字节码定义的控制流图有不同的类型,每个类型连接一个源指令和一个目标指令,当然有时候源指令和目标指令并不存在,或者无法被明确(异常)。不同类型的插入策略也是不一样的。

代码覆盖工具Jacoco使用示例及源码分析Content调用的开源框架测试源码对比两种插桩模式关于switch插桩分析Report生成插桩策略参考

下面说明如何在不同的边缘去情况下具体的插入探针。

代码覆盖工具Jacoco使用示例及源码分析Content调用的开源框架测试源码对比两种插桩模式关于switch插桩分析Report生成插桩策略参考

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很深讲的