本文项目地址:https://gitcode.com/one_bit/RuntimeTracker
字节码插桩
什么是字节码插桩?
在Java开发中,我们经常需要对程序进行监控、调试或功能增强,但又不希望修改原有的业务代码。这时候,字节码插桩技术就成为了一个可靠的解决方案。
字节码插桩(Bytecode Instrumentation)是一种在不修改源代码的情况下,通过在Java字节码中插入额外指令来实现程序功能增强的技术。简单来说,就是在程序的”中间层”——字节码层面进行”动手术”,为程序添加新的功能。
使用场景
- 你需要统计每个方法的执行时间,但不想在每个方法里手动添加计时代码
- 你想要监控数据库连接的使用情况,但不想修改所有的数据库访问代码
- 你需要在生产环境中动态开启某些调试功能,但不想重新发布应用
字节码编程插桩这种技术常与 Javaagent
技术结合用在系统的非入侵监控中,这样就可以替代在方法中进行硬编码操作。比如,你需要监控一个方法,包括;方法信息、执行耗时、出入参数、执行链路以及异常等。那么就非常适合使用这样的技术手段进行处理。
这些需求的共同点是:需要增强程序功能,但要保持代码的非侵入性。字节码插桩技术正是为了解决这类问题而生的。
综上,字节码插桩技术的核心价值就在于非侵入性、动态性、通用性、透明性。
执行流程
Java字节码
字节码是Java源代码编译后的中间表示形式,它是平台无关的,这也是Java”一次编写,到处运行”的基础。
Java程序的执行流程一般是:

字节码,也就是.class文件的结构一般是这样的:
ClassFile {
u4 magic; // 魔数 0xCAFEBABE
u2 minor_version; // 次版本号
u2 major_version; // 主版本号
u2 constant_pool_count; // 常量池计数
cp_info constant_pool[]; // 常量池
u2 access_flags; // 访问标志
u2 this_class; // 当前类
u2 super_class; // 父类
u2 interfaces_count; // 接口计数
u2 interfaces[]; // 接口表
u2 fields_count; // 字段计数
field_info fields[]; // 字段表
u2 methods_count; // 方法计数
method_info methods[]; // 方法表
u2 attributes_count; // 属性计数
attribute_info attributes[]; // 属性表
}
而对于方法,每个方法的字节码结构包括:
- 方法签名:方法名、参数类型、返回类型
- 访问标志:public、private、static等
- 字节码指令:具体的执行指令序列
- 局部变量表:存储方法参数和局部变量
- 操作数栈:用于计算的栈结构
常用的操作字节码的命令主要有:
aload_0
:加载局部变量表中索引为0的引用类型变量(通常是this)invokevirtual
:调用实例方法invokestatic
:调用静态方法return
:方法返回istore
:存储整数到局部变量表
可以使用 javap -c xxx.class 命令查看.java文件编译后的字节码指令,这对我们理解和调试JVM很有帮助。

所以,字节码插桩其实就是在原有的字节码指令序列中插入新的指令。
三种插桩时机
字节码插桩可以在程序生命周期的不同阶段进行,每种方式都有其特定的适用场景和实现方式。
编译时插桩
编译时插桩是在源代码编译成字节码的过程中或编译完成后,直接修改生成的.class文件。这种方式灵活性不够,比较少见。

比较适合于静态分析工具,如代码质量检查、安全扫描、性能优化,如编译时优化、代码生成、框架增强,如Lombok的注解处理
优势在于性能开销最小,插桩在编译时完成,并且可以进行复杂的静态分析,生成的字节码还可以进一步优化。
加载时插桩
加载时插桩是在类被ClassLoader加载到JVM之前,拦截并修改字节码的技术。这是最常用的插桩方式。
加载时插桩常与JavaAgent技术结合使用:

核心组件包括:
- Java Agent:程序启动时加载的代理程序
- ClassFileTransformer:字节码转换接口
- Instrumentation API:JVM提供的插桩API
工作流程为:
- JVM启动时加载Java Agent
- JVM调用Agent的premain()方法
- Agent注册ClassFileTransformer到Instrumentation
- 当ClassLoader加载类时,JVM调用transform方法
- 在transform方法中修改字节码
- JVM加载修改后的字节码
虽然每个类只在加载时会被修改字节码,但在实际使用的时候有的类会被卸载,重新加载时又一次调用了transformer方法,性能开销中等吧。
好处在于它能够实现完全的定制化,你可以在在transform方法中任意拦截你想增强的类或方法,并且去修改字节码,实现想要的功能,而且对原服务代码完全透明;因为是在加载时动态侵入的,所以又很方便为我们进行动态调试。
公司内部的大型项目都是基于如sf、DAG框架,接口、抽象类层出不穷,各种代理类、反射,如果直接看源码,头都看大了,真正想要理解项目,不如自己部署到预生产环境,写个请求自检一下,然后看看请求在服务内部到底是怎么执行的,我想这种方式应该是理解代码逻辑最快速的方式之一了。
运行时插桩
运行时插桩是在程序运行过程中,动态修改已经加载到JVM中的类。

