对类的植入锁定进行判断

几个可以对覆盖率跟踪的Java类定义进行instrument的API

public byte[] instrument(final ClassReader reader) {
final ClassWriter writer = new ClassWriter(reader, 0) {
@Override
protected String getCommonSuperClass(final String type1,
final String type2) {
throw new IllegalStateException();
}
};
final IProbeArrayStrategy strategy = ProbeArrayStrategyFactory
.createFor(reader, accessorGenerator);
final ClassVisitor visitor = new ClassProbesAdapter(
new ClassInstrumenter(strategy, writer), true);
reader.accept(visitor, ClassReader.EXPAND_FRAMES);
return writer.toByteArray();
}
public byte[] instrument(final byte[] buffer, final String name)
throws IOException {
try {
return instrument(new ClassReader(buffer));
} catch (final RuntimeException e) {
throw instrumentError(name, e);
}
}
public byte[] instrument(final InputStream input, final String name)
throws IOException {
final byte[] bytes;
try {
bytes = InputStreams.readFully(input);
} catch (final IOException e) {
throw instrumentError(name, e);
}
return instrument(bytes, name);
}
public void instrument(final InputStream input, final OutputStream output,
final String name) throws IOException {
output.write(instrument(input, name));
}
private IOException instrumentError(final String name,
final Exception cause) {
final IOException ex = new IOException(
String.format("Error while instrumenting %s.", name));
ex.initCause(cause);
return ex;
}
  • 执行流程图:
    Java代码覆盖率框架JaCoCo的core-instr core.internal.instr 包类源码解析_数组

所以最终的出口在于最下面的instrument(input,output,string),下面是剩余部分代码:

public int instrumentAll(final InputStream input, final OutputStream output,
final String name) throws IOException {
final ContentTypeDetector detector;
try {
detector = new ContentTypeDetector(input);
} catch (final IOException e) {
throw instrumentError(name, e);
}
switch (detector.getType()) {
case ContentTypeDetector.CLASSFILE:
instrument(detector.getInputStream(), output, name);
return 1;
case ContentTypeDetector.ZIPFILE:
return instrumentZip(detector.getInputStream(), output, name);
case ContentTypeDetector.GZFILE:
return instrumentGzip(detector.getInputStream(), output, name);
case ContentTypeDetector.PACK200FILE:
return instrumentPack200(detector.getInputStream(), output, name);
default:
copy(detector.getInputStream(), output, name);
return 0;
}
}
private int instrumentZip(final InputStream input,
final OutputStream output, final String name) throws IOException {
final ZipInputStream zipin = new ZipInputStream(input);
final ZipOutputStream zipout = new ZipOutputStream(output);
ZipEntry entry;
int count = 0;
while ((entry = nextEntry(zipin, name)) != null) {
final String entryName = entry.getName();
if (signatureRemover.removeEntry(entryName)) {
continue;
}

zipout.putNextEntry(new ZipEntry(entryName));
if (!signatureRemover.filterEntry(entryName, zipin, zipout)) {
count += instrumentAll(zipin, zipout, name + "@" + entryName);
}
zipout.closeEntry();
}
zipout.finish();
return count;
}
private ZipEntry nextEntry(final ZipInputStream input,
final String location) throws IOException {
try {
return input.getNextEntry();
} catch (final IOException e) {
throw instrumentError(location, e);
}
}
private int instrumentGzip(final InputStream input,
final OutputStream output, final String name) throws IOException {
final GZIPInputStream gzipInputStream;
try {
gzipInputStream = new GZIPInputStream(input);
} catch (final IOException e) {
throw instrumentError(name, e);
}
final GZIPOutputStream gzout = new GZIPOutputStream(output);
final int count = instrumentAll(gzipInputStream, gzout, name);
gzout.finish();
return count;
}
private int instrumentPack200(final InputStream input,
final OutputStream output, final String name) throws IOException {
final InputStream unpackedInput;
try {
unpackedInput = Pack200Streams.unpack(input);
} catch (final IOException e) {
throw instrumentError(name, e);
}
final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
final int count = instrumentAll(unpackedInput, buffer, name);
Pack200Streams.pack(buffer.toByteArray(), output);
return count;
}
private void copy(final InputStream input, final OutputStream output,
final String name) throws IOException {
final byte[] buffer = new byte[1024];
int len;
while ((len = read(input, buffer, name)) != -1) {
output.write(buffer, 0, len);
}
}
private int read(final InputStream input, final byte[] buffer,
final String name) throws IOException {
try {
return input.read(buffer);
} catch (final IOException e) {
throw instrumentError(name, e);
}
}

