首先了解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实现字节码植入,是对指令级别上的字节码植入,从而可以定位到执行的代码行,以达到覆盖率的统计。在这个基础上,jacoco有对类级别,方法级别,逻辑分支级别以及代码行级别做了专门的处理封装。具体的封装类在internal.analysis.flow下面,涉及的类分别是ClassprobesAdapter.java(类级别),Instruction.java(指令级别),LabelFlowAnalysis.java(逻辑分支级别)和MethodProbesAdapter.java(方法级别)。

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);}}};}

可见类覆盖率字节码埋入实际上是对类中每一个方法和每一个逻辑分支做埋入,只要记录调用类中方法的覆盖代码行,自然类的覆盖就会被统计到。

接着看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中明显看到字节码指令信息,对于一个方法的进入,jvm中是一个方法栈的创建,入口指令是入栈指令,退出是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;

}

}

逻辑跳转的有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主要实现代码:

@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;}

首先要知道对于一串指令比如:

iLoad A;

iLoad B;

Add A,B;

iStore;

……

如果没有跳转指令 GOTO LABEL或者jump,那么指令值按顺序执行的,所以我们只要在开始的时候添加一个探针就好,只要探针指令执行了,那么下面的指令一定会被执行的,除非有了跳转逻辑。因此我们只要在每一个跳转的开始和结束添加探针就好,就可以完全实现统计代码块的覆盖,而没有必要每一行都要植入探针。

接着在看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的实现是为了记录对应指令的代码行,记录在跳转的label处对应的代码行数,那么类推可以等到整个覆盖和未覆盖的代码行。

上面已经了解我们对于类,方法,逻辑块以及具体代码的记录和探针植入;接着我们需要了解具体植入的是什么指令。首先看下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));}

大致思路就是,在对应字节码执行入口和跳转入口处,放入probe,是一个数值(这个数值和probe id有关系),入栈之后加1,则记录一次执行。所有放入的探针对应一个boolean [],探针入栈之后,那么boolean[] 对应的位置变成true,记录执行了。

InstrSupport类中关键的两个方法:

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是用来对于不同的变量值入栈的不同方式,当int取值-1~5时,JVM采用iconst指令将常量压入栈中,当int取值-128~127时,JVM采用bipush指令将常量压入栈中,当int取值-32768~32767时,JVM采用sipush指令将常量压入栈中,当int取值-2147483648~2147483647时,JVM采用ldc指令将常量压入栈中。

在jacoco对类和方法进行植入的时候,会对类的植入锁定进行判断,对应的类是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;}

