代码覆盖工具Jacoco使用示例及源码分析

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() * 100.0D);
    if (rand % 2 == 0)
      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[0] = true;
    int rand = (int)(Math.random() * 100.0D);
    if (rand % 2 == 0) { arrayOfBoolean[1] = true;
      System.out.println("Hi,0"); arrayOfBoolean[2] = true;
    } else {
      System.out.println("Hi,1"); arrayOfBoolean[3] = true;
    }
    System.out.println("End");
    arrayOfBoolean[4] = true;
  }
}
public class HelloTest
{
  public HelloTest()
  {
    arrayOfBoolean[0] = true;
  }
  public static void main(String[] arg0) {
    boolean[] arrayOfBoolean = $jacocoInit(); Hello h = new Hello();
    arrayOfBoolean[1] = true;
  }
}

插入前字节码

F:\Jacoco\target\classes>javap -c Hello
Compiled from "Hello.java"
public class Hello {
  public Hello();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."":()V
       4: invokestatic  #2                  // Method java/lang/Math.random:()D
       7: ldc2_w        #3                  // double 100.0d
      10: dmul
      11: d2i
      12: istore_1
      13: iload_1
      14: iconst_2
      15: irem
      16: ifne          30
      19: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      22: ldc           #6                  // String Hi,0
      24: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: goto          38
      30: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      33: ldc           #8                  // String Hi,1
      35: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      38: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      41: ldc           #9                  // String End
      43: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      46: return
}

插入后字节码

F:\Jacoco\target\classes-instr>javap -c Hello
Compiled from "Hello.java"
public class Hello {
  public Hello();
    Code:
       0: invokestatic  #49                 // Method $jacocoInit:()[Z
       3: astore_1
       4: aload_0
       5: invokespecial #1                  // Method java/lang/Object."":()V
       8: aload_1
       9: iconst_0
      10: iconst_1
      11: bastore
      12: invokestatic  #2                  // Method java/lang/Math.random:()D
      15: ldc2_w        #3                  // double 100.0d
      18: dmul
      19: d2i
      20: istore_2
      21: iload_2
      22: iconst_2
      23: irem
      24: ifne          46
      27: aload_1
      28: iconst_1
      29: iconst_1
      30: bastore
      31: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      34: ldc           #6                  // String Hi,0
      36: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      39: aload_1
      40: iconst_2
      41: iconst_1
      42: bastore
      43: goto          58
      46: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      49: ldc           #8                  // String Hi,1
      51: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      54: aload_1
      55: iconst_3
      56: iconst_1
      57: bastore
      58: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      61: ldc           #9                  // String End
      63: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      66: aload_1
      67: iconst_4
      68: iconst_1
      69: bastore
      70: 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 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

    }

插入后反编译Java源码:

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

插入后class文件字节码:

 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

可以发现,在每一个label处都执行了探针插入(具体在goto指令前),return前也插入了一个探针。

LookupSwitch

lookupswitch与tableswitch类似,依旧是每一个label和return处插入探针。

其插桩后的class文件反编译:

 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 1000:
      System.out.println("10");
      arrayOfBoolean[8] = true; break;
    default:
      System.out.println("..."); 
      arrayOfBoolean[9] = true;
    }
    arrayOfBoolean[10] = true; 
  } 

Report生成

在结束插桩后,再次运行class文件,会产生一个Jacoco.exec的文件,里面存放了探针的执行信息,显示如下:

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

Task report

Ant Task Report

我们使用report task来生成不同格式的报告。report task声明包含以下几个部分,和指定输入的数据;指定输出的格式。


        <jacoco:report>

            
            <executiondata>
              
                <file file="${result.exec.file}" />
            executiondata>

            
            <structure name="JaCoCo Ant Example">
                <classfiles>
                    
                    <fileset dir="${result.classes.dir}" />
                classfiles>
                <sourcefiles encoding="UTF-8">
                    
                    <fileset dir="${src.dir}" />
                sourcefiles>
            structure>

            
            <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,sourcefilemethod 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使用示例及源码分析_第1张图片

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

代码覆盖工具Jacoco使用示例及源码分析_第2张图片

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

代码覆盖工具Jacoco使用示例及源码分析_第3张图片

Probe探针是通过以下四个指令来设置的。

ALOAD    probearray
xPUSH    probeid
ICONST_1
BASTORE

注意到探针是线程安全的,它不会改变操作栈和本地数组。它也不会通过外部的调用而离开函数。先决条件仅仅是探针数组作为一个本地变量被获取。在每个函数的开始,附加的指令代码将会插入以获得相应类的数组对象,避免代码复制,这个初始化会在静态私有方法$jacocoinit()中进行。

具体详见:Control Flow Analysis for Java Methods

# 源码解析

整个工具主要分为两个部分,对编译好的字节码文件插桩以及根据探针的执行情况生成报告。在build.xml中,二者的代码分别是。

插桩:

<target name="instrument" depends="compile">
        
        
        <jacoco:instrument destdir="${result.classes.instr.dir}">
            <fileset dir="${result.classes.dir}" />
        jacoco:instrument>
    target>
 <taskdef name="instrument" classname="org.jacoco.ant.InstrumentTask"/>

生成报告:

    <target name="report" depends="test">
        
        <jacoco:report>

            
            <executiondata>
                <file file="${result.exec.file}" />
            executiondata>

            
            <structure name="JaCoCo Ant Example">
                <classfiles>
                    
                    <fileset dir="${result.classes.dir}" />
                classfiles>
                <sourcefiles encoding="UTF-8">
                    
                    <fileset dir="${src.dir}" />
                sourcefiles>
            structure>

            
            <html destdir="${result.report.dir}" />
            <csv destfile="${result.report.dir}/report.csv" />
            <xml destfile="${result.report.dir}/report.xml" />
        jacoco:report>
    target>
 <taskdef name="report" classname="org.jacoco.ant.ReportTask"/>

插桩

在插桩中,程序入口是org.jacoco.ant.InstrumentTask,向其传入了两个参数destdirfileset,分别是存放插入后的字节码文件位置以及字节码文件。

InstrumentTask类中,由于是自定义Ant Task,所以执行函数是excute(),在instrument()函数中调用Instrumenter类,在instrument(final ClassReader reader)函数中,有以下代码:

     final ClassWriter writer = new ClassWriter(reader, 0);

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

你可能感兴趣的:(CodeCoverage)