这种方式灵活性很高,开销较大,实现的复杂度会比较高。
适用场景:
- 热部署:不重启应用更新代码
- 动态配置:运行时开启/关闭某些功能
- 故障排查:临时添加调试代码
- A/B测试:动态切换不同的实现
常用框架
在字节码编程方面有三个比较常见的框架;ASM
、byte-buddy
、Javassist
ASM是一个底层的字节码操作框架,直接操作字节码指令。开销最小,但是最复杂,非常非常复杂(头大),最一开始我是选用ASM来实现的,处理报错比编码时间还长,个人觉得一般场景可能也用不到,除非是对性能要求极高或者是框架组件等的开发。
Javassist相较于ASM就很友好了,提供了更高级的API,可以使用Java语法来操作字节码,几乎就是拼装Java代码,学习成本低(基本不需要了解字节码),可以快速验证功能,性能方面够用。
ByteBuddy是一个现代化的字节码操作库,提供了流畅的API设计,如果侵入SpringBoot应用,可能得检查下版本之间的依赖冲突。
三种框架的风格对比:
// Javassist风格
method.insertBefore("long startTime = System.currentTimeMillis();");
method.insertAfter("System.out.println(\"执行时间: \" + (System.currentTimeMillis() - startTime));");
// ASM风格
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LSTORE, 1);
// ByteBuddy风格
new ByteBuddy()
.subclass(Object.class)
.method(named("toString"))
.intercept(MethodDelegation.to(MyInterceptor.class))
.make()
.load(getClass().getClassLoader())
.getLoaded();
目前主流框架或组件都是基于这三种方式搭建的:
- Spring AOP:使用ASM(通过CGLIB)
- Hibernate:使用Javassist
- Mockito:使用ByteBuddy
- SkyWalking:使用ByteBuddy
- Arthas:使用ASM
实践案例
本节基于JavaAgent + Javassist 实现对SpringBoot应用的监控
这里准备了一个简单的SpringBoot应用


