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

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM