Jacoco探针源码解析(0.8.5 版本)
发布日期:2021-06-30 12:23:36 浏览次数:3 分类:技术文章

本文共 24168 字,大约阅读时间需要 80 分钟。

目录

0 前言

全是干货的技术殿堂

文章收录在我的 GitHub 仓库,欢迎Star/fork:

https://github.com/Wasabi1234/Java-Interview-Tutorial

1 jacoco agent入口类

查看 MANIFEST.MF 文件

入口类—PreMain 通过 agent 参数和命令行配置,通过 JVM 初始化 agent 时调用,使得 javaagent 得以在 JVM 启动后,应用启动前载入,通知自己 ok 且 javaagent 可用于 instrument 和 transform 字节码了

  • 参数 Instrumentation类是java.lang 下面的类
  • 然后调用了CoverageTransformer 类,这个类和 premain同一个包下面:
/******************************************************************************* * Copyright (c) 2009, 2020 Mountainminds GmbH & Co. KG and Contributors * This program and the accompanying materials are made available under * the terms of the Eclipse Public License 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0 * * SPDX-License-Identifier: EPL-2.0 * * Contributors: *    Marc R. Hoffmann - initial API and implementation * *******************************************************************************/package org.jacoco.agent.rt.internal;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.CodeSource;import java.security.ProtectionDomain;import org.jacoco.core.instr.Instrumenter;import org.jacoco.core.runtime.AgentOptions;import org.jacoco.core.runtime.IRuntime;import org.jacoco.core.runtime.WildcardMatcher;/** * Class file transformer to instrument classes for code coverage analysis. */public class CoverageTransformer implements ClassFileTransformer {
private static final String AGENT_PREFIX; static {
final String name = CoverageTransformer.class.getName(); AGENT_PREFIX = toVMName(name.substring(0, name.lastIndexOf('.'))); } 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; /** * New transformer with the given delegates. * * @param runtime * coverage runtime * @param options * configuration options for the generator * @param logger * logger for exceptions during instrumentation */ public CoverageTransformer(final IRuntime runtime, final AgentOptions options, final IExceptionLogger logger) {
this.instrumenter = new Instrumenter(runtime); this.logger = logger; // Class names will be reported in VM notation: includes = new WildcardMatcher(toVMName(options.getIncludes())); excludes = new WildcardMatcher(toVMName(options.getExcludes())); exclClassloader = new WildcardMatcher(options.getExclClassloader()); classFileDumper = new ClassFileDumper(options.getClassDumpDir()); inclBootstrapClasses = options.getInclBootstrapClasses(); inclNoLocationClasses = options.getInclNoLocationClasses(); } // 这个类中的关键方法: public byte[] transform(final ClassLoader loader, final String classname, final Class
classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) throws IllegalClassFormatException {
// We do not support class retransformation: if (classBeingRedefined != null) {
return null; } if (!filter(loader, classname, protectionDomain)) {
return null; } try {
classFileDumper.dump(classname, classfileBuffer); return instrumenter.instrument(classfileBuffer, classname); } catch (final Exception ex) {
final IllegalClassFormatException wrapper = new IllegalClassFormatException( ex.getMessage()); wrapper.initCause(ex); // Report this, as the exception is ignored by the JVM: logger.logExeption(wrapper); throw wrapper; } } /** * Checks whether this class should be instrumented. * * @param loader * loader for the class * @param classname * VM name of the class to check * @param protectionDomain * protection domain for the class * @return true if the class should be instrumented */ boolean filter(final ClassLoader loader, final String classname, final ProtectionDomain protectionDomain) {
if (loader == null) {
if (!inclBootstrapClasses) {
return false; } } else {
if (!inclNoLocationClasses && !hasSourceLocation(protectionDomain)) {
return false; } if (exclClassloader.matches(loader.getClass().getName())) {
return false; } } return !classname.startsWith(AGENT_PREFIX) && includes.matches(classname) && !excludes.matches(classname); } /** * Checks whether this protection domain is associated with a source * location. * * @param protectionDomain * protection domain to check (or null) * @return true if a source location is defined */ private boolean hasSourceLocation(final ProtectionDomain protectionDomain) {
if (protectionDomain == null) {
return false; } final CodeSource codeSource = protectionDomain.getCodeSource(); if (codeSource == null) {
return false; } return codeSource.getLocation() != null; } private static String toVMName(final String srcName) {
return srcName.replace('.', '/'); }}

