Document Version: 1.0 Last Updated: July 2026 Status: Implemented (v3.0.0+)
BTrace performs bytecode instrumentation through a small internal SPI, InstrumentationBackend, with two implementations:
| Backend | Source set | Availability | Class file versions |
|---|---|---|---|
AsmInstrumentationBackend |
src/main/java (Java 8) |
Always | ≤ 71 (up to Java 27) |
ClassFileApiBackend |
src/main/java24 (Java 24) |
Agent running on JDK 24+ | > 71 (Java 28+) |
All types live in the io.btrace.instr package of the btrace-agent module:
btrace-agent/src/main/java/io/btrace/instr/InstrumentationBackend.javabtrace-agent/src/main/java/io/btrace/instr/AsmInstrumentationBackend.javabtrace-agent/src/main/java/io/btrace/instr/BackendSelector.javabtrace-agent/src/main/java/io/btrace/instr/ClassMeta.javabtrace-agent/src/main/java24/io/btrace/instr/ClassFileApiBackend.java
BTrace's instrumentation pipeline is built on ASM. ASM can only parse class files up to a major version it explicitly knows about and throws when handed anything newer, so every new Java release used to need an ASM upgrade before BTrace could instrument it. The ASM backend's ceiling is the highest class-file version BTrace has validated its ASM pipeline against, class file major version 71 (Java 27); anything newer is routed to the ClassFile API backend, regardless of whether the bundled ASM release (9.11.1, settings.gradle) happens to parse it:
/** Highest class file major version handled by the ASM backend (see class javadoc). */
static final int MAX_ASM_MAJOR_VERSION = 71; // Java 27The constant is raised deliberately once the ASM path has been validated on a new class-file version, not automatically with each ASM dependency bump, so the ClassFile API backend covers the JDK versions ahead of the validated ASM ceiling (at the time of writing: JDK 28 early-access builds, class file major version 72).
Without an alternative backend, an application compiled for a Java release newer than the ASM ceiling (currently Java 28+, class file major version 72+) could not be instrumented at all. The JDK ClassFile API (java.lang.classfile.*, standardized in JDK 24) always understands the class file format of the JDK it ships with, so it provides a forward-compatible path for such classes.
interface InstrumentationBackend {
/** Returns {@code true} when this backend can process the given class file major version. */
boolean supports(int classFileMajorVersion);
/**
* Instruments {@code classfileBuffer} by applying all applicable probes.
*
* @param loader the classloader loading the target class (may be {@code null})
* @param classfileBuffer raw class file bytes
* @param probes all currently registered probes
* @return transformed class bytes if at least one probe matched, {@code null} otherwise
*/
byte[] instrument(ClassLoader loader, byte[] classfileBuffer, Collection<BTraceProbe> probes);
}The ClassMeta interface complements the SPI: it exposes the minimal class metadata (Java class name, internal name, runtime-visible annotation types, classloader) needed for probe-to-class matching in BTraceProbeSupport, decoupling the matching logic from ASM's ClassReader so alternative backends can perform matching without constructing an ASM object.
BackendSelector chooses a backend based on the class file major version. The selection logic, verbatim:
static InstrumentationBackend select(int classFileMajorVersion) {
if (!ASM.supports(classFileMajorVersion) && CLASSFILE_API != null) {
return CLASSFILE_API;
}
return ASM;
}In other words:
- Class file version ≤ 71 → ASM backend (the default, full-featured path).
- Class file version > 71 and the ClassFile API backend is available → ClassFile API backend.
- Class file version > 71 but the ClassFile API backend is unavailable (agent running on JDK < 24) → falls back to ASM, which will fail to parse the class; instrumentation of that class is effectively skipped.
The ClassFile API backend is loaded reflectively at class-initialization time so the main (Java 8-compiled) source set has no compile-time dependency on java.lang.classfile:
Class<?> cls =
Class.forName(
"io.btrace.instr.ClassFileApiBackend", true, BackendSelector.class.getClassLoader());
return (InstrumentationBackend) cls.getDeclaredConstructor().newInstance();On JDK < 24 the Class.forName fails (the compiled class targets class file version 68/Java 24 and references java.lang.classfile), the failure is logged at debug level, and the field stays null.
- The agent must run on JDK 24+ — the backend is compiled with
sourceCompatibility = 24/targetCompatibility = 24and usesjava.lang.classfile.*. - It is engaged only for class file major versions > 71 (
supports()returnsclassFileMajorVersion > AsmInstrumentationBackend.MAX_ASM_MAJOR_VERSION).
The backend parses the class with ClassFile.parse(), builds a ClassMeta from the class model (name, runtime-visible annotations, classloader), collects applicable handlers via BTraceProbe.getApplicableHandlers(meta), and injects probe calls as invokedynamic instructions bootstrapped by io.btrace.runtime.IndyDispatcher.bootstrap(...) — entry probes before the first real instruction, return probes before each ReturnInstruction.
Verified in btrace-agent/src/main/java24/io/btrace/instr/ClassFileApiBackend.java (class javadoc
and the handler dispatch in instrument(...)):
- All probe kinds are supported:
ENTRY,RETURN,CALL,LINE,FIELD_GET,FIELD_SET,ARRAY_GET,ARRAY_SET,CHECKCAST,INSTANCEOF,THROW,CATCH,ERROR,NEWARRAY,NEW,SYNC_ENTRY, andSYNC_EXIT. - Method matching supports exact names,
/regex/patterns, and type-constrained matching (a non-emptytypein@OnMethod). - Handler parameters:
@ProbeClassName,@ProbeMethodName,@Self, ordinary probed-method arguments (includingAnyType[]),@Return,@TargetInstance(caught/escaping throwable forCATCH/ERROR, lock object forSYNC_*), and@Durationwhere the kind defines it. @Durationis defined only forRETURN,ERROR, andCALLwithWhere.AFTER. The probe verifier (BTraceMethodNode.verifySpecialParameters) rejects it on any other kind, includingSYNC_ENTRY/SYNC_EXIT, before either backend sees the handler, so this is a probe-language rule and not a backend difference.- Classes the ClassFile API fails to parse are skipped (warning logged) rather than failing class loading.
The java24 source set is declared in btrace-agent/build.gradle and compiled with a JDK 24 toolchain. Its output is merged into the root of the regular agent jar:
jar {
into('') {
from sourceSets.java24.output
}
}Important consequences:
- The agent jar is NOT a Multi-Release JAR — there is no
META-INF/versions/24/entry and noMulti-Releasemanifest attribute. The compiled-at-24ClassFileApiBackend.classsits at the jar root next to the Java 8-compatible classes. - The class is never referenced directly from Java 8-compiled code; it is only ever loaded reflectively by
BackendSelector. On older JDKs the reflective load fails cleanly (UnsupportedClassVersionErrorcaught asThrowable), and BTrace continues with the ASM backend only.
- ExtensionInvokeDynamicBridge — invokedynamic-based extension linkage