Using Java Agent and Instrumentation for Non‑Intrusive Method Timing and Tracing
This article explains how to replace invasive manual timing code with a lightweight Java Agent that leverages the java.lang.instrument API, ASM bytecode manipulation, and the Attach API to measure method execution time, perform dynamic class redefinition, and integrate with tools like Arthas for runtime tracing.
In many Java projects developers insert explicit timing code (e.g., @Override public void method(Req req) { StopWatch stopWatch = new StopWatch(); stopWatch.start("method‑耗时统计"); method(); stopWatch.stop(); log.info("查询耗时分布:{}", stopWatch.prettyPrint()); } ) which is intrusive and hard to maintain. The article proposes using a Java Agent to achieve the same goal without modifying source code.
The JDK introduced the java.lang.instrument package in version 1.5, providing an Instrumentation interface with methods such as addTransformer , removeTransformer , retransformClasses , and getAllLoadedClasses . A ClassFileTransformer can intercept class loading and rewrite bytecode.
Instrumentation can be used in two ways: (1) attaching an agent JAR at JVM startup via the premain method, and (2) loading an agent into a running JVM using the Attach API via the agentmain method.
Premain demo – a minimal agent that measures method execution time:
public class MyClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class
classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if ("com/example/aop/agent/MyTest".equals(className)) {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new TimeStatisticsVisitor(Opcodes.ASM7, cw);
cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
return cw.toByteArray();
}
return classfileBuffer;
}
}
public class TimeStatisticsVisitor extends ClassVisitor {
public TimeStatisticsVisitor(int api, ClassVisitor classVisitor) { super(api, classVisitor); }
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
if (name.equals("
")) return mv;
return new TimeStatisticsAdapter(api, mv, access, name, descriptor);
}
}
public class TimeStatisticsAdapter extends AdviceAdapter {
@Override
protected void onMethodEnter() { super.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/aop/agent/TimeStatistics", "start", "()V", false); }
@Override
protected void onMethodExit(int opcode) { super.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/aop/agent/TimeStatistics", "end", "()V", false); }
}
public class TimeStatistics {
public static ThreadLocal
t = new ThreadLocal<>();
public static void start() { t.set(System.currentTimeMillis()); }
public static void end() { long time = System.currentTimeMillis() - t.get(); System.out.println(Thread.currentThread().getStackTrace()[2] + " spend: " + time); }
}
public class AgentMain {
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("premain方法");
instrumentation.addTransformer(new MyClassFileTransformer(), true);
}
}The Maven assembly plugin is used to build a "jar‑with‑dependencies" and the manifest entries Premain-Class and Agent-Class point to AgentMain . The agent is launched with:
java -javaagent:/path/to/aop-0.0.1‑SNAPSHOT‑jar‑with‑dependencies.jar com.example.aop.agent.MyTestRunning the test prints the measured execution time.
Attach API demo – dynamically load an agent after the JVM has started:
public class PrintNumTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class
classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if ("com/example/aop/agent/PrintNumTest".equals(className)) {
ClassReader cr = new ClassReader(classfileBuffer);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ClassVisitor cv = new TransformPrintNumVisitor(Opcodes.ASM7, cw);
cr.accept(cv, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
return cw.toByteArray();
}
return classfileBuffer;
}
}
public class PrintNumAgent {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
System.out.println("agentmain");
inst.addTransformer(new PrintNumTransformer(), true);
for (Class c : inst.getAllLoadedClasses()) {
if (c.getSimpleName().equals("PrintNumTest")) {
System.out.println("Reloading: " + c.getName());
inst.retransformClasses(c);
break;
}
}
}
}
public class MyAttachMain {
public static void main(String[] args) throws Exception {
VirtualMachine vm = VirtualMachine.attach(args[0]);
try { vm.loadAgent("/path/to/aop-0.0.1‑SNAPSHOT‑jar‑with‑dependencies.jar"); }
finally { vm.detach(); }
}
}The target program prints 100 repeatedly; after attaching the agent the output changes to 50 , demonstrating runtime bytecode replacement.
Arthas integration – the article shows how to debug the same demo with Alibaba’s Arthas tool, enabling remote JDWP debugging, setting breakpoints in com.taobao.arthus.agent334.AgentBootstrap#main , and using the trace command to monitor method execution.
Bytekit example – a higher‑level API built on ASM simplifies bytecode enhancement. A sample interceptor uses annotations @AtEnter , @AtExit , and @AtExceptionExit to log method arguments and return values:
public class SampleInterceptor {
@AtEnter(inline = false, suppress = RuntimeException.class, suppressHandler = PrintExceptionSuppressHandler.class)
public static void atEnter(@Binding.This Object object, @Binding.Class Object clazz,
@Binding.Args Object[] args, @Binding.MethodName String methodName,
@Binding.MethodDesc String methodDesc) {
System.out.println("atEnter, args[0]: " + args[0]);
}
@AtExit(inline = true)
public static void atExit(@Binding.Return Object returnObject) {
System.out.println("atExit, returnObject: " + returnObject);
}
@AtExceptionExit(inline = true, onException = RuntimeException.class)
public static void atExceptionExit(@Binding.Throwable RuntimeException ex,
@Binding.Field(name = "exceptionCount") int exceptionCount) {
System.out.println("atExceptionExit, ex: " + ex.getMessage() + ", field exceptionCount: " + exceptionCount);
}
}The article then walks through how Arthas implements the trace command. The core logic resides in EnhancerCommand.enhance , which registers a transformer, parses interceptor classes (e.g., SpyInterceptor1 , SpyTraceInterceptor* ), and modifies matching methods by inserting calls to SpyAPI.atEnter , atExit , and listener registration. Key snippets include the transform method that builds a ClassNode , filters methods, applies InterceptorProcessor objects, and finally returns the enhanced byte array.
public byte[] transform(final ClassLoader inClassLoader, String className, Class
classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// load SpyAPI, filter classes, build ClassNode, remove JSR, parse interceptors, etc.
for (MethodNode methodNode : matchedMethods) {
if (AsmUtils.containsMethodInsnNode(methodNode, Type.getInternalName(SpyAPI.class), "atBeforeInvoke")) {
// register listener for existing instrumentation
} else {
MethodProcessor mp = new MethodProcessor(classNode, methodNode, groupLocationFilter);
for (InterceptorProcessor interceptor : interceptorProcessors) {
try {
List
locations = interceptor.process(mp);
for (Location loc : locations) {
if (loc instanceof MethodInsnNodeWare) {
MethodInsnNode node = ((MethodInsnNodeWare) loc).methodInsnNode();
AdviceListenerManager.registerTraceAdviceListener(inClassLoader, className,
node.owner, node.name, node.desc, listener);
}
}
} catch (Throwable e) { logger.error(...); }
}
}
AdviceListenerManager.registerAdviceListener(inClassLoader, className, methodNode.name, methodNode.desc, listener);
}
// dump class, update affect, return enhanced bytes
return AsmUtils.toBytes(classNode, inClassLoader, classReader);
}Finally, the article lists reference links for deeper reading.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.