Java 的字节码编程
本文最后更新于9 天前,其中的信息可能已经过时,如有错误请发送邮件到2125094989@qq.com

Java 的字节码编程

围绕“如何以非侵入方式生成、改写和观测 Java 程序”的笔记。串一串 ASM、Javassist、Byte Buddy、JavaAgent 和 JVMTI。

1. 概览

Java 源码会编译为 .class 字节码,并由 JVM 执行。字节码编程就是在类被加载前或运行期读取、生成、改写这些字节码,从而实现动态代理、AOP、热更新、监控、链路追踪、异常诊断等能力。

典型的非侵入监控链路:

应用启动
  -> -javaagent 加载 Agent
  -> premain(Instrumentation) 注册 ClassFileTransformer
  -> 类加载时匹配目标类/方法
  -> ASM / Javassist / Byte Buddy 注入增强逻辑
  -> 方法执行时采集耗时、参数、返回值、异常、TraceId
  -> 日志 / MQ / 监控平台

2. 学习前的底座:class、描述符与执行区域

2.1 类文件里有什么

.class 文件除了字节码指令,还包含类名、父类、接口、字段、方法、常量池、注解等元数据。字节码框架本质上是在读写这份结构化数据。

2.2 两个必须理解的运行时区域

  • 局部变量表:保存 this、方法参数、局部变量;实例方法的槽位 0this
  • 操作数栈:指令将值压栈、弹栈并计算;例如 ILOAD -> ILOAD -> IADD -> IRETURN 完成两个 int 相加。

ASM 的难点就在于必须显式维护这两个区域;ClassWriter.COMPUTE_MAXS 可帮助计算最大栈深和局部变量数量,但不能替代对控制流和类型的理解。

2.3 常用类型与方法描述符