核心关键是instrumentAll这个方法,根据文件包是class还是zip,或者gz等,不同的加载方式。

ClassInstrumenter 类

适配器为了类覆盖率跟踪。

import org.jacoco.core.internal.flow.ClassProbesVisitor;
import org.jacoco.core.internal.flow.MethodProbesVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;

/**
* Adapter that instruments a class for coverage tracing.
*/
public class ClassInstrumenter extends ClassProbesVisitor {

private final IProbeArrayStrategy probeArrayStrategy;

private String className;

/**
* Emits a instrumented version of this class to the given class visitor.
* 向给定的class 访问者发出此类的instrumented版本
*
* @param probeArrayStrategy
* 该策略将用于访问探针数组
* @param cv
* 访问链中的下一位 delegate 将获得 instrumente 类
*/
public ClassInstrumenter(final IProbeArrayStrategy probeArrayStrategy,
final ClassVisitor cv) {
super(cv);
this.probeArrayStrategy = probeArrayStrategy;
}

@Override
public void visit(final int version, final int access, final String name,
final String signature, final String superName,
final String[] interfaces) {
this.className = name;
super.visit(version, access, name, signature, superName, interfaces);
}

@Override
public FieldVisitor visitField(final int access, final String name,
final String desc, final String signature, final Object value) {
InstrSupport.assertNotInstrumented(name, className);
return super.visitField(access, name, desc, signature, value);
}

@Override
public MethodProbesVisitor visitMethod(final int access, final String name,
final String desc, final String signature,
final String[] exceptions) {

InstrSupport.assertNotInstrumented(name, className);

final MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
exceptions);

if (mv == null) {
return null;
}
final MethodVisitor frameEliminator = new DuplicateFrameEliminator(mv);
final ProbeInserter probeVariableInserter = new ProbeInserter(access,
name, desc, frameEliminator, probeArrayStrategy);
return new MethodInstrumenter(probeVariableInserter,
probeVariableInserter);
}

@Override
public void visitTotalProbeCount(final int count) {
probeArrayStrategy.addMembers(cv, count);
}

}

DuplicateFrameEliminator

消除了导致ASM创建无效类文件的连续 stackmap frames 定义。 当原始类文件在意外偏移处包含其他 stackmap frames 时,就会发生这种情况,某些使用ECJ编译的类文件就是这种情况。

ProbeInserter - 探针植入类

Java代码覆盖率框架JaCoCo的core-instr core.internal.instr 包类源码解析_数组_02

内部实用程序,用于将探针添加到方法的控制流中。

探针的代码只是将布尔数组的某个插槽设置为true。

另外,必须在方法开始时检索探针数组并将其存储在局部变量中。

构造方法

  • 创建一个新的ProbeInserter
/**
*
* @param access
* access flags of the adapted method
* @param name
* the method's name
* @param desc
* the method's descriptor
* @param mv
* the method visitor to which this adapter delegates calls
* @param arrayStrategy
* callback to create the code that retrieves the reference to
* the probe array
*/

Java代码覆盖率框架JaCoCo的core-instr core.internal.instr 包类源码解析_数组_03

visitmax

探针代码的最大堆栈大小为3,这可以增加到原始堆栈大小,具体取决于探针位置。 访问者堆栈大小是绝对最大值,因为当堆栈大小为空时,访问者代码会在每种方法的开头插入。

@Override
public void visitMaxs(final int maxStack, final int maxLocals) {
final int increasedStack = Math.max(maxStack + 3, accessorStackSize);
mv.visitMaxs(increasedStack, maxLocals + 1);
}

insertProbe - 插入具有给定id的探针

Java代码覆盖率框架JaCoCo的core-instr core.internal.instr 包类源码解析_ide_04

Java代码覆盖率框架JaCoCo的core-instr core.internal.instr 包类源码解析_堆栈_05

visitIincInsn - 访问 IINC 指令

Java代码覆盖率框架JaCoCo的core-instr core.internal.instr 包类源码解析_ide_06

visitLocalVariable - 访问局部变量声明

Visits a local variable declaration.

