天天看點

Jacoco探針源碼解析(0.8.5 版本)(下)instrumenter - 對類的植入鎖定進行判斷loadclass入口類:CoverageTransformerInstructionIProbeArrayStrategy

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

其實作類如下:

Jacoco探針源碼解析(0.8.5 版本)(下)instrumenter - 對類的植入鎖定進行判斷loadclass入口類:CoverageTransformerInstructionIProbeArrayStrategy

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;