Jacoco字节码植入原理(源码分析)
首先了解jacoco agent入口類(MANIFEST.M文件聲明):
入口類—PreMain:
代碼:
packageorg.jacoco.agent.rt.internal_6da5971;importjava.lang.instrument.Instrumentation;importorg.jacoco.agent.rt.internal_6da5971.core.runtime.AgentOptions;importorg.jacoco.agent.rt.internal_6da5971.core.runtime.IRuntime;importorg.jacoco.agent.rt.internal_6da5971.core.runtime.ModifiedSystemClassRuntime;publicfinal class PreMain{public static void premain(String options,Instrumentation inst)throws Exception{AgentOptions agentOptions = newAgentOptions(options);Agent agent = Agent.getInstance(agentOptions);IRuntime runtime = createRuntime(inst);runtime.startup(agent.getData());inst.addTransformer(newCoverageTransformer(runtime, agentOptions, IExceptionLogger.SYSTEM_ERR));}private static IRuntime createRuntime(Instrumentationinst)throws Exception{return ModifiedSystemClassRuntime.createFor(inst,"java/util/UUID");}}Jaococ使用asm實(shí)現(xiàn)字節(jié)碼植入,是對(duì)指令級(jí)別上的字節(jié)碼植入,從而可以定位到執(zhí)行的代碼行,以達(dá)到覆蓋率的統(tǒng)計(jì)。在這個(gè)基礎(chǔ)上,jacoco有對(duì)類級(jí)別,方法級(jí)別,邏輯分支級(jí)別以及代碼行級(jí)別做了專門的處理封裝。具體的封裝類在internal.analysis.flow下面,涉及的類分別是ClassprobesAdapter.java(類級(jí)別),Instruction.java(指令級(jí)別),LabelFlowAnalysis.java(邏輯分支級(jí)別)和MethodProbesAdapter.java(方法級(jí)別)。
ClassprobesAdapter類核心代碼:
?
publicfinal MethodVisitor visitMethod(intaccess, String name, String desc, String signature, String[] exceptions){MethodProbesVisitor mv =this.cv.visitMethod(access, name, desc, signature, exceptions);MethodProbesVisitor methodProbes;final MethodProbesVisitor methodProbes;if (mv == null) {methodProbes =EMPTY_METHOD_PROBES_VISITOR;} else {methodProbes = mv;}new MethodSanitizer(null, access, name,desc, signature, exceptions){public void visitEnd(){super.visitEnd();LabelFlowAnalyzer.markLabels(this);MethodProbesAdapter probesAdapter = newMethodProbesAdapter(methodProbes, ClassProbesAdapter.this);if(ClassProbesAdapter.this.trackFrames){AnalyzerAdapter analyzer = new AnalyzerAdapter(ClassProbesAdapter.this.name,this.access, this.name, this.desc, probesAdapter); probesAdapter.setAnalyzer(analyzer);accept(analyzer);}else{accept(probesAdapter);}}};}可見(jiàn)類覆蓋率字節(jié)碼埋入實(shí)際上是對(duì)類中每一個(gè)方法和每一個(gè)邏輯分支做埋入,只要記錄調(diào)用類中方法的覆蓋代碼行,自然類的覆蓋就會(huì)被統(tǒng)計(jì)到。
接著看MethodProbesAdapter 中的代碼:
@Overridepublic void visitLabel(final Label label) {if (LabelInfo.needsProbe(label)) {if(tryCatchProbeLabels.containsKey(label)) {probesVisitor.visitLabel(tryCatchProbeLabels.get(label));}probesVisitor.visitProbe(idGenerator.nextId());}probesVisitor.visitLabel(label);}@Overridepublic 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;}}@Overridepublic void visitJumpInsn(final int opcode, final Label label) {if (LabelInfo.isMultiTarget(label)) {probesVisitor.visitJumpInsnWithProbe(opcode,label,idGenerator.nextId(), frame(jumpPopCount(opcode)));} else {probesVisitor.visitJumpInsn(opcode,label);}}private int jumpPopCount(final int opcode) {switch (opcode) {case Opcodes.GOTO:return 0;case Opcodes.IFEQ:case Opcodes.IFNE:case Opcodes.IFLT:case Opcodes.IFGE:case Opcodes.IFGT:case Opcodes.IFLE:case Opcodes.IFNULL:case Opcodes.IFNONNULL:return 1;default: // IF_CMPxx and IF_ACMPxxreturn 2;}}@Overridepublic void visitLookupSwitchInsn(final Label dflt, final int[]keys,final Label[] labels) {if (markLabels(dflt, labels)) {probesVisitor.visitLookupSwitchInsnWithProbes(dflt,keys, labels,frame(1));} else {probesVisitor.visitLookupSwitchInsn(dflt,keys, labels);}}@Overridepublic void visitTableSwitchInsn(final int min, final int max,final Label dflt, final Label...labels) {if (markLabels(dflt, labels)) {probesVisitor.visitTableSwitchInsnWithProbes(min,max, dflt,labels, frame(1));} else {probesVisitor.visitTableSwitchInsn(min,max, dflt, labels);}}在MethodProbesAdapter中明顯看到字節(jié)碼指令信息,對(duì)于一個(gè)方法的進(jìn)入,jvm中是一個(gè)方法棧的創(chuàng)建,入口指令是入棧指令,退出是return:
privateint jumpPopCount(finalint opcode) {
??????? switch (opcode) {
??????? case Opcodes.GOTO:
??????????? return0;
??????? caseOpcodes.IFEQ:
??????? caseOpcodes.IFNE:
??????? caseOpcodes.IFLT:
??????? caseOpcodes.IFGE:
??????? caseOpcodes.IFGT:
??????? caseOpcodes.IFLE:
??????? caseOpcodes.IFNULL:
??????? caseOpcodes.IFNONNULL:
??????????? return1;
??????? default:// IF_CMPxx and IF_ACMPxx
??????????? return2;
??????? }
??? }
退出方法是return 指令:
publicvoid visitInsn(finalint opcode) {
??????? switch (opcode) {
??????? case Opcodes.IRETURN:
??????? caseOpcodes.LRETURN:
??????? caseOpcodes.FRETURN:
??????? caseOpcodes.DRETURN:
??????? caseOpcodes.ARETURN:
??????? caseOpcodes.RETURN:
??????? caseOpcodes.ATHROW:
??????????? probesVisitor.visitInsnWithProbe(opcode,idGenerator.nextId());
??????????? break;
??????? default:
??????? ??? probesVisitor.visitInsn(opcode);
??????????? break;
??????? }
??? }
邏輯跳轉(zhuǎn)的有switch,if
publicvoid visitTableSwitchInsn(finalint min, final int max,
??????????? final Label dflt, final Label...labels) {
??????? if (markLabels(dflt, labels)) {
??????????? probesVisitor.visitTableSwitchInsnWithProbes(min,max, dflt,
??????????????????? labels, frame(1));
??????? } else {
??????????? probesVisitor.visitTableSwitchInsn(min,max, dflt, labels);
??????? }
??? }
If分支:
case Opcodes.GOTO:
??????????? return0;
??????? caseOpcodes.IFEQ:
??????? caseOpcodes.IFNE:
??????? caseOpcodes.IFLT:
??????? caseOpcodes.IFGE:
??????? caseOpcodes.IFGT:
??????? caseOpcodes.IFLE:
??????? caseOpcodes.IFNULL:
??????? caseOpcodes.IFNONNULL:
??????????? return1;
??????? default:// IF_CMPxx and IF_ACMPxx
??????????? return2;
??????? }?
LabelFlowAnalysis主要實(shí)現(xiàn)代碼:
@Overridepublic void visitJumpInsn(final int opcode, final Label label) {LabelInfo.setTarget(label);if (opcode == Opcodes.JSR) {thrownew AssertionError("Subroutines not supported.");}successor = opcode != Opcodes.GOTO;first = false;}@Overridepublic void visitLabel(final Label label) {if (first) {LabelInfo.setTarget(label);}if (successor) {LabelInfo.setSuccessor(label);}}@Overridepublic void visitLineNumber(final int line, final Label start) {lineStart = start;}@Overridepublic void visitTableSwitchInsn(final int min, final int max,final Label dflt, final Label...labels) {visitSwitchInsn(dflt, labels);}@Overridepublic void visitLookupSwitchInsn(final Label dflt, final int[]keys,final Label[] labels) {visitSwitchInsn(dflt, labels);}@Overridepublic void visitInsn(final int opcode) {switch (opcode) {case Opcodes.RET:throw new AssertionError("Subroutinesnot supported.");case Opcodes.IRETURN:case Opcodes.LRETURN:case Opcodes.FRETURN:case Opcodes.DRETURN:case Opcodes.ARETURN:case Opcodes.RETURN:case Opcodes.ATHROW:successor = false;break;default:successor = true;break;}first = false;}首先要知道對(duì)于一串指令比如:
iLoad A;
iLoad B;
Add A,B;
iStore;
……
如果沒(méi)有跳轉(zhuǎn)指令 GOTO LABEL或者jump,那么指令值按順序執(zhí)行的,所以我們只要在開(kāi)始的時(shí)候添加一個(gè)探針就好,只要探針指令執(zhí)行了,那么下面的指令一定會(huì)被執(zhí)行的,除非有了跳轉(zhuǎn)邏輯。因此我們只要在每一個(gè)跳轉(zhuǎn)的開(kāi)始和結(jié)束添加探針就好,就可以完全實(shí)現(xiàn)統(tǒng)計(jì)代碼塊的覆蓋,而沒(méi)有必要每一行都要植入探針。
接著在看Instruction代碼:
*/public void setPredecessor(final Instructionpredecessor,final int branch) {this.predecessor = predecessor;predecessor.addBranch();this.predecessorBranch = branch;}/***Marks one branch of this instruction as covered. Also recursively marks* allpredecessor instructions as covered if this is the first covered*branch.**@param branch* branch number to mark as covered*/public void setCovered(final int branch) {Instruction i = this;int b = branch;while (i != null) {if (!i.coveredBranches.isEmpty()) {i.coveredBranches.set(b);break;}i.coveredBranches.set(b);b = i.predecessorBranch;i = i.predecessor;}}Instruction的實(shí)現(xiàn)是為了記錄對(duì)應(yīng)指令的代碼行,記錄在跳轉(zhuǎn)的label處對(duì)應(yīng)的代碼行數(shù),那么類推可以等到整個(gè)覆蓋和未覆蓋的代碼行。
上面已經(jīng)了解我們對(duì)于類,方法,邏輯塊以及具體代碼的記錄和探針植入;接著我們需要了解具體植入的是什么指令。首先看下jacoco中探針植入類—ProbeInserter
ProbeInserter(final int access, final String name, finalString desc, final MethodVisitor mv,final IProbeArrayStrategyarrayStrategy) {super(InstrSupport.ASM_API_VERSION, mv);this.clinit =InstrSupport.CLINIT_NAME.equals(name);this.arrayStrategy = arrayStrategy;int pos = (Opcodes.ACC_STATIC &access) == 0 ? 1 : 0;for (final Type t :Type.getArgumentTypes(desc)) {pos += t.getSize();}variable = pos;}public void insertProbe(final int id) {// For a probe we set the correspondingposition in the boolean[] array// to true.mv.visitVarInsn(Opcodes.ALOAD, variable);// Stack[0]: [ZInstrSupport.push(mv, id);// Stack[1]: I// Stack[0]: [Zmv.visitInsn(Opcodes.ICONST_1);// Stack[2]: I// Stack[1]: I// Stack[0]: [Zmv.visitInsn(Opcodes.BASTORE);}private void visitInsn() {final Instruction insn = newInstruction(currentNode, currentLine);nodeToInstruction.put(currentNode,insn);instructions.add(insn);if (lastInsn != null) {insn.setPredecessor(lastInsn, 0);}final int labelCount =currentLabel.size();if (labelCount > 0) {for (int i = labelCount; --i >=0;) {LabelInfo.setInstruction(currentLabel.get(i),insn);}currentLabel.clear();}lastInsn = insn;}@Overridepublic final void visitIincInsn(final int var, final intincrement) {mv.visitIincInsn(map(var), increment);}@Overridepublic final void visitLocalVariable(final String name, final Stringdesc,final String signature, final Labelstart, final Label end,final int index) {mv.visitLocalVariable(name, desc,signature, start, end, map(index));}大致思路就是,在對(duì)應(yīng)字節(jié)碼執(zhí)行入口和跳轉(zhuǎn)入口處,放入probe,是一個(gè)數(shù)值(這個(gè)數(shù)值和probe id有關(guān)系),入棧之后加1,則記錄一次執(zhí)行。所有放入的探針對(duì)應(yīng)一個(gè)boolean [],探針入棧之后,那么boolean[] 對(duì)應(yīng)的位置變成true,記錄執(zhí)行了。
InstrSupport類中關(guān)鍵的兩個(gè)方法:
publicstatic void assertNotInstrumented(finalString member,final String owner) throwsIllegalStateException {if (member.equals(DATAFIELD_NAME) ||member.equals(INITMETHOD_NAME)) {throw new IllegalStateException(format("Class%s is already instrumented.", owner));}}/***Generates the instruction to push the given int value on the stack.*Implementation taken from*{@link org.objectweb.asm.commons.GeneratorAdapter#push(int)}.**@param mv* visitor to emit the instruction*@param value* the value to be pushed on the stack.*/public static void push(final MethodVisitor mv, final int value) {if (value >= -1 && value<= 5) {mv.visitInsn(Opcodes.ICONST_0 +value);} else if (value >= Byte.MIN_VALUE&& value <= Byte.MAX_VALUE) {mv.visitIntInsn(Opcodes.BIPUSH,value);} else if (value >= Short.MIN_VALUE&& value <= Short.MAX_VALUE) {mv.visitIntInsn(Opcodes.SIPUSH,value);} else {mv.visitLdcInsn(Integer.valueOf(value));}}Push是用來(lái)對(duì)于不同的變量值入棧的不同方式,當(dāng)int取值-1~5時(shí),JVM采用iconst指令將常量壓入棧中,當(dāng)int取值-128~127時(shí),JVM采用bipush指令將常量壓入棧中,當(dāng)int取值-32768~32767時(shí),JVM采用sipush指令將常量壓入棧中,當(dāng)int取值-2147483648~2147483647時(shí),JVM采用ldc指令將常量壓入棧中。
?
在jacoco對(duì)類和方法進(jìn)行植入的時(shí)候,會(huì)對(duì)類的植入鎖定進(jìn)行判斷,對(duì)應(yīng)的類是instrumenter。
publicbyte[] instrument(finalbyte[] buffer, final String name)throws IOException {try {return instrument(newClassReader(buffer));} catch (final RuntimeException e) {throwinstrumentError(name, e);}}/***Creates a instrumented version of the given class if possible. The*provided {@link InputStream} is not closed by this method.**@param input* stream to read class definition from*@param name* a name used for exception messages*@return instrumented definition*@throws IOException* if reading data from the stream fails or the class can't be* instrumented*/public byte[] instrument(final InputStream input, final Stringname)throws IOException {final byte[] bytes;try {bytes =InputStreams.readFully(input);} catch (final IOException e) {throw instrumentError(name, e);}return instrument(bytes, name);}/***Creates a instrumented version of the given class file. The provided*{@link InputStream} and {@link OutputStream} instances are not closed by*this method.**@param input* stream to read class definition from*@param output* stream to write the instrumented version of the class to*@param name* a name used for exception messages*@throws IOException* if reading data from the stream fails or the class can't be* instrumented*/public void instrument(final InputStream input, finalOutputStream output,final String name) throwsIOException {output.write(instrument(input, name));}private IOException instrumentError(finalString name,finalException cause) {final IOException ex = new IOException(String.format("Errorwhile instrumenting %s.", name));ex.initCause(cause);return ex;}總結(jié)
以上是生活随笔為你收集整理的Jacoco字节码植入原理(源码分析)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: JVMTM Tool Interface
- 下一篇: sonar 集群环境工作机制的深入理解