Jacoco字节码植入原理(源码分析)相关推荐

  1. Unity Fog 原理 源码分析 案例

    Unity Fog 原理 源码分析 案例 效果图 简述 背景知识 clip空间坐标的范围 d3d (near,0), unity remapping to (0,far) 越靠近相机z值越小 open ...

  2. 【Elasticsearch源码】CCR源码分析(一)

    1 CCR的基本概念 什么是CCR? CCR( cross-cluster replication):跨集群复制是ES 6.5发布的一个新的特性:可以将两个集群中的数据进行远程复制. 集群复制类似于数 ...

  3. PHP实现个人免签约微信支付接口原理+源码

    什么是个人免签支付 个人免签支付就是给个人用的支付接口,一般的支付接口都需要营业执照才能申请,个人很难申请的到,或者是没有资质去申请,要和支付商进行签约的.免签,顾名思义就是不需要签约.那么个人免签支 ...

  4. 仿猎豹垃圾清理 实现原理+源码

    仿猎豹垃圾清理(实现原理+源码) 转载请注明出处: 仿猎豹垃圾清理(实现原理+源码) 前几天无意打开猎豹内存大师, 发现它的垃圾清理很强大, 效果也不错, 闲着就研究了下. 不过.. 结果貌似和我想象 ...

  5. 仿猎豹垃圾清理(实现原理+源码)

    仿猎豹垃圾清理(实现原理+源码) 转载请注明出处: 仿猎豹垃圾清理(实现原理+源码) 前几天无意打开猎豹内存大师, 发现它的垃圾清理很强大, 效果也不错, 闲着就研究了下. 不过.. 结果貌似和我想象 ...

  6. 仿iOS猎豹垃圾清理(实现原理+源码)

    转载请注明出处: 仿猎豹垃圾清理(实现原理+源码) 前几天无意打开猎豹内存大师, 发现它的垃圾清理很强大, 效果也不错, 闲着就研究了下. 不过.. 结果貌似和我想象的不太一样.怎么说呢, 听我下文一 ...

  7. 【Elasticsearch源码】CCR源码分析(二)

    接上一篇:[Elasticsearch源码]CCR源码分析(一). sendShardChangesRequest方法最终进入到ShardChangesAction.TransportAction#s ...

  8. k线顶分型 python_顶底分型-(K线分类及顶底分型的一种数学原理 源码 贴图)...

    好股票软件下载网(www.goodgupiao.com)提示:您正在下载的是:顶底分型-(K线分类及顶底分型的一种数学原理 源码 贴图) 参考缠论,研究了很多天终于将顶底分型进行了具体的数学量化,涵盖 ...

  9. 艾默生充电15kw+台达三相PFC源程序 艾默生充电桩15kw模块+台达三相PFC源码,软件源码加原理 图BOM

    艾默生充电15kw+台达三相PFC源程序 艾默生充电桩15kw模块+台达三相PFC源码,软件源码加原理 图BOM 艾默生充电桩15kw模块原版软件源码含核心算法,PFC+DCDC双DSP数字控制,原理 ...

  10. 【Linux后台开发系列】Nginx源码从模块开发开始,不再对nginx源码陌生丨源码分析

    Nginx源码从模块开发开始,不再对nginx源码发怵,值得学习,认真听完. 1.  nginx的conf配置,cmd解析 2.  nginx模块的八股文 3.  nginx开发的细枝末节 [Linu ...

最新文章

  1. Leetcode 102. 二叉树的层次遍历 解题思路及C++实现
  2. Python开发【Part 11】:线程与进程
  3. kafka整理笔记笔记
  4. 在 IntelliJ IDEA 中,如何快速将选中文件用资源管理器打开
  5. mysql8.0提示命令_Mysql 8.0 相关命令
  6. 我与Linux系统的交集
  7. C# 答群友:把窗体应用改成类库输出然后去引用
  8. 公里与英里的换算c语言函数_60迈=60码=60公里?这三者天壤之别,可别搞错了
  9. php 判断下载状态,php下获取http状态的实现代码
  10. Bash-Shell-02
  11. 网络工程师的人生之路是这样的开始的!
  12. java创建按钮_java中制作一个按钮需要那些步骤
  13. html5 连连看小游戏
  14. python实现百度翻译
  15. 计算机中的颗粒度(granularity)什么是颗粒度?
  16. 简单高效的图片降噪方法
  17. @CacheEvict
  18. html5页面拨打电话,5.添加页面/设置点击拨打电话
  19. 用人工智能设计超酷T恤,除了那专属感,还透露了这些时装设计大趋势 || 万有AI...
  20. 腾讯技术分享:微信小程序音视频技术背后的故事

热门文章

  1. (error) ERR wrong number of arguments for 'hmget' command
  2. 页面上显示的带有乱码名称的js文件是怎么回事?
  3. 哑编码官方代码自己的注解
  4. C++PrimerCH1
  5. ## 7.3 奇异值分解的几何意义
  6. nvml.dll 英伟达公司提供的动态库用途
  7. 机器学习 数据增加_【机器学习】数据降维概述
  8. python桌面开发吐血_Python3环境(Windows10)单独配置Spyder——记录我的吐血之路...
  9. 项目中最常用到的颜色
  10. Android数据库存放的具体位置