controller中定义了一个buyFood接口,会调用两个service中的方法,以及utils中的一个静态方法。
接下来,实现一个Java Agent,添加javassist依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
创建Agent类,实现premain方法:(这里类名不重要,主要实现premain)
@Slf4j
public class Agent{
public static void premain(String args, Instrumentation inst) {
// 由JVM调用
log.info("====Java Agent start==== \nargs: {}", Arrays.toString(args));
inst.addTransformer(new MethodTransformer());
}
}
然后创建MethodTransformer类,实现ClassFileTransformer接口,实现接口中的transformer方法
@Slf4j
public class MethodTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
// 代理类或者编译器生成的类我们可以跳过
if (shouldSkipClass(className)) {
return null;
}
log.info("[INFO] Processing class: {}", className);
try {
// 创建独立的 ClassPool,避免缓存问题
ClassPool pool = new ClassPool(true);
// 添加应用的类路径,确保能找到所有依赖类
if (loader != null) {
pool.appendClassPath(new javassist.LoaderClassPath(loader));
}
// 添加系统类路径
pool.appendSystemPath();
// 将斜杠格式转换为点格式
String dotClassName = className.replace('/', '.');
// 直接从字节数组创建 CtClass
CtClass ctClass = pool.makeClass(new ByteArrayInputStream(classfileBuffer));
// 验证类名是否匹配
if (!ctClass.getName().equals(dotClassName)) {
log.warn("[WARN] Class name mismatch: expected {}, got {}", dotClassName, ctClass.getName());
ctClass.detach();
return null;
}
// 跳过接口、注解、枚举和抽象类
if (ctClass.isInterface() || ctClass.isAnnotation() || ctClass.isEnum()) {
log.info("[INFO] Skipping interface/annotation/enum: {}", className);
ctClass.detach();
return null;
}
boolean modified = false;
CtMethod[] methods = ctClass.getDeclaredMethods();
log.info("[INFO] Found {} methods in class: {}", methods.length, className);
for (CtMethod method : methods) {
// 跳过特殊方法
if (shouldSkipMethod(method)) {
log.info("[INFO] Skipping method: {} in class: {}", method.getName(), className);
continue;
}
try {
enhanceMethod(method);
modified = true;
log.info("[INFO] Enhanced method: {} in class: {}", method.getName(), className);
} catch (Exception e) {
log.warn("[WARN] Failed to enhance method: {} in class: {}, error: {}",
method.getName(), className, e.getMessage());
}
}
if (modified) {
byte[] bytecode = ctClass.toBytecode();
ctClass.detach();
return bytecode;
} else {
log.info("[INFO] No methods enhanced in class: {} (found {} methods)", className, methods.length);
ctClass.detach();
}
} catch (Exception e) {
log.error("[ERROR] Failed to transform class: {}, error: {}: {}",
className, e.getClass().getName(), e.getMessage(), e);
}
return null;
}
}
考虑到性能问题,这里建议跳过一些用不上的方法和类
/**
* 检查是否为代理类或编译器生成的类
*/
private boolean isProxyOrGeneratedClass(String className) {
return className.contains("$EnhancerBySpringCGLIB$") ||
className.contains("$FastClassBySpringCGLIB$") ||
className.contains("$Proxy") ||
className.contains("$Lambda$") ||
className.contains("$");
}
/**
* 跳过大部分系统类和框架类
*/
private boolean shouldSkipClass(String className) {
// 跳过JDK核心类
if (className.startsWith("java/") ||
className.startsWith("javax/") ||
className.startsWith("sun/") ||
className.startsWith("com/sun/") ||
className.startsWith("jdk/")) {
return true;
}
// 跳过常见框架类(但允许业务代码中的框架类)
if (className.startsWith("org/springframework/") ||
className.startsWith("org/apache/") ||
className.startsWith("org/slf4j/") ||
className.startsWith("ch/qos/logback/") ||
className.startsWith("com/fasterxml/jackson/") ||
className.startsWith("org/hibernate/") ||
className.startsWith("org/mybatis/") ||
className.startsWith("com/mysql/") ||
className.startsWith("org/eclipse/") ||
className.startsWith("org/junit/") ||
className.startsWith("org/mockito/")) {
return true;
}
// 跳过Agent自身的类
if (className.startsWith("com/agent/")) {
return true;
}
// 跳过类加载器相关类
if (className.startsWith("org/springframework/boot/loader/")) {
return true;
}
// 处理其他类
return false;
}
/**
* 跳过方法
*/
private boolean shouldSkipMethod(CtMethod method) {
try {
int modifiers = method.getModifiers();
String methodName = method.getName();
// 跳过抽象方法、native方法、构造方法、静态初始化方法
if (Modifier.isAbstract(modifiers) ||
Modifier.isNative(modifiers) ||
methodName.equals("<init>") ||
methodName.equals("<clinit>")) {
return true;
}
// 检查合成方法标志位 (ACC_SYNTHETIC = 0x1000)
if ((modifiers & 0x1000) != 0) {
return true;
}
// 检查桥接方法标志位 (ACC_BRIDGE = 0x0040)
if ((modifiers & 0x0040) != 0) {
return true;
}
// 跳过 Lambda 表达式和编译器生成的方法
if (methodName.startsWith("lambda$") || methodName.startsWith("access$")) {
return true;
}
return false;
} catch (Exception e) {
return true;
}
}
重点是需要实现这里的enhanceMethod类,即定制化我们的增强方法
方法很简单,你只需要实现另外一个功能类,并将逻辑插到方法的字节码前后即可。
我想要捕获方法的调用链路关系,那么我在每个方法之前添加捕获类名、入参、方法名等信息,这些逻辑写在了MethodTracer类中,分别使用logMethodEntry、logMethodExit、logMethodException来记录,与ASM不同的是,这里的增强使用的就是Java代码,不需要去了解字节码;使用insertBefore、insertAfter来插入代码,看起来是不是和AOP很像。
/**
* 增强方法
*/
private void enhanceMethod(CtMethod method) throws CannotCompileException {
try {
// 获取方法信息
String className = method.getDeclaringClass().getName();
String methodName = method.getName();
log.info("[INFO] Enhancing method: {}.{}", className, methodName);
// 获取方法起始行号
int startLine = method.getMethodInfo().getLineNumber(0);
if (startLine <= 0) {
startLine = method.getMethodInfo().getLineNumber(1);
}
// 获取方法参数类型信息
String paramTypesCode = buildParamTypesCode(method);
// 方法入口代码 - 使用带参数类型信息的重载方法
String beforeCode = "{" +
"Object[] params = ($args != null) ? $args : new Object[0];" +
"Class[] paramTypes = " + paramTypesCode + ";" +
"com.codezj.MethodTracer.logMethodEntry(\"" + className + "\", \"" + methodName + "\", params, paramTypes, " + startLine + ");" +
"}";
// 方法正常退出代码
String afterCode = "{" +
"com.codezj.MethodTracer.logMethodExit(\"" + className + "\", \"" + methodName + "\", ($w) $_);" +
"}";
// 方法异常退出代码
String catchCode = "{" +
"com.codezj.MethodTracer.logMethodException(\"" + className + "\", \"" + methodName + "\", $e);" +
"throw $e;" +
"}";
// 注入代码
method.insertBefore(beforeCode);
method.insertAfter(afterCode);
// 添加异常处理,捕获所有异常
method.addCatch(catchCode, method.getDeclaringClass().getClassPool().get("java.lang.Throwable"));
log.info("[INFO] Successfully enhanced method: {}.{} (line: {}) with complete lifecycle",
className, methodName, startLine);
} catch (CannotCompileException e) {
log.error("[ERROR] Cannot compile enhanced code for method: {}, error: {}", method.getName(), e.getMessage(), e);
throw e;
} catch (Exception e) {
log.error("[ERROR] Unexpected error enhancing method: {}, error: {}", method.getName(), e.getMessage(), e);
throw new CannotCompileException(e);
}
}
MethodTracer类中我们实现对链路信息的记录,这里主要实现在方法进入时的加强,记录方法名、入参、入参类型等,同样方法退出时,可以记录出参、出参类型等等,方法的异常同样可以记录下来。
首先,我们创建MethodCall
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MethodCall {
/**
* 类名
*/
private String className;
/**
* 方法名
*/
private String methodName;
/**
* 参数列表
*/
private String params;
/**
* 参数类型列表
*/
private String paramTypes;
/**
* 调用者行号
*/
private int lineNumber;
/**
* 调用时间戳
*/
private long timestamp;
}
实现方法进入时的增强:
/**
* 方法入口时调用
*
* @param className 类名
* @param methodName 方法名
* @param params 入参
* @param paramTypes 入参类型
* @param lineNumber 行号
*/
public static void logMethodEntry(String className, String methodName, Object[] params, Class<?>[] paramTypes, int lineNumber) {
// 获取当前线程的调用栈
Deque<MethodCall> stack = callStack.get();
// 栈深度
if (stack.size() >= MAX_STACK_DEPTH) {
logger.warn("[WARN] Call stack depth exceeded limit, clearing stack to prevent memory leak");
stack.clear();
}
// 调用者
MethodCall methodCaller = stack.isEmpty() ? null : stack.peek();
// 被调用者
MethodCall methodCallee = MethodCall.builder()
.className(className)
.methodName(methodName)
.params(formatParam(params))
.paramTypes(formatParamTypesFromClasses(paramTypes))
.lineNumber(realCallLineNumber)
.timestamp(System.currentTimeMillis())
.build();
// 输出调用关系链
// 格式化打印看看
StringBuilder sb = new StringBuilder();
if (Objects.isNull(methodCaller)) {
// caller不存在,认为是入口
sb.append("[ENTRY] ");
} else {
sb.append("[CALL] ");
}
sb.append(JsonUtil.toJson(methodCaller)).append(" -> ").append(JsonUtil.toJson(methodCallee));
logger.info(sb.toString());
// 将当前方法信息推入调用栈
stack.push(methodCallee);
// 增加调用计数,定期清理
Integer count = callCount.get();
callCount.set(count + 1);
if (count > 0 && count % 1000 == 0) {
cleanupIfNeeded();
}
}
这里使用ThreadLocal保存每个线程的调用栈,这样能隔离开每条流量的调用链路
private static final ThreadLocal<Deque<MethodCall>> callStack = new ThreadLocal<Deque<MethodCall>>() {
@Override
protected Deque<MethodCall> initialValue() {
return new ArrayDeque<>();
}
};
至此,我们完整实现了一个Java Agent。打包成Jar包,打包时指定premain方法所在的类
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>com.codezj.Agent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
然后使用 如下命令启动服务:
java -javaagent:{$your_agent_jar_path.jar} -jar {$your_application_jar.jar}
等待服务正常启动,java agent也就无侵入的植入到代码中了,对服务完全透明。
为了简化输出,这里我们选择跳过“get”和“set”开头的方法,还有框架类

