Java代码覆盖率统计的原理

转自 http://linmingren.me/blog/2014/02/java%E4%BB%A3%E7%A0%81%E8%A6%86%E7%9B%96%E7%8E%87%E7%BB%9F%E8%AE%A1%E7%9A%84%E5%8E%9F%E7%90%86/

Java中有一堆统计代码覆盖率的库,我用过的就有JaCoCoCobertura。看起来很高端,不过原理很简单,今天没事自己写了几个类来验证一下。

假设有一个想要被测试的类是这样(实际的类当然不可能这么简单,不过拿来理解原理足够了)

package test;
 
public class UserMgr {
    public int getRole(String username) { 
        if (username.equals("admin")) {
            return 1;
        }
 
        if (username.equals("system")) {
            return 2;
        }
        return -1;
    }
}

如果想要统计getRole函数哪些语句被覆盖到了,最直观的方法就是给这个类加一个列表来保存哪些语句被执行了,然后在每条语句前都往这些列表添加上当前的行号,写出来是这样

package test;
 
import java.util.ArrayList;
import java.util.List;
 
public class UserMgr {
    public static List<Integer> lineCovered = new ArrayList<Integer>();//保存了执行过的行号 
    public int getRole(String username) {
        lineCovered.add(当前行号); if (username.equals("admin")) {
            lineCovered.add(当前行号); return 1;
        }
 
        lineCovered.add(当前行号); if (username.equals("system")) {
            lineCovered.add(当前行号); return 2;
        }
 
        lineCovered.add(当前行号); return -1;
    }
}

接下来要做的就是在不修改UserMgr源码的前提下,直接把UserMgr的class文件的内容换成带有lineCovered成员的那个版本。妈呀,这也太高端了吧?别担心,有了asm和它的Bytecode Outline插件的支持,做这个就是copy&paste的技术含量。

先给UserMgr类增加lineCovered这个静态变量。粗略扫描了一下asm的官方指南asm-guide, 要对class文件作修改,标准的做法是自己实现对应的ClassVisitor和MethodVisitor,然后根据需要加入对应的语句,然后把转换后的class内容保存另外一个class文件中,这个过程就是所谓的instrument.对应的代码如下(记得先在当前工程目录加/instrument/test这两层目录,当然你也可以把修改后的class文件存在别的地方,随你便)

package instrument;
 
import java.io.FileOutputStream;
import java.io.IOException;
 
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
 
public class InstrumentMain {
    
    public static void main(String[] args) throws IOException {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);//用了COMPUTE_MAXS就不需要去处理visitMax了 
        CodeCoverageClassVisitor mv = new CodeCoverageClassVisitor(cw);
        ClassReader cr = new ClassReader("test.UserMgr");
        cr.accept(mv, 0);
FileOutputStream fs = new FileOutputStream("./instrument/test/UserMgr.class");
fs.write(cw.toByteArray());
fs.close();
    }
}

CodeCoverageClassVisitor的代码是这样,关键的地方就是在visitEnd这里要加什么东西,现在轮到Bytecode Outline发神威了。

package instrument;
import static org.objectweb.asm.Opcodes.*;
 
import org.objectweb.asm.ClassVisitor;
public class CodeCoverageClassVisitor extends ClassVisitor { 
    public CodeCoverageClassVisitor(ClassVisitor cv) {
        super(ASM4,cv);
    }
 
    @Override
    public void visitEnd() {
        //在这里给目标类加上lineCovered静态变量
super.visitEnd();
    }
} 

在Eclipse里通过http://andrei.gmxhome.de/eclipse/安装Bytecode Outline插件(不要安装官方版本,否么要么装不上,要么用不了)。安装后把上面的UserMgr的第二个版本在Eclipse里写一遍,然后通过Window -> Show View -> Other -> Java -> Bytecode查看对应的asm代码(点那个Show ASMified Code按钮),可以看到public static ListlineCovered = new ArrayList();这句代码对应了两部分的asm代码,一部分是声明,一部分是初始化。