Java代码覆盖率框架JaCoCo的core-instr core.internal.instr 包类源码解析_堆栈_07

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

大致就是,在对应字节码的执行入口和跳转入口处,置放 probe,是一个数值(该数值和probe id有关),入栈后加1,则记录一次执行


  • 所有放入的探针对应一个boolean[]
  • 探针入栈之后,那么boolean[] 对应的位置变成true,记录执行了。

InstrSupport 类原理

Constants and utilities for byte code instrumentation

字节码检测的常量和实用程序。

属性

public static final int ASM_API_VERSION = Opcodes.ASM7;
  • 接口初始化方法的名称。
static final String CLINIT_NAME = "<clinit>";

存储类的boolean[]数组的coverage信息的字段的数据类型

public static final String DATAFIELD_DESC = "[Z";

初始化方法的名称。

public static final String INITMETHOD_NAME = "$jacocoInit";

初始化方法的描述符。

public static final String INITMETHOD_DESC = "()[Z";
/**
* Access modifiers of the initialization method.
*/
public static final int INITMETHOD_ACC = Opcodes.ACC_SYNTHETIC
| Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC;

needsFrames

/**
* 确定给定的 class 文件版本是否需要 stackmap frames.
*
* @param version
* class file version
* @return <code>true</code> if frames are required
*/
public static boolean needsFrames(final int version) {
// consider major version only (due to 1.1 anomaly)
return (version & 0xFFFF) >= Opcodes.V1_6;
}

classReaderFor

/**
* Creates a {@link ClassReader} instance for given bytes of class even if
* its version not yet supported by ASM.
*
* @param b
* bytes of class
* @return {@link ClassReader}
*/
public static ClassReader classReaderFor(final byte[] b) {
final int originalVersion = getMajorVersion(b);
if (originalVersion == Opcodes.V14 + 1) {
// temporarily downgrade version to bypass check in ASM
setMajorVersion(Opcodes.V14, b);
}
final ClassReader classReader = new ClassReader(b);
setMajorVersion(originalVersion, b);
return classReader;
}

assertNotInstrumented

Ensures that the given member does not correspond to a internal member created by the instrumentation process. This would mean that the class is already instrumented.

确保给定成员与 instrumentation 过程创建的内部成员不对应。 这意味着该类已经被检测。

Java代码覆盖率框架JaCoCo的core-instr core.internal.instr 包类源码解析_ide_08

push

Generates the instruction to push the given int value on the stack.

Implementation taken from org.objectweb.asm.commons.GeneratorAdapter#push(int)

生成指令以将给定的int值压入堆栈。

取自org.objectweb.asm.commons.GeneratorAdapter#push(int)的实现Java代码覆盖率框架JaCoCo的core-instr core.internal.instr 包类源码解析_ide_09

Push是用来对于不同的变量值入栈的不同方式,当int取值


  • -1 ~ 5,JVM采用iconst指令将常量压入栈中
  • -128 ~ 127,bipush
  • -32768 ~ 32767,sipush
  • -2147483648~2147483647,ldc

主要作为单例的使用,ClassInstrumenter, ClassAnalyzer调用InstrSupport

ClassAnalyzer 类调用如下:

public static final String DATAFIELD_DESC = "[Z";

// === Init Method ===

/**
* Name of the initialization method.
*/
public static final String INITMETHOD_NAME = "$jacocoInit";

/**
* Descriptor of the initialization method.
*/
public static final String INITMETHOD_DESC = "()[Z";
public static void assertNotInstrumented(final String member,
final String owner) throws IllegalStateException {
if (member.equals(DATAFIELD_NAME) || member.equals(INITMETHOD_NAME)) {
throw new IllegalStateException(format(
"Class %s is already instrumented.", owner));
}
}

IProbeArrayStrategy

检索类型内每个方法的探针数组实例的策略。 这种抽象是必需的,因为我们需要根据所检测的类型是类还是接口来遵循不同的策略。

storeInstance

/**
* Creates code that stores the probe array instance in the given variable.
*
* @param mv
* visitor to create code
* @param clinit
* true in case of {@code <clinit>} method
* @param variable
* variable index to store probe array to
* @return maximum stack size required by the generated code
*/
int storeInstance(MethodVisitor mv, boolean clinit, int variable);

创建将探针数组实例存储在给定变量中的代码。