Java 类型描述符
boolean / byte / char / short / intZ / B / C / S / I
float / long / doubleF / J / D
voidV
ObjectLjava/lang/Object;
int[][I

方法描述符格式为:(参数描述符)返回值描述符

void main(String[] args)         -> ([Ljava/lang/String;)V
int sum(int a, int b)            -> (II)I
String query(String id, long ts) -> (Ljava/lang/String;J)Ljava/lang/String;

3. 三种字节码框架如何选

框架抽象层级优势代价 / 适用场景
ASM最底层,直接操作指令性能好、控制力强、能力完整学习曲线陡;适合框架、探针、复杂精细改写
Javassist偏源码式 API,也支持低级 Bytecode上手快;可用 insertBeforeinsertAfteraddCatch复杂场景仍需理解类结构;适合快速插桩、原型
Byte Buddy高级流式 DSL匹配、委托、Advice、注解绑定自然;与 Agent 结合顺滑需掌握匹配与委托模型;适合生产级 Agent、代理、监控

Byte Buddy / Javassist 搭出可运行的监控案例,再回到 ASM 学描述符、栈和指令

4. ASM:直接编写与改写字节码

4.1 生成类与方法

核心对象:

  • ClassWriter:生成 class 文件。
  • ClassReader:读取已有 class。
  • ClassVisitor / MethodVisitor:访问、改写类与方法。
  • AdviceAdapter:对“方法进入 / 退出”插桩的常用适配器。

生成 HelloWorld.main 的本质指令:

GETSTATIC java/lang/System.out
LDC "Hello World"
INVOKEVIRTUAL java/io/PrintStream.println
RETURN

生成 int sum(int a, int b) 的本质:从局部变量表加载两个 int,执行 IADD,再 IRETURN

4.2 方法耗时、参数与返回值插桩

最小监控模型:

// 目标代码被增强后的等价效果
long start = System.nanoTime();
try {
    Object result = originalMethod(...);
    Monitor.point(methodId, start, args, result);
    return result;
} catch (Throwable e) {
    Monitor.error(methodId, start, args, e);
    throw e;
}

关键点:

  • onMethodEnter 记录起始时间,并用 newLocal(Type.LONG_TYPE) 保存局部变量索引。
  • 采集参数时,基本类型必须先装箱为对象,再放进 Object[]longdouble 占两个局部变量槽位。
  • onMethodExit 中,返回值原本位于操作数栈,必须先保存到局部变量,调用监控方法后再恢复返回值。
  • 退出码不同,对应不同返回类型:IRETURNLRETURNFRETURNDRETURNARETURNRETURNATHROW 表示异常路径。
  • 方法的固定元数据(类、方法、描述符、参数类型、返回类型)应在类转换阶段生成唯一 methodId 并缓存;热路径只传 ID 与动态数据。

4.3 给原方法包上 Try/Catch

ASM 可通过 LabelvisitTryCatchBlockvisitFrameATHROW 构造异常处理块。正确的语义是:记录异常后继续抛出原异常,不能吞掉异常或改变调用方行为。

高频错误:

  • 只处理 ARETURN,漏掉基本类型、void 或异常分支。
  • 对构造器、<clinit>、桥接方法、合成方法无差别增强。
  • 插桩逻辑自身再次被增强,导致递归、死循环或 StackOverflowError
  • 引入新分支却未正确计算栈帧,触发 VerifyError

5. Javassist:更贴近 Java 代码的插桩

5.1 基本模型

  • ClassPoolCtClass 的对象池。
  • CtClassCtFieldCtMethodCtConstructor:类、字段、方法、构造器。
  • toClass() / toBytecode():加载或取得生成后的字节码。

可以用 makeClass + CtMethod#setBody 创建新类,也可以获取已有 CtMethod 后进行增强。

5.2 常用插桩 API

ctMethod.addLocalVariable("startNanos", CtClass.longType);
ctMethod.insertBefore("{ startNanos = System.nanoTime(); }");
ctMethod.insertAfter("{ Monitor.point(id, startNanos, $_); }", false);
ctMethod.addCatch("{ Monitor.error(id, $e); throw $e; }",
                  pool.get("java.lang.Exception"));

特殊变量:

  • $0this
  • $1$2 …:第 N 个参数。
  • $$:全部参数。
  • $_:返回值。
  • $e:捕获到的异常。

实践提醒:addCatchinsertBeforeinsertAfter 的调用顺序会影响最终包裹范围;对静态方法读取局部变量参数名时,不要误把 this 当作第一个参数。

5.3 运行期热替换

javassist.tools.HotSwapper 可通过 JDWP 触发重定义。启动时需开启调试端口,例如:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000

这类能力适合调试、受控热修复或模型包更新。生产使用必须明确 JVM 重定义限制、类加载器边界、回滚与权限控制;它不是常规业务发布的替代品。

5.4 注解与低级 Bytecode API

Javassist 既能读取类/方法注解,也可借由 AnnotationsAttributeAnnotation 和各种 MemberValue 创建运行时注解。若高级 API 不够用,可使用 MethodInfoCodeAttributeBytecode 直接写指令,如 addGetstaticaddInvokevirtualaddReturn

6. Byte Buddy:以匹配、委托和 Advice 建模

6.1 创建与改写类

Byte Buddy 的主链路通常为:

new ByteBuddy()
    .subclass(BizMethod.class)
    .method(ElementMatchers.named("queryUserInfo"))
    .intercept(MethodDelegation.to(Monitor.class))
    .make()
    .load(loader)
    .getLoaded();

常用能力:

  • subclass:创建子类;也可使用 redefine/rebase 思路改写已有类。
  • namedefineMethodwithParameter:定义类与方法。
  • ElementMatchers:按名称、类型、注解等匹配目标。
  • FixedValue:返回固定值。
  • MethodDelegation:将调用交给拦截器。
  • Advice:在方法进入/退出位置植入切面逻辑。

6.2 委托拦截器的关键注解

注解用途
@Origin获取原始 Method、构造器、类或方法描述
@AllArguments获取全部参数数组
@Argument(n)获取指定参数
@SuperCallCallable 调用原父类方法
@This / @Super获取当前对象 / 父类对象
@RuntimeType放宽返回值或参数的严格类型匹配

典型的耗时采集应把原调用放在 try 内、指标输出放在 finally 内,确保异常路径也能结束计时。若要记录异常,应保留并抛回原异常。

6.3 泛型、抽象方法与注解

Byte Buddy 可以构造参数化父类(如 Repository<String>),再通过 MethodDelegation 实现抽象方法,并以 annotateTypeannotateMethod 注入运行时注解。这适用于网关接口、RPC 服务描述、动态代理契约等场景。

7. JavaAgent:让增强发生在类加载时

7.1 启动入口与打包

JVM 以如下参数加载 Agent:

-javaagent:/path/to/agent.jar=agentArgs

Agent JAR 的 MANIFEST.MF 至少声明:

Premain-Class: com.example.agent.MyAgent
Can-Redefine-Classes: true

JVM 在应用 main 之前调用:

public static void premain(String agentArgs, Instrumentation inst) {
    inst.addTransformer(new MyTransformer());
}

Agent 依赖(ASM、Javassist、Byte Buddy 等)必须可靠地随 Agent 打包或隔离,避免与业务应用的同名依赖发生类加载冲突。

7.2 Transformer 的职责

ClassFileTransformer#transform 接收类加载器、内部类名与原始字节码,返回新字节码或 null。生产实现需先做严格过滤:

  • 只匹配业务包与明确的组件;排除 JDK、Agent 自身、字节码框架自身。
  • 排除 main、构造器、抽象/原生方法或按策略单独处理。
  • 转换异常必须隔离,发生故障时回退原字节码,不能阻塞应用启动。
  • 为转换命中、忽略、失败建立诊断日志与指标。

8. 从“方法监控”到“应用级可观测性”

8.1 方法级事件模型

每次方法调用可形成如下事件:

methodId, traceId, spanId, parentSpanId,
startTime, duration, parameterSummary,
returnSummary, throwable, thread, host

敏感数据(密码、令牌、身份证、完整请求体)必须脱敏/采样,避免监控系统成为数据泄露源;参数序列化还要限制长度与深度,防止性能和内存问题。

8.2 JVM 与 GC 指标

ManagementFactory 提供 MemoryMXBeanGarbageCollectorMXBean,可定期采集:

  • 堆与非堆的 init/max/used/committed、使用率。
  • 每个 GC 收集器的收集次数、总耗时、关联内存池。

原书以定时线程打印指标演示原理。工程中应使用指标系统(如 Micrometer/OpenTelemetry 生态)上报,并设置频率、标签基数和告警阈值,避免打印洪泛。

8.3 ThreadLocal 链路追踪

同一线程内可将 traceId 存入 ThreadLocal,以栈维护嵌套 span:

  1. 根调用生成 TraceId。
  2. 方法进入时 push span,继承父 TraceId。
  3. 方法退出时 pop span,计算耗时。
  4. 栈为空时 remove() 清理上下文。

重要边界:ThreadLocal 只解决同线程传播;线程池、异步任务、消息队列、HTTP/RPC 跨进程都需要显式传递和恢复上下文。在线程池中忘记清理还会造成上下文串链和内存泄漏。

8.4 插件化 Agent 架构

把监控能力拆成插件比把所有逻辑塞进 premain 更可维护:

Plugin
  ├─ name()                 // 例如 jvm、link、jdbc、http
  ├─ buildTypeMatcher()     // 命中哪些类
  ├─ buildMethodMatcher()   // 命中哪些方法
  └─ adviceClass()          // 进入/退出时执行什么

Agent 启动时遍历启用插件,将匹配器和 Advice 组合进 AgentBuilder。进一步可做成外部配置、灰度开关、按服务动态启停与采样策略。

9. JVMTI、JDWP、JDI:更低层的诊断能力

JPDA 体系由低到高分为:

编程语言作用
JVMTINative / C、C++获取和控制 JVM 状态,注册事件回调
JDWP协议层调试器与被调试 JVM 的通信格式
JDIJava面向调试工具的高层 API

JVMTI Agent 通过 Agent_OnLoad 获取 jvmtiEnv、声明能力、注册回调,并启用 JVMTI_EVENT_EXCEPTION,可在异常发生时定位类签名、方法名、签名、字节码位置与异常类型。

它适合性能分析、调试、线程/内存/异常诊断等 JVM 级能力,但开发和部署复杂度显著高于 JavaAgent。优先选择 Java Agent;确实需要 JVM native 事件时再引入 JVMTI,并做好平台兼容性与 native 崩溃风险控制。

10. 推荐的动手路线

  1. javap -c -v 观察一段普通 Java 代码的指令、描述符与局部变量表。
  2. 使用 ASM 生成 HelloWorld、两数相加,理解 LOAD / STORE / RETURN
  3. 用 ASM 为一个指定方法插入耗时与参数采集,并分别覆盖正常返回与异常返回。
  4. 用 Javassist 的 insertBeforeinsertAfteraddCatch 复刻同一功能。
  5. 用 Byte Buddy 的 MethodDelegation@Origin@AllArguments@SuperCall 实现监控。
  6. 将 Byte Buddy 拦截器接入 premain,只匹配一个测试包。
  7. 加入 TraceId、span 栈、JVM 指标、异常事件,并把事件异步输出。
  8. 最后再处理插件化、采样、脱敏、性能基准、兼容矩阵和故障回退。

11. 工程检查清单

  • 目标类/方法匹配范围是否足够窄,且排除了 Agent 自身?
  • 正常、void、基本类型、对象返回值与异常路径是否都保持原语义?
  • 是否避免了递归增强、重复转换、类加载器冲突和 VerifyError
  • 监控开销是否有采样、异步队列、批量上报与熔断保护?
  • 参数、返回值、异常栈是否脱敏、截断、限深?
  • ThreadLocal 是否在所有退出路径清理?异步传播是否有明确方案?
  • Agent 出错时是否回退原字节码且不影响业务可用性?
  • 是否覆盖目标 JDK、框架版本和启动方式的集成测试?

12. 总结

字节码编程是为了在不改业务代码的前提下获得可控的运行时能力。ASM 提供最强控制,Javassist 提供快速插桩,Byte Buddy 提供最适合构建现代 Agent 的抽象;JavaAgent 把这些能力接入类加载生命周期,ThreadLocal、插件化和指标体系则把单点增强推进为可观测性系统。真正的工程门槛不在“能增强”,而在不破坏语义、控制开销、保证隔离,并能安全演进

试试实现ThreadLocal链路追踪

Google开源的Dapper链路追踪组件,并在2010年发表了论文《Dapper, a Large-Scale 
Distributed Systems Tracing Infrastructure》,这篇文章是业内实现链路追踪的标杆和理论基
础,具有非常大的参考价值。目前,链路追踪组件有Google的Dapper,Twitter 的Zipkin,以及
阿里的Eagleeye (鹰眼)等,它们都是非常优秀的链路追踪开源组件。这里主要讲述如何在
Spring Cloud Sleuth中集成Zipkin。在Spring Cloud Sleuth中集成Zipkin非常的简单,只需要引
入相应的依赖和做相关的配置即可
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