直接把Bytecode Outline插件里对应的代码复制到visitEnd即可。现在的完整代码是这样:

package instrument;
import static org.objectweb.asm.Opcodes.*;
 
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
public class CodeCoverageClassVisitor extends ClassVisitor {
    public CodeCoverageClassVisitor(ClassVisitor cv) {
        super(ASM4,cv);
    }
 
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
            String signature, String[] exceptions) {
        MethodVisitor mv;
        mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if (mv != null) {
            mv = new CodeCoverageMethodVisitor(mv);
        }
        return mv;
    }
    @Override
    public void visitEnd() {
        FieldVisitor fv = cv.visitField(ACC_PUBLIC + ACC_STATIC, "lineCovered", "Ljava/util/List;", "Ljava/util/List<Ljava/lang/Integer;>;", null); 
        fv.visitEnd();
cv.visitEnd();
 
        MethodVisitor mv = cv.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
        mv.visitCode();
        Label l0 = new Label();
        mv.visitLabel(l0);
        mv.visitLineNumber(7, l0);
        mv.visitTypeInsn(NEW, "java/util/ArrayList");
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESPECIAL, "java/util/ArrayList", "<init>", "()V");
        mv.visitFieldInsn(PUTSTATIC, "test/UserMgr", "lineCovered", "Ljava/util/List;");
        mv.visitInsn(RETURN);
        mv.visitMaxs(2, 0);
        
        //super.visitEnd();
    }
}

最后就是写一个MethodVisitor来给每行代码加上对应的lineCovered.add(当前行号)。初始版本是这样:

package instrument;
 
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.*;
public class CodeCoverageMethodVisitor extends MethodVisitor { 
    public CodeCoverageMethodVisitor(MethodVisitor mv) {
        super(ASM4,mv);
    }
    
    @Override
    public void visitLineNumber(int line, Label arg1) {
        //在每行语句前加lineCovered.add()
        super.visitLineNumber(line, arg1);
    }
}
  
visitLineNumber里面的内容也可以通过Bytecode Outline直接复制即可。

CodeCoverageMethodVisitor 的完整代码如下:

package instrument;
 
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import static org.objectweb.asm.Opcodes.*;
public class CodeCoverageMethodVisitor extends MethodVisitor {
    public CodeCoverageMethodVisitor(MethodVisitor mv) {
        super(ASM4,mv);
    }
    
    @Override
    public void visitLineNumber(int line, Label arg1) {
        mv.visitFieldInsn(GETSTATIC, "test/UserMgr", "lineCovered","Ljava/util/List;");
        mv.visitIntInsn(SIPUSH, line);
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf","(I)Ljava/lang/Integer;"); 
        mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "add","(Ljava/lang/Object;)Z");
        mv.visitInsn(POP);
        super.visitLineNumber(line, arg1);
    }
}

现在脏活干完了,重新运行下InstrumentMain来生成对应的修改后的class文件。最后写个main函数来测试一下:

package test;
 
public class RunTest {
    public static void main(String[] args) {
        UserMgr e = new UserMgr();
        e.lineCovered.clear();//清除旧的行号数据,
        
        e.getRole("admin");
        System.out.println("getRole on admin covers the following lines:");
        for (Integer line: UserMgr.lineCovered) {
            System.out.println("line: " + line);
        }
        
        e.lineCovered.clear();
        e.getRole("system");
        System.out.println("getRole on system covers the following lines:"); 
        for (Integer line: UserMgr.lineCovered) {
            System.out.println("line: " + line);
        }
    }
}

输出的结果是(在jdk7上可能会出现java.lang.VerifyError: Expecting a stackmap frame这样的错误,这时给测试程序加上-XX:-UseSplitVerifier参数即可):

getRole on admin covers the following lines:
line: 5
line: 6
getRole on system covers the following lines: 
line: 5
line: 9
line: 10

 

 
posted @ 2016-02-02 16:15  princessd8251  阅读(4244)  评论(2编辑  收藏  举报