访问接口,查看输出

从图中看出,请求从入口处访问了buyFood方法,接下来,buyFood依此访问weather、buyFood、mood、price方法,并将方法的入参、入参类型、行号和时间戳打印出来。
这个地方还有一个处理数组的问题,修改下上面的demo,添加一个入参为数组的方法:

再次请求下

这里看到数组类型打印出来是这样的:[Ljava.lang.String,这表示该参数是一个字符串数组(String[]
)。
这种表示法是Java反射API中使用的类型表示方式:
[
符号表示这是一个数组,[ -> []一维数组 , [[ -> [[]]二维数组L
表示后面跟着的是一个引用类型(非原始类型)java.lang.String
是完整的类名(包名+类名);
表示类名的结束
按照这种规则,可以单独处理数组类型的入参
/**
* 转换数组类型名称
* [Ljava.lang.String; -> java.lang.String[]
* [I -> int[]
* [[Ljava.lang.String; -> java.lang.String[][]
*/
private static String convertArrayTypeName(String arrayTypeName) {
// 计算数组维度
int dimensions = 0;
int index = 0;
while (index < arrayTypeName.length() && arrayTypeName.charAt(index) == '[') {
dimensions++;
index++;
}
// 构建数组后缀
StringBuilder suffix = new StringBuilder();
for (int i = 0; i < dimensions; i++) {
suffix.append("[]");
}
// 获取元素类型
if (index >= arrayTypeName.length()) {
return "Unknown" + suffix.toString();
}
char typeChar = arrayTypeName.charAt(index);
String elementType;
switch (typeChar) {
case 'Z':
elementType = "boolean";
break;
case 'B':
elementType = "byte";
break;
case 'C':
elementType = "char";
break;
case 'D':
elementType = "double";
break;
case 'F':
elementType = "float";
break;
case 'I':
elementType = "int";
break;
case 'J':
elementType = "long";
break;
case 'S':
elementType = "short";
break;
case 'L':
// 对象类型,格式为 Ljava.lang.String;
int semicolonIndex = arrayTypeName.indexOf(';', index);
if (semicolonIndex > index) {
elementType = arrayTypeName.substring(index + 1, semicolonIndex);
} else {
elementType = "Unknown";
}
break;
default:
elementType = "Unknown";
break;
}
return elementType + suffix.toString();
}
重新请求一下,这种写法比较清晰一些

至此,我们使用Java Agent + Javassist实现了服务监控。
最佳实践
缓存字节码操作结果
为了避免重复的字节码分析,推荐字节码转换结果缓存下来
private final Map<String, Boolean> enhancementCache = new ConcurrentHashMap<>();
静态变量问题
建议使用ThreadLocal传参
// 避免在插桩代码中使用静态变量
// 因为不同类加载器可能导致多个实例
public class InstrumentationHelper {
// 使用ThreadLocal或者传参方式
private static final ThreadLocal<Context> context = new ThreadLocal<>();
}
避免内存泄漏
及时清理ThreadLocal以及缓存
// 及时清理不再需要的引用
public class MyTransformer implements ClassFileTransformer {
private final WeakHashMap<ClassLoader, Set<String>> processedClasses = new WeakHashMap<>();
public void cleanup() {
processedClasses.clear();
}
}
可能的发展趋势
- 云原生时代的挑战
随着容器化和微服务架构的普及,字节码插桩技术面临新的挑战:启动时间敏感、资源限制、动态扩缩容 - 新技术的影响
GraalVM Native Image:对传统字节码插桩提出挑战
Project Loom:虚拟线程对插桩工具的影响
Project Valhalla:值类型对字节码操作的影响 - 工具演进方向
更智能的插桩:基于AI的自动插桩决策
更好的性能:零开销或近零开销的插桩技术
更强的兼容性:跨平台、跨版本的统一插桩方案
总结
字节码插桩技术作为Java生态系统中的重要技术,为我们提供了强大的程序增强能力。随着Java平台的不断发展和新技术的涌现,这项技术也在持续演进。
掌握字节码插桩技术不仅能够帮助我们更好地理解Java程序的运行机制,还能为我们提供解决复杂问题的新思路。无论是开发监控工具、实现AOP功能,还是进行性能优化,字节码插桩都是一个值得深入学习和应用的技术。