轉自 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中有一堆統計代碼覆蓋率的庫,我用過的就有JaCoCo和Cobertura。看起來很高端,不過原理很簡單,今天沒事自己寫了幾個類來驗證一下。
假設有一個想要被測試的類是這樣(實際的類當然不可能這么簡單,不過拿來理解原理足夠了)
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); } }
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