浅析jacoco Off-line instrumentation

jacoco 是一个代码覆盖测试的开源工具(java),有许多种集成方法,集成之后,我们就可以看到,那些代码被执行,那些没被执行过。管这个就是代码覆盖测试。可以开发自己写单元测试,也可以测试手动去点,只有执行到,有记录,就算代码有覆盖到测试。

引文:

这两天在写代码覆盖测试的东西,项目中有同事用到了jacoco。没有什么注释&&文档,我做了一点背调,越看资料越是云里雾里的。晚上从jacoco的官方文档入手,做一点点分析。抛开那些包了一层又一层的框架,jacoco到底做了什么呢?

先写一个简单的java吧~

public final class Test{
        public String me = "Yeshen";

        public static void main(String[] args){
                Test t = new Test();
                System.out.println(t.me);
                if(args != null && args.length > 0){
                        System.out.println(args[0]);
                }else{
                        t.hi();
                }
        }

        public void hi(){
                System.out.println("hi");
        }

        public void nonono(){
                System.out.println("nonono");
        }
}
javac Test.java
java Test longlongArgs

ok,这是原滋原味的java,以及执行结果。用jacoco处理之后呢?

获取jacoco

wget http://search.maven.org/remotecontent?filepath=org/jacoco/jacoco/0.8.1/jacoco-0.8.1.zip
7z x remotecontent?filepath=org/jacoco/jacoco/0.8.1/jacoco-0.8.1.zip
# jacococli.jar lib/jacocoagent.jar is what we need

jacoco处理class文件,参考cli

# jacoco offline 修改
java -jar jacococli.jar instrument Test.class --dest out
# cp jacococli.jar out && cp lib/jacocoagent.jar out && cd out
# exec Test
java -cp .:jacocoagent.jar Test anotherArgs
# check the code coverage
java -jar jacococli.jar execinfo jacoco.exec

可以看到jacoco.exec 就是代码的覆盖执行报告了。jacoco已经完成了它的功能了。

接下来我们看看字节码部分被修改了多少

javap -c -v Test.class
cd out && javap -c -v Test.class

可以看到,常量池被加入了这些代码,这些代码依赖于jacocoagent.jar。(所以修改之后,我们要手动引用这个jar包。)

#42 = Utf8               $jacocoInit
#43 = Utf8               ()[Z
#44 = NameAndType        #42:#43        // $jacocoInit:()[Z
#45 = Methodref          #21.#44        // Test.$jacocoInit:()[Z
#46 = Utf8               [Z
#47 = Class              #46            // "[Z"
#48 = Utf8               $jacocoData
#49 = NameAndType        #48:#46        // $jacocoData:[Z
#50 = Fieldref           #4.#49         // Test.$jacocoData:[Z
#51 = Long               4767435040597437437l
#53 = String             #29            // Test
#54 = Utf8               org/jacoco/agent/rt/internal_c13123e/Offline
#55 = Class              #54            // org/jacoco/agent/rt/internal_c13123e/Offline
#56 = Utf8               getProbes
#57 = Utf8               (JLjava/lang/String;I)[Z
#58 = NameAndType        #56:#57        // getProbes:(JLjava/lang/String;I)[Z
#59 = Methodref          #55.#58        // org/jacoco/agent/rt/internal_c13123e/Offline.getProbes:(JLjava/lang/String;I)[Z

每个函数段都被加了一些调用的字节码,举个例子

原来

public void hi();
descriptor: ()V
flags: ACC_PUBLIC
Code:
  stack=2, locals=1, args_size=1
     0: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
     3: ldc           #9                  // String hi
     5: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     8: return
  LineNumberTable:
    line 15: 0
    line 16: 8

修改后

public void hi();
descriptor: ()V
flags: ACC_PUBLIC
Code:
  stack=5, locals=2, args_size=1
     0: invokestatic  #45                 // Method $jacocoInit:()[Z
     3: astore_1
     4: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
     7: ldc           #9                  // String hi
     9: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    12: aload_1
    13: bipush        8
    15: iconst_1
    16: bastore
    17: return
  LineNumberTable:
    line 15: 4
    line 16: 12

这两个取个diff,就可以看到jacoco做了什么,主要是增加了几个字节码

# 函数调用前
invokestatic
astore_1   # JVM从操作数栈顶部弹出一个引用类型或者returnAddress类型值,然后将该值存入由索引1指定的局部变量中,即将引用类型或者returnAddress类型值存入局部变量1

# 函数调用后
aload_1   # 把存放在局部变量表中索引1位置的对象引用压入操作栈
bipush    # 把压入栈
iconst_1  # 将整形常量1压入栈(作为存入数组中的值)
bastore   # 操作数栈的值存储到数组元素

