ClassInstrumenter
Adapter that instruments a class for coverage tracing.
擴充卡為類覆寫率跟蹤。
@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);
}
在jacoco對類和方法進行植入的時候,會
instrumenter - 對類的植入鎖定進行判斷
Several APIs to instrument Java class definitions for coverage tracing.
幾個API可以對覆寫範圍跟蹤的Java類定義進行檢測。
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;
}
instrument(input,output,string) => instrument(input,string) => instrument([],string) => instrument(classreader)
是以最終的出口在于最下面的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等,不同的加載方式。
loadclass入口類:CoverageTransformer
public class CoverageTransformer
implements ClassFileTransformer
{
private static final String AGENT_PREFIX;
private final Instrumenter instrumenter;
private final IExceptionLogger logger;
private final WildcardMatcher includes;
private final WildcardMatcher excludes;
private final WildcardMatcher exclClassloader;
private final ClassFileDumper classFileDumper;
private final boolean inclBootstrapClasses;
private final boolean inclNoLocationClasses;
static
{
String name = CoverageTransformer.class.getName();
AGENT_PREFIX = toVMName(name.substring(0, name.lastIndexOf('.')));
}
public CoverageTransformer(IRuntime runtime, AgentOptions options, IExceptionLogger logger)
{
this.instrumenter = new Instrumenter(runtime);
this.logger = logger;
this.includes = new WildcardMatcher(toVMName(options.getIncludes()));
this.excludes = new WildcardMatcher(toVMName(options.getExcludes()));
this.exclClassloader = new WildcardMatcher(options.getExclClassloader());
this.classFileDumper = new ClassFileDumper(options.getClassDumpDir());
this.inclBootstrapClasses = options.getInclBootstrapClasses();
this.inclNoLocationClasses = options.getInclNoLocationClasses();
}
public byte[] transform(ClassLoader loader, String classname, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException
{
if (classBeingRedefined != null) {
return null;
}
if (!filter(loader, classname, protectionDomain)) {
return null;
}
try
{
this.classFileDumper.dump(classname, classfileBuffer);
return this.instrumenter.instrument(classfileBuffer, classname);
}
catch (Exception ex)
{
IllegalClassFormatException wrapper = new IllegalClassFormatException(ex.getMessage());
wrapper.initCause(ex);
this.logger.logExeption(wrapper);
throw wrapper;
}
}
boolean filter(ClassLoader loader, String classname, ProtectionDomain protectionDomain)
{
if (loader == null)
{
if (!this.inclBootstrapClasses) {
return false;
}
}
else
{
if ((!this.inclNoLocationClasses) && (!hasSourceLocation(protectionDomain))) {
return false;
}
if (this.exclClassloader.matches(loader.getClass().getName())) {
return false;
}
}
return (!classname.startsWith(AGENT_PREFIX)) && (this.includes.matches(classname)) && (!this.excludes.matches(classname));
}
private boolean hasSourceLocation(ProtectionDomain protectionDomain)
{
if (protectionDomain == null) {
return false;
}
CodeSource codeSource = protectionDomain.getCodeSource();
if (codeSource == null) {
return false;
}
return codeSource.getLocation() != null;
}
private static String toVMName(String srcName)
{
return srcName.replace('.', '/');
}
}
核心的兩條代碼:transform
try{
this.classFileDumper.dump(classname, classfileBuffer);
return this.instrumenter.instrument(classfileBuffer, classname);
}
Jaococ使用asm實作位元組碼植入,是對指令級别上的位元組碼植入,進而可以定位到執行的代碼行,以達到覆寫率的統計。
flow 包分析
接着看
Instruction
/*******************************************************************************
* Copyright (c) 2009, 2018 Mountainminds GmbH & Co. KG and Contributors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Marc R. Hoffmann - initial API and implementation
*
*******************************************************************************/
package org.jacoco.core.internal.flow;
import org.objectweb.asm.tree.AbstractInsnNode;
import java.util.BitSet;
/**
* Representation of a byte code instruction for analysis. Internally used for
* analysis.
*/
public class Instruction {
private final AbstractInsnNode node;
private final int line;
private int branches;
private final BitSet coveredBranches;
private Instruction predecessor;
private int predecessorBranch;
/**
* New instruction at the given line.
*
* @param node
* corresponding node
* @param line
* source line this instruction belongs to
*/
public Instruction(final AbstractInsnNode node, final int line) {
this.node = node;
this.line = line;
this.branches = 0;
this.coveredBranches = new BitSet();
}
/**
* @return corresponding node
*/
public AbstractInsnNode getNode() {
return node;
}
/**
* Adds an branch to this instruction.
*/
public void addBranch() {
branches++;
}
/**
* Sets the given instruction as a predecessor of this instruction and adds
* branch to the predecessor. Probes are inserted in a way that every
* instruction has at most one direct predecessor.
*
* @see #addBranch()
* @param predecessor
* predecessor instruction
* @param branch
* branch number in predecessor that should be marked as covered
* when this instruction marked as covered
*/
public void setPredecessor(final Instruction predecessor,
final int branch) {
this.predecessor = predecessor;
predecessor.addBranch();
this.predecessorBranch = branch;
}
/**
* Marks one branch of this instruction as covered. Also recursively marks
* all predecessor 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;
}
}
/**
* Returns the source line this instruction belongs to.
*
* @return corresponding source line
*/
public int getLine() {
return line;
}
/**
* Returns the total number of branches starting from this instruction.
*
* @return total number of branches
*/
public int getBranches() {
return branches;
}
/**
* Returns the number of covered branches starting from this instruction.
*
* @return number of covered branches
*/
public int getCoveredBranches() {
return coveredBranches.cardinality();
}
/**
* Merges information about covered branches of given instruction into this
* instruction.
*
* @param instruction
* instruction from which to merge
*/
public void merge(Instruction instruction) {
this.coveredBranches.or(instruction.coveredBranches);
}
@Override
public String toString() {
return coveredBranches.toString();
}
}
記錄對應指令的代碼行,記錄在跳轉的label處對應的代碼行數,那麼類推可以等到整個覆寫和未覆寫的代碼行。
接着看看具體植入的是什麼指令
org.jacoco.core.internal.instr.ProbeArrayStrategyFactory
工廠:用于尋找合适的政策來通路給定類的探針數組
createFor()
為給定 reader 描述的 class 建立合适的政策執行個體。 建立的執行個體隻能用于處理為其建立了執行個體的類或接口,并且隻能使用一次。
/**
* @param classId
* class identifier
* @param reader
* reader to get information about the class
* @param accessorGenerator
* accessor to the coverage runtime
* @return strategy instance
*/
public static IProbeArrayStrategy createFor(final long classId,
final ClassReader reader,
final IExecutionDataAccessorGenerator accessorGenerator) {
final String className = reader.getClassName();
final int version = InstrSupport.getMajorVersion(reader);
if (isInterfaceOrModule(reader)) {
final ProbeCounter counter = getProbeCounter(reader);
if (counter.getCount() == 0) {
return new NoneProbeArrayStrategy();
}
if (version >= Opcodes.V11 && counter.hasMethods()) {
return new CondyProbeArrayStrategy(className, true, classId,
accessorGenerator);
}
if (version >= Opcodes.V1_8 && counter.hasMethods()) {
return new InterfaceFieldProbeArrayStrategy(className, classId,
counter.getCount(), accessorGenerator);
} else {
return new LocalProbeArrayStrategy(className, classId,
counter.getCount(), accessorGenerator);
}
} else {
if (version >= Opcodes.V11) {
return new CondyProbeArrayStrategy(className, false, classId,
accessorGenerator);
}
return new ClassFieldProbeArrayStrategy(className, classId,
InstrSupport.needsFrames(version), accessorGenerator);
}
}
IProbeArrayStrategy
其實作類如下:

isInterfaceOrModule
private static boolean isInterfaceOrModule(final ClassReader reader) {
return (reader.getAccess()
& (Opcodes.ACC_INTERFACE | Opcodes.ACC_MODULE)) != 0;
}
ClassFieldProbeArrayStrategy
正常類的政策添加了一個用于儲存探針數組的 static 字段和一個從運作時請求探針數組的靜态初始化方法。
屬性
/**
* Frame stack with a single boolean array.
*/
private static final Object[] FRAME_STACK_ARRZ = new Object[] {
InstrSupport.DATAFIELD_DESC };
/**
* Empty frame locals.
*/
private static final Object[] FRAME_LOCALS_EMPTY = new Object[0];
private final String className;
private final long classId;
private final boolean withFrames;
private final IExecutionDataAccessorGenerator accessorGenerator;