Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Who instruments the instrumenters?

Who instruments the instrumenters?

Avatar for Johannes Bechberger

Johannes Bechberger

June 05, 2025
Tweet

Transcript

  1. Code Coverage public class DiscountCalculator { public int calculateDiscount(int price,

    boolean isMember) { if (price > 100) { return isMember ? 20 : 10; } return isMember ? 5 : 0; } }
  2. Code Coverage class DiscountCalculatorTest { @Test void calculateDiscount() { DiscountCalculator

    calc = new DiscountCalculator(); assertEquals(0, calc.calculateDiscount(50, false)); assertEquals(5, calc.calculateDiscount(50, true)); } }
  3. Code Coverage public class DiscountCalculator { public int calculateDiscount(int price,

    boolean isMember) { if (price > 100) { return isMember ? 20 : 10; } return isMember ? 5 : 0; } } executed in the tests?
  4. Code Coverage public class DiscountCalculator { public int calculateDiscount(int price,

    boolean isMember) { if (price > 100) { return isMember ? 20 : 10; } return isMember ? 5 : 0; } } log(…) log(…) log(…)
  5. Debugging public class DiscountCalculator { public int calculateDiscount(int price, boolean

    isMember) { if (price > 100) { return isMember ? 20 : 10; } return isMember ? 5 : 0; } }
  6. Debugging public class DiscountCalculator { public int calculateDiscount(int price, boolean

    isMember) { if (price > 100) { return isMember ? 20 : 10; } return isMember ? 5 : 0; } } dbg() dgb() dbg()
  7. Debugging public class DiscountCalculator { public int calculateDiscount(int price, boolean

    isMember) { if (price > 100) { return isMember ? 20 : 10; } return isMember ? 5 : 0; } } dbg() dgb() dbg() Not in Java
  8. Debugging public class DiscountCalculator { public int calculateDiscount(int price, boolean

    isMember) { if (price > 100) { return isMember ? 20 : 10; } return isMember ? 5 : 0; } } dbg() dgb() dbg() But in Python* https://mostlynerdless.de/blog/tag/lets-create-a-debugger-together/
  9. Example public class DiscountCalculator { public int calculateDiscount(int price, boolean

    isMember) { if (price > 100) { return isMember ? 20 : 10; } return isMember ? 5 : 0; } }
  10. Example public class Main { public static void main(String[] args)

    { DiscountCalculator calculator = new DiscountCalculator(); use(calculator.calculateDiscount(150, true)); } }
  11. Example public class Main { public static void main(String[] args)

    { DiscountCalculator calculator = new DiscountCalculator(); System.out.println("cd(" + 150 + ", " + true + ")"); use(calculator.calculateDiscount(150, true)); } }
  12. public class AgentMain { public static void premain(String agentArgs, Instrumentation

    inst) { inst.addTransformer(new DiscountTransformer()); } } -javaattach:agent.jar=args
  13. DiscountTransformer extends ClassFileTransformer “ An agent registers an implementation of

    this interface using the addTransformer method so that the transformer’s transform method is invoked when classes are loaded, redefined, or retransformed – ClassFileTransformer Documentation
  14. class DiscountTransformer implements ClassFileTransformer { private final ScopedClassPoolFactoryImpl scopedClassPoolFactory =

    new ScopedClassPoolFactoryImpl(); @Override public byte[] transform(!!...) { ClassPool cp = scopedClassPoolFactory.create(loader, …); CtClass cc = cp.makeClass(new ByteArrayInputStream(buffer)); transform(cc); return cc.toBytecode(); }
  15. class DiscountTransformer implements ClassFileTransformer { private void transform(CtClass cc){ var

    exprEditor = new DiscountEditor(); !// !!... for (CtMethod method : cc.getDeclaredMethods()) { method.instrument(exprEditor); } } }
  16. class DiscountTransformer implements ClassFileTransformer { private void transform(CtClass cc){ var

    exprEditor = new DiscountEditor(); cc.instrument(exprEditor); } }
  17. Class Transformer class DiscountEditor extends ExprEditor { @Override public void

    edit(MethodCall m) { if (isCalculateDiscountMethod(m)) { m.replace(""" { System.out.println("cd(" + $1 + ", " + $2 + ")"); $_ = $proceed($$); } """); } } }
  18. InstrumentationHandler.addTransformer(…) public static void addTransformer( Instrumentation inst, ClassFileTransformer trans, boolean

    canRetransform) { !// !!... inst.addTransformer( new ClassFileTransformer() { @Override public byte[] transform(!!...) { byte[] old = classfileBuffer.clone(); byte[] current = transformer.transform(!!...); addDiff(transformer, old, current); return current; } }, canRetransform ) } https://github.com/parttimenerd/meta-agent/blob/main/src/main/java/me/bechberger/meta/runtime/InstrumentationHandler.java
  19. InstrumentationHandler.addTransformer(…) public static void addTransformer( Instrumentation inst, ClassFileTransformer trans, boolean

    canRetransform) { !// !!... inst.addTransformer( new ClassFileTransformer() { @Override public byte[] transform(!!...) { byte[] old = classfileBuffer.clone(); byte[] current = transformer.transform(!!...); addDiff(transformer, old, current); return current; } }, canRetransform ) } https://github.com/parttimenerd/meta-agent/blob/main/src/main/java/me/bechberger/meta/runtime/InstrumentationHandler.java
  20. public class DiscountCalculator { public int calculateDiscount(int price, boolean isMember)

    { if (price > 100) { return isMember ? 20 : 10; } return isMember ? 5 : 0; } }
  21. Helps to understand your tools As JaCoCo works on byte

    code level also constructors and static initializers are counted as methods. Some of these methods may not have a direct correspondence in Java source code, like implicit and thus generated default constructors or initializers for constants. “ https://www.jacoco.org/jacoco/trunk/doc/counters.html
  22. Object.equals(...) The equals method implements an equivalence relation on non-null

    object references: • It is reflexive • It is symmetric • It is transitive • It is consistent https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#equals-java.lang.Object-
  23. Object.equals(...) The equals method implements an equivalence relation on non-null

    object references: • It is reflexive • It is symmetric • It is transitive • It is consistent https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#equals-java.lang.Object-
  24. Making a runtime library available to all instrumented classes can

    be a painful or impossible task in frameworks that use their own class loading mechanisms. […] As our minimum target is Java 1.5 JaCoCo decouples the instrumented classes and the coverage runtime through official JRE API types only. The instrumented classes communicate through the Object.equals(Object) method with the runtime. A instrumented class can retrieve its probe array instance with the following code. JaCoCo – Implementation Design https://www.jacoco.org/jacoco/trunk/doc/implementation.html “
  25. !!/** * Container for runtime execution and meta data. All

    access to the runtime data * is thread safe. !*/ public class RuntimeData { !/* !!... !*/ !!/** * In violation of the regular semantic of {@link Object#equals(Object)} * this implementation is used as the interface to the execution data store. * * @param args * the arguments as an {@link Object} array * @return has no meaning !*/ @Override public boolean equals(final Object args) { if (args instanceof Object[]) { getProbes((Object[]) args); } return super.equals(args); } !/* !!... !*/ https://github.com/jacoco/jacoco/blob/f0dcc6b0b78b04dd94617707551f6c2d18426d09/org.jacoco.core/src/org/jacoco/core/runtime/RuntimeData.java
  26. interface Iterable { !// !!... default void forEach(Consumer<? super T>

    action) { Objects.requireNonNull(action); for (T element : this) { action.accept(element); } } }
  27. interface Iterable { !// !!... default void forEach(Consumer<? super T>

    action) { MockMethodDispatcher mmd = MockMethodDispatcher.get("q9Lk1TRa", this); Callable handleForEach = mmd !!= null !&& mmd.isMocked(this) !&& !mmd.isOverridden(this, Iterable.class.getMethod("forEach", Consumer.class)) ? mmd.handle(this, Iterable.class.getMethod("forEach", Consumer.class), new Object[]{action}) : null; if (handleForEach !== null) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } if (handleForEach !!= null) { handleForEach.call(); } } }
  28. interface Iterable { !// !!... default void forEach(Consumer<? super T>

    action) { MockMethodDispatcher mmd = MockMethodDispatcher.get("q9Lk1TRa", this); Callable handleForEach = mmd !!= null !&& mmd.isMocked(this) !&& !mmd.isOverridden(this, Iterable.class.getMethod("forEach", Consumer.class)) ? mmd.handle(this, Iterable.class.getMethod("forEach", Consumer.class), new Object[]{action}) : null; if (handleForEach !== null) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } if (handleForEach !!= null) { handleForEach.call(); } } }
  29. interface Iterable { !// !!... default void forEach(Consumer<? super T>

    action) { MockMethodDispatcher mmd = MockMethodDispatcher.get("q9Lk1TRa", this); Callable handleForEach = mmd !!= null !&& mmd.isMocked(this) !&& !mmd.isOverridden(this, Iterable.class.getMethod("forEach", Consumer.class)) ? mmd.handle(this, Iterable.class.getMethod("forEach", Consumer.class), new Object[]{action}) : null; if (handleForEach !== null) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } if (handleForEach !!= null) { handleForEach.call(); } } }
  30. interface Iterable { !// !!... default void forEach(Consumer<? super T>

    action) { MockMethodDispatcher mmd = MockMethodDispatcher.get("q9Lk1TRa", this); Callable handleForEach = mmd !!= null !&& mmd.isMocked(this) !&& !mmd.isOverridden(this, Iterable.class.getMethod("forEach", Consumer.class)) ? mmd.handle(this, Iterable.class.getMethod("forEach", Consumer.class), new Object[]{action}) : null; if (handleForEach !== null) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } if (handleForEach !!= null) { handleForEach.call(); } } }
  31. interface Iterable { !// !!... default void forEach(Consumer<? super T>

    action) { MockMethodDispatcher mmd = MockMethodDispatcher.get("q9Lk1TRa", this); Callable handleForEach = mmd !!= null !&& mmd.isMocked(this) !&& !mmd.isOverridden(this, Iterable.class.getMethod("forEach", Consumer.class)) ? mmd.handle(this, Iterable.class.getMethod("forEach", Consumer.class), new Object[]{action}) : null; if (handleForEach !== null) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } if (handleForEach !!= null) { handleForEach.call(); } } }
  32. interface Iterable { !// !!... default void forEach(Consumer<? super T>

    action) { MockMethodDispatcher mmd = MockMethodDispatcher.get("q9Lk1TRa", this); Callable handleForEach = mmd !!= null !&& mmd.isMocked(this) !&& !mmd.isOverridden(this, Iterable.class.getMethod("forEach", Consumer.class)) ? mmd.handle(this, Iterable.class.getMethod("forEach", Consumer.class), new Object[]{action}) : null; if (handleForEach !== null) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } } if (handleForEach !!= null) { handleForEach.call(); } } }
  33. But where is the List implementation? @Mock List<String> mockedList; @Test

    public void testList() { mockedList.add("one"); Mockito.verify(mockedList).add("one"); }
  34. http://localhost:7071/all/decompile?pattern=org.mockito.codegen.List$MockitoMock$iBo7tFTz public class List$MockitoMock$iBo7tFTz implements List, MockAccess { private static

    final long serialVersionUID = 42L; private MockMethodInterceptor mockitoInterceptor; !// !!... !// $VF: synthetic field private static final Method cachedValue$46t0jdUa$sgg2351 = List.class.getMethod("add", Object.class); !// !!... @Override public boolean add(Object var1) { return (Boolean)DispatcherDefaultingToRealMethod .interceptAbstract(this, this.mockitoInterceptor, false, cachedValue$46t0jdUa$sgg2351, new Object[]{var1}); } public void setMockitoInterceptor(MockMethodInterceptor var1) { this.mockitoInterceptor = var1; } !// !!... }
  35. http://localhost:7071/all/decompile?pattern=org.mockito.codegen.List$MockitoMock$iBo7tFTz public class List$MockitoMock$iBo7tFTz implements List, MockAccess { private static

    final long serialVersionUID = 42L; private MockMethodInterceptor mockitoInterceptor; !// !!... !// $VF: synthetic field private static final Method cachedValue$46t0jdUa$sgg2351 = List.class.getMethod("add", Object.class); !// !!... @Override public boolean add(Object var1) { return (Boolean)DispatcherDefaultingToRealMethod .interceptAbstract(this, this.mockitoInterceptor, false, cachedValue$46t0jdUa$sgg2351, new Object[]{var1}); } public void setMockitoInterceptor(MockMethodInterceptor var1) { this.mockitoInterceptor = var1; } !// !!... }
  36. Implementation public interface Instrumentation { !!/** * Returns an array

    of all classes currently loaded !*/ @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); Based on: https://github.com/openjdk/jdk/blob/bc5cde1b198baf6e2e36d370b0aaa907c8f35777/src/java.instrument/share/classes/java/lang/instrument/Instrumentation.java#L74
  37. @AnnotationTest.Annotation(key = "key", value = "value") public class AnnotationTest {

    @Retention(RetentionPolicy.RUNTIME) @interface Annotation { String key(); String value(); } @Test public void testAnnotation() throws InterruptedException { Annotation annotation = AnnotationTest.class.getAnnotation(Annotation.class); System.out.println(annotation.key()); System.out.println(annotation.value()); Thread.sleep(1000000); } }
  38. @AnnotationTest.Annotation(key = "key", value = "value") public class AnnotationTest {

    @Retention(RetentionPolicy.RUNTIME) @interface Annotation { String key(); String value(); } @Test public void testAnnotation() throws InterruptedException { Annotation annotation = AnnotationTest.class.getAnnotation(Annotation.class); System.out.println(annotation.key()); System.out.println(annotation.value()); Thread.sleep(1000000); } }
  39. @AnnotationTest.Annotation(key = "key", value = "value") public class AnnotationTest {

    @Retention(RetentionPolicy.RUNTIME) @interface Annotation { String key(); String value(); } @Test public void testAnnotation() throws InterruptedException { Annotation annotation = AnnotationTest.class.getAnnotation(Annotation.class); System.out.println(annotation.key()); System.out.println(annotation.value()); Thread.sleep(1000000); } } Type of this object?
  40. public final class $Proxy7 extends Proxy implements Annotation { private

    static final Method hashCodeMethod; !// !!... public $Proxy7(InvocationHandler handler) { super(handler); } @Override public final int hashCode() { try { return (Integer) super.h.invoke(this, hashCodeMethod, null); } catch (RuntimeException | Error e) { throw e; } catch (Throwable t) { throw new UndeclaredThrowableException(t); } }
  41. Objects are created by the Annotation Parser public static Annotation

    annotationForMap(final Class<? extends Annotation> type, final Map<String, Object> memberValues) { return AccessController.doPrivileged(new PrivilegedAction<Annotation>() { public Annotation run() { return (Annotation) Proxy.newProxyInstance( type.getClassLoader(), new Class<?>[] { type }, new AnnotationInvocationHandler(type, memberValues)); !}}); } Source: https://github.com/openjdk/jdk/blob/bc5cde1b198baf6e2e36d370b0aaa907c8f35777/src/java.base/share/classes/sun/reflect/annotation/AnnotationParser.java
  42. Class Loader Hierarchies platform app bootstrap X Y class reference

    delegates to searches in runtime.jar https://mostlynerdless.de/blog/2023/06/02/class-loader-hierarchies/
  43. Implementation public interface Instrumentation { void appendToBootstrapClassLoaderSearch( JarFile jarfile); Based

    on: https://github.com/openjdk/jdk/blob/bc5cde1b198baf6e2e36d370b0aaa907c8f35777/src/java.instrument/share/classes/java/lang/instrument/Instrumentation.java#L74
  44. public class PreventingInstrumentationHandler implements InstrumentationCallback { @Override public CallbackAction onAddTransformer(

    ClassFileTransformer transformer) { System.err.println("Preventing " + transformer.getClass().getName()); return CallbackAction.IGNORE; } }
  45. public class PreventingInstrumentationHandler implements InstrumentationCallback { @Override public CallbackAction onAddTransformer(

    ClassFileTransformer transformer) { System.err.println("Preventing " + transformer.getClass().getName()); return CallbackAction.IGNORE; } }
  46. <plugin> <groupId>me.bechberger!</groupId> <artifactId>meta-agent-maven-plugin!</artifactId> <version>0.0.3!</version> <executions> <execution> <phase>validate!</phase> <goals> <goal>meta-agent!</goal> !</goals>

    !</execution> !</executions> <configuration> <callbackClasses> <callbackClass>PreventingInstrumentationHandler!</callbackClass> !</callbackClasses> <server>true!</server> !</configuration> !</plugin>