Code path:
org.jacoco.core.instr.Instrumenter
Entry function:
private byte[] instrument(final byte[] source) { 1. final long classId = CRC64.classId(source); 2. final ClassReader reader = InstrSupport.classReaderFor(source); 3. final ClassWriter writer = new ClassWriter(reader, 0) { @Override protected String getCommonSuperClass(final String type1, final String type2) { throw new IllegalStateException(); } }; 4. final IProbeArrayStrategy strategy = ProbeArrayStrategyFactory .createFor(classId, reader, accessorGenerator); 5. final int version = InstrSupport.getMajorVersion(reader); 6. final ClassVisitor visitor = new ClassProbesAdapter( new ClassInstrumenter(strategy, writer), InstrSupport.needsFrames(version)); 7. reader.accept(visitor, ClassReader.EXPAND_FRAMES); 8. return writer.toByteArray(); }
Step 1: generate classId
classId (class identifier) is a 64 bit integer, such as 0x638e104737889183. It is generated by calculating the CRC64 of the original class file and is used to uniquely identify a class. It is used to identify the java coverage of the class and stored in the jacobo.exec file. When parsing the coverage data, the coverage data is associated with the original class through the class identifier.
classId please refer to the article for details https://blog.csdn.net/tyut_aa/article/details/108003892
Step 2 and step 3: load the asm core class library
The implementation of Jacobo plug-in adopts Asm, which is a java bytecode modification framework. After loading class information through ClassReader, it parses class information, modifies class behavior, and outputs it through ClassWriter.
Core class library:
- ClassReader: this class is used to parse compiled class bytecode files.
- ClassWriter: this class is used to rebuild the compiled class, for example, modify the class name, properties and methods, and even generate a bytecode file of a new class.
- ClassAdapter: this class also implements the ClassVisitor interface, which delegates method calls to another ClassVisitor object.
step4: creating a pile insertion strategy
Parse the bytecode type and create the corresponding pile insertion strategy. The pile insertion strategies include:
-
NoneProbeArrayStrategy: empty pile insertion strategy
-
CondyProbeArrayStrategy: class interface instrumentation strategy containing methods in Java version 11 and above
-
InterfaceFieldProbeArrayStrategy: the interface instrumentation strategy of java8 and above including methods
-
LocalProbeArrayStrategy: Instrumentation strategy for anonymous interface class methods
-
ClassFieldProbeArrayStrategy: class instrumentation strategy of java version 8 and above
Step 5: read the major version number of the class
Java6 introduces the Stack Map Frames feature (optional) to track the type of local variable table and operand type in bytecode instructions, which is more convenient for type verification when the JVM is loaded. Java7 introduces this feature by default.
step6: create a Jacobo instrumentation proxy class
ClassProbesAdapter inherits the ClassVisitor class, mainly the Visitor method class of the execution agent
Core class library:
ClassProbesAdapter: the proxy class of ClassVisitor, which executes the method of proxy ClassVisitor, such as:
Proxy ProbeCounter gets the insert count:
private static ProbeCounter getProbeCounter(final ClassReader reader) { final ProbeCounter counter = new ProbeCounter(); reader.accept(new ClassProbesAdapter(counter, false), 0); return counter; }
Insert piles on behalf of pile inserting class ClassInstrumenter:
final ClassVisitor visitor = new ClassProbesAdapter( new ClassInstrumenter(strategy, writer), InstrSupport.needsFrames(version)); reader.accept(visitor, ClassReader.EXPAND_FRAMES);
ClassInstrumenter core code:
@Override public MethodProbesVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) { 1. InstrSupport.assertNotInstrumented(name, className); 2. final MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); if (mv == null) { return null; } 3. final MethodVisitor frameEliminator = new DuplicateFrameEliminator(mv); 4. final ProbeInserter probeVariableInserter = new ProbeInserter(access, name, desc, frameEliminator, probeArrayStrategy); 5. return new MethodInstrumenter(probeVariableInserter, probeVariableInserter); }
step1: judge whether the pile has been inserted
Step 2: call the visiteMethod method of the proxy cv, which refers to ClassWriter to obtain the byte code stream
step3: create a Stack Map Frames property processing class
step4: Create method instrumentation execution class
Execute the specific pile insertion action and modify the pile array tag
step5: Create method instrumentation class
The pile insertion logic of the method is controlled in this class. If pile insertion is required, call the pile insertion execution class of step4
step6: I just want to insert piles
step7: output the class after pile insertion