从表象上猜测是这样的,每个函数都有一个调用次数,调用前把这个数放出来,调用后加一,再放回去。不过从class中没看到修改之后的保存字节码,猜测是有一个全局的方法,把这些调用次数的数据回刷到 jacoco.exec 中。

回头看文档,发现用到了ASM,看到这几个熟悉的操作码。有一点理解错了,它使用了boolean[] array。

回到问题,所以是做了什么?嗯,修改了上面这些字节码。

结合代码来看

git clone https://github.com/jacoco/jacoco

jacoco/org.jacoco.core/src/org/jacoco/core/instr/Instrumenter.java

主要修改的代码在

private byte[] instrument(final byte[] source) {
    final long classId = CRC64.classId(source);
    final int originalVersion = BytecodeVersion.get(source);
    final byte[] b = BytecodeVersion.downgradeIfNeeded(originalVersion,
            source);
    final ClassReader reader = new ClassReader(b);
    final ClassWriter writer = new ClassWriter(reader, 0) {
        @Override
        protected String getCommonSuperClass(final String type1,
                final String type2) {
            throw new IllegalStateException();
        }
    };
    final IProbeArrayStrategy strategy = ProbeArrayStrategyFactory
            .createFor(classId, reader, accessorGenerator);
    final ClassVisitor visitor = new ClassProbesAdapter(
            new ClassInstrumenter(strategy, writer),
            InstrSupport.needsFrames(originalVersion));
    reader.accept(visitor, ClassReader.EXPAND_FRAMES);
    final byte[] instrumented = writer.toByteArray();
    BytecodeVersion.set(instrumented, originalVersion);
    return instrumented;
}

可以看到,对class的修改是这几个类做了的

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;

进去看就看到熟悉的常量池,看到熟悉的字节码的修改了,这部分就是offline的修改。那么如何统计呢?

从上面可以看到,在常量池是有一个 org/jacoco/agent/rt/internal_c13123e/Offline 的调用

代码在 jacoco/org.jacoco.agent.rt/src/org/jacoco/agent/rt/internal/Offline.java

public final class Offline {

    private static final RuntimeData DATA;
    private static final String CONFIG_RESOURCE = "/jacoco-agent.properties";

    static {
        final Properties config = ConfigLoader.load(CONFIG_RESOURCE,
                System.getProperties());
        DATA = Agent.getInstance(new AgentOptions(config)).getData();
    }

    private Offline() {
        // no instances
    }

    /**
     * API for offline instrumented classes.
     * 
     * @param classid
     *            class identifier
     * @param classname
     *            VM class name
     * @param probecount
     *            probe count for this class
     * @return probe array instance for this class
     */
    public static boolean[] getProbes(final long classid,
            final String classname, final int probecount) {
        return DATA.getExecutionData(Long.valueOf(classid), classname,
                probecount).getProbes();
    }

}

可以看到 getProbes 就在这里存了代码访问的统计数据了。这里之后,class的字节码信息就完了,接下来就是调用到offline这个方法,做的一些统计了。

ExecutionData有点像数据库,存了这几个数据,存在内存中

private final long id;
private final String name;
private final boolean[] probes;

当需要保存的时候,用ExecutionDataWriter写文件到磁盘上。这个是可选配置,在

jacoco/org.jacoco.core/src/org/jacoco/core/runtime/AgentOptions.java

/**
 * Sets whether coverage data should be dumped on exit.
 * 
 * @param dumpOnExit
 * <code>true</code> if coverage data should be written on VM
 * exit
 */
public void setDumpOnExit(final boolean dumpOnExit) {
    setOption(DUMPONEXIT, dumpOnExit);
}
public static synchronized Agent getInstance(final AgentOptions options) {
    if (singleton == null) {
        final Agent agent = new Agent(options, IExceptionLogger.SYSTEM_ERR);
        agent.startup();
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                agent.shutdown();
            }
        });
        singleton = agent;
    }
    return singleton;
}
/**
 * Shutdown the agent again.
 */
public void shutdown() {
    try {
        if (options.getDumpOnExit()) {
            output.writeExecutionData(false);
        }
        output.shutdown();
        if (jmxRegistration != null) {
            jmxRegistration.call();
        }
    } catch (final Exception e) {
        logger.logExeption(e);
    }
}

简单说就是在VM退出的时候,加了一个Hook,如果有设置了保存的属性,就在退出的那个时间点,把内存中的统计序列化到文件中,当然这个dump的方法也可以手动调用。

PS:如果你在nanny上看到这篇文章,那就是我发的 : )

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值