transform 方法调用了

classFileDumper 类

ClassFileDumper.java/******************************************************************************* * Copyright (c) 2009, 2020 Mountainminds GmbH & Co. KG and Contributors * This program and the accompanying materials are made available under * the terms of the Eclipse Public License 2.0 which is available at * http://www.eclipse.org/legal/epl-2.0 * * SPDX-License-Identifier: EPL-2.0 * * Contributors: *    Marc R. Hoffmann - initial API and implementation * *******************************************************************************/package org.jacoco.agent.rt.internal;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.io.OutputStream;import org.jacoco.core.internal.data.CRC64;/** * 对class文件的内部dumper程序。 */class ClassFileDumper {
private final File location; /** * Create a new dumper for the given location. * * @param location * relative path to dump directory. null if no dumps * should be written */ ClassFileDumper(final String location) {
if (location == null) {
this.location = null; } else {
this.location = new File(location); } } /** * 如果指定了非空位置,则以给定名称 dump 给定二进制内容。 * * @param name * qualified class name in VM notation */ void dump(final String name, final byte[] contents) throws IOException {
if (location != null) {
final File outputdir; final String localname; final int pkgpos = name.lastIndexOf('/'); if (pkgpos != -1) {
outputdir = new File(location, name.substring(0, pkgpos)); localname = name.substring(pkgpos + 1); } else {
outputdir = location; localname = name; } outputdir.mkdirs(); final Long id = Long.valueOf(CRC64.classId(contents)); final File file = new File(outputdir, String.format("%s.%016x.class", localname, id)); final OutputStream out = new FileOutputStream(file); out.write(contents); out.close(); } }}

这个类将class out 输出,上面transform方法在调用的时候同时使用:

ClassAnalyzer调用

public class ClassAnalyzer extends ClassProbesVisitor implements IFilterContext {
private final ClassCoverageImpl coverage; private final boolean[] probes; private final StringPool stringPool; private final Set
classAnnotations = new HashSet(); public ClassAnalyzer(ClassCoverageImpl coverage, boolean[] probes, StringPool stringPool) {
this.coverage = coverage; this.probes = probes; this.stringPool = stringPool; } public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
this.coverage.setSignature(this.stringPool.get(signature)); this.coverage.setSuperName(this.stringPool.get(superName)); this.coverage.setInterfaces(this.stringPool.get(interfaces)); } public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
this.classAnnotations.add(desc); return super.visitAnnotation(desc, visible); } public void visitSource(String source, String debug) {
this.coverage.setSourceFileName(this.stringPool.get(source)); } public MethodProbesVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
InstrSupport.assertNotInstrumented(name, this.coverage.getName()); return new MethodAnalyzer(this.stringPool.get(name), this.stringPool.get(desc), this.stringPool.get(signature), this.probes, Filters.ALL, this) {
public void visitEnd() {
super.visitEnd(); IMethodCoverage methodCoverage = this.getCoverage(); if (methodCoverage.getInstructionCounter().getTotalCount() > 0) {
ClassAnalyzer.this.coverage.addMethod(methodCoverage); } } }; } public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
InstrSupport.assertNotInstrumented(name, this.coverage.getName()); return super.visitField(access, name, desc, signature, value); }

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;

转载地址:https://javaedge.blog.csdn.net/article/details/104986841 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:Java的Instrumentation类原理分析
下一篇:Java电商系统商品详情页存储方案设计

发表评论

最新留言

哈哈,博客排版真的漂亮呢~
[***.90.31.176]2024年04月05日 10时59分24秒