ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Byte Buddy实战:动态方法定义与拦截优先级全解析

Byte Buddy实战:动态方法定义与拦截优先级全解析 做Java Agent相关的项目快三年真正让我觉得离“字节码自由”最近的时刻是去年一次旧系统监控改造目标类没有接口、不能改源码还要求在不重启应用的情况下把“耗时统计、缓存降级、动态新增上报方法”三件事一起办了。JDK Proxy拦不住普通类CGLIB的MethodInterceptor虽然能拦但多个拦截逻辑谁先谁后全得靠回调链自己维护绕来绕去很容易把人绕晕。翻了一圈工具真正把动态方法定义、方法拦截以及多层拦截的优先级控制都做得比较舒服的还是Byte Buddy。这篇文章把我常用的那套实战路线重新捋一遍核心就三个词动态方法的定义、方法拦截、优先级博弈。无论你是第一次接触Byte Buddy还是已经在写Agent插件但被委托方法选择、Advice嵌套顺序折磨过这篇应该都能提供一些能直接抄走的方案和排查思路。1. 动态方法需求从哪来为什么我在监控改造中选了Byte Buddy1.1 传统方案解决不了的问题先还原一下当时的场景。线上有一个OrderService只有一个无接口的具体类queryOrder方法里面是数据库查询。需求有三个在不改源码的前提下给这个类动态增加一个getMetrics方法返回近期的调用统计。拦截queryOrder记录每次调用的耗时和参数。后续叠加一个缓存降级策略命中缓存时直接跳过原方法。我一开始的候选方案是JDK Proxy、CGLIB、ASM、Byte Buddy这四个。JDK Proxy第一个被排除因为OrderService没有接口它根本代理不了。CGLIB能通过生成子类实现拦截但它的MethodInterceptor只有一个invoke入口后面接一个增强逻辑就要在手写的回调链里插入一层随着需求叠加会越来越难维护。ASM是万能选项直接改写字节码可问题是学习成本太高一个visitMethodInsn就要盯半天方法描述符纯手写实现“给类新增一个方法”这种需求没有几百行下不来。Byte Buddy给我的感觉更像“带抽象层的字节码手术工具”。它底层最终生成的还是ASM字节码但上层封装成了声明式API——defineMethod定义方法、intercept定义方法体、MethodDelegation.to把逻辑委托给普通Java类。我不用关心visitXxx调用顺序只用关心“我想让这个类变成什么样”。这一点在快速迭代的监控系统里非常关键。1.2 Byte Buddy的能力边界与适用场景为了说清楚它的边界我列了一张对比表方便你按场景选型需求JDK ProxyCGLIBASMByte Buddy代理接口支持不支持需手写支持代理普通类不支持支持需手写支持运行时动态新增方法不支持需扩展繁琐支持修改已有方法体不支持需扩展繁琐支持拦截原方法的Super调用不支持部分支持手动通过SuperCall多拦截逻辑优先级控制不支持回调链自理手动BindingPriority Advice嵌套从我实际使用的经验看Byte Buddy最舒服的场景有两类。第一类是Mock和测试替身比如Mockito内部就是基于Byte Buddy生成的动态子类。第二类是Java Agent和APM方向类加载前的埋点、方法出入参采集、甚至直接把某个方法替换成降级逻辑它都提供了比较顺手的API。我的监控改造属于后者所以最终选型就是Byte Buddy。选型定下来后接下来最核心的问题就是动态方法到底怎么定义拦截又是怎么做的这两件事是后面优先级博弈的基础。2. 动态方法的定义从defineMethod到MethodDelegation2.1 最小可运行示例凭空生成一个带方法的新类先上一个最小的可运行代码目标是在运行时生成一个com.example.CustomGreeter类它继承Object但凭空多了一个greet(String)方法import net.bytebuddy.ByteBuddy; import net.bytebuddy.ClassFileVersion; import net.bytebuddy.description.modifier.Visibility; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.MethodDelegation; import net.bytebuddy.implementation.bind.annotation.Argument; import java.lang.reflect.Method; public class DynamicDefineDemo { public static void main(String[] args) throws Exception { Class? type new ByteBuddy(ClassFileVersion.JAVA_V8) .subclass(Object.class) .name(com.example.CustomGreeter) .defineMethod(greet, String.class, Visibility.PUBLIC) .withParameter(String.class, name) .intercept(MethodDelegation.to(GreetDelegate.class)) .make() .load(DynamicDefineDemo.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); Object obj type.getDeclaredConstructor().newInstance(); Method greet type.getMethod(greet, String.class); Object result greet.invoke(obj, Byte Buddy); System.out.println(result); } public static class GreetDelegate { public static String greet(Argument(0) String name) { return Hello, name !; } } }这段代码的关键点有三个。第一defineMethod(greet, String.class, Visibility.PUBLIC)是在声明方法头第二withParameter(String.class, name)是声明参数第三intercept(...)是在定义方法体。有意思的是委托方法的名字不需要和动态方法一致——MethodDelegation的匹配只看签名和注解不看方法名。所以我上面的GreetDelegate.greet碰巧同名纯粹是为了好读。执行后输出的是Hello, Byte Buddy!一个全新建类、新建方法、新方法体的过程就完整走通了。2.2 参数绑定方式与委托方法签名设计MethodDelegation之所以好用是因为它允许把拦截逻辑放到一个普通的Java类里而参数的传递通过注解来控制。我常用的参数注解有以下几种注解用途示例Argument(n)精确绑定目标方法的第n个参数Argument(0) String orderIdAllArguments把目标方法全部参数打包成数组AllArguments Object[] argsThis绑定当前目标方法的所属实例This OrderService instanceOrigin绑定当前被调用的反射Method对象Origin Method methodSuperCall绑定一个Callable执行原方法体SuperCall CallableObject zuperDefaultCall绑定接口的default方法调用DefaultCall CallableObject dflt一开始做动态方法时我习惯在委托方法里写AllArguments Object[] args因为它最省事无论目标方法几个参数都能兜住。但后来在压测中发现如果只需要取某一个参数用Argument(0)会更好。原因是Byte Buddy在生成字节码时Argument会把参数绑定固化成对局部变量的直接引用编译期就确定了下标而AllArguments需要额外生成一个数组对象并填充所有参数多了一步装箱和数组拷贝。想象一下快递分拣一个是直接把包裹递给指定货架上的工作人员一个是把所有包裹先倒进一个篮子里再找性能差距就是这样拉开的。签名匹配还有个容易被忽略的点委托方法应该尽量使用和目标方法相同的参数类型或者用其父类型。Byte Buddy在生成调用指令时会做类型校验一旦委托方法的参数类型和目标方法不兼容编译期不会报错但运行时会抛IllegalArgumentException提示找不到可绑定的方法。2.3 void方法、静态方法和构造方法在定义时的注意事项动态定义方法时返回值类型也容易踩坑。如果目标方法返回void我建议委托方法也保持void。虽然Byte Buddy在部分场景会自动处理多余的返回值但真实项目里一旦方法体复杂栈深了就容易出现VerifyError。宁可多写一个void委托方法也别让字节码栈顶留一个多余对象。这就像插座和插头——三孔插头插进两孔插座即使勉强能插进去接触不良的隐患一直都在。static方法的拦截又是另一个话题。static方法没有this所以This和SuperCall在静态方法拦截场景不可用。想要调用原静态方法通常得通过Origin Method拿到Method对象再用反射或者MethodHandle触发。另外构造方法的拦截用的是constructor(any())匹配拦截器里的参数绑定要从构造参数的0号下标开始。构造方法比较特殊它是没有返回值的所以委托方法也必须保持void签名熟悉之后其实也很好理解。定义这部分掌握之后真正的重头戏来了——拦截。拦截不是简简单单“换一个方法体”而是要清楚地知道你做的到底是哪一种手术。3. 拦截的三种手术方式subclass、rebase、redefine怎么选3.1 三种方式与原方法的关系Byte Buddy的拦截分三种模式subclass、rebase、redefine。名字看着像但原方法的去向完全不同。我习惯用“房子改造”来类比。subclass不改原房子在旁边加盖一个一模一样的户型再在新房子里做调整。原类字节码完全不变动态类加载为原类的子类。Mockito的mock对象就是这种模式。redefine直接把原房子的墙拆了重建旧墙不存在任何地方。也就是说原方法体被彻底丢弃无法再调用。rebase拆掉墙之前先把旧墙每一块砖编号拍照存到仓库然后新墙上留了一个隐藏通道可以回到仓库。对应到字节码就是原方法体被改名保存新方法体可以通过SuperCall调回原来的逻辑。三种方式的对比如下方式原类字节码原方法体典型场景subclass不变可通过superMock、测试替身、隔离加载新版本redefine被覆盖丢弃彻底替换逻辑不需要回退rebase被覆盖改名保留可通过SuperCall调用Agent埋点、APM、需要原方法执行的增强在实际的Agent开发中rebase是我默认选择。原因很简单只要原方法体还在我就保留了一条回退和兜底的路而redefine一旦把旧方法体丢了后续想对比新旧行为或者做灰度回滚就得靠外部系统手动恢复麻烦很多。3.2 SuperCall实现前置、后置、环绕增强的关键先看一个最典型的环绕增强写法。假设要对OrderService.queryOrder(String)做耗时统计public class OrderInterceptor { public static Object intercept(SuperCall CallableObject zuper, Origin Method method, Argument(0) String orderId) throws Exception { long start System.nanoTime(); try { return zuper.call(); } finally { System.out.println(method.getName() cost (System.currentTimeMillis() - start) ms); } } }SuperCall在rebase场景下会生成一个Callable调用的是原方法体。理解它可以类比成客服转接电话原本打电话的人直接找当事人现在电话先被客服坐席拦截器接起来坐席确认信息后把电话转接给当事人原方法体整个通话时长由坐席记录。这个模式天然支持前置、后置和环绕三种增强自由度非常高。还有一个容易被忽略的点SuperCall是可选的。如果我不想调用原方法直接返回一个固定值也行。这就埋下了下一章“优先级博弈”的引子——拦截器完全可以决定原方法到底跑不跑以及什么时候跑。3.3 构造方法与静态方法的拦截差异构造方法拦截用constructor(any())匹配一般用于给类实例注入一些额外属性或者统计对象创建次数。但构造方法不能像普通方法那样返回拦截值委托方法只能做副作用操作比如计数、打日志。我在实际项目中很少直接拦构造方法除非要做对象创建的访问追踪。静态方法拦截需要注意SuperCall不可用这点。因为静态方法没有实例上下文Byte Buddy无法生成“调用原方法”的Callable。我通常的做法是结合Origin Method拿到原方法引用然后通过MethodHandle调用。例如public static Object intercept(Origin Method method, AllArguments Object[] args) throws Throwable { MethodHandle handle MethodHandles.lookup().unreflectSpecial(method, ...); return handle.invokeWithArguments(args); }不过这种写法对访问权限要求较高在Agent场景里还涉及模块访问权限所以能不用尽量不用。如果只是想在静态方法入口加日志直接返回静态逻辑就行。拦截方式本身讲完了但真正的难点不是“能不能拦”而是“多个拦截逻辑同时存在时谁说了算”。这就是标题里“优先级博弈”的由来。4. 优先级博弈多个拦截逻辑并存时Byte Buddy的裁决规则4.1 同一个intercept调用只产生一次“手术”拦截不是AOP链先说一个最常见的误解很多人以为对同一个方法调用两次.intercept(...)会像Spring AOP的Advice链一样串起来执行。实际完全不是这样。Byte Buddy的.intercept()短语相当于一次性的手术决定——后一次调用会覆盖前一次决定而不是追加成一条链。如果你写builder.method(named(queryOrder)) .intercept(MethodDelegation.to(TimingInterceptor.class)); builder.method(named(queryOrder)) .intercept(MethodDelegation.to(CacheInterceptor.class));最终线上生效的只会是后者CacheInterceptor前者的耗时统计逻辑直接消失。这个特性一开始让我很意外后来想明白了Byte Buddy修改的是类的方法体方法体只能有一个它不是一个拦截器容器。想要多个逻辑共存得主动设计容器。基于这个认知我总结了两种实现“多个拦截逻辑共存”的姿势一种是用BindingPriority在单个MethodDelegation内控制多个候选委托方法的选择另一种是用Advice的wrap嵌套形成洋葱模型。下面分别讲。4.2 BindingPriority多个委托候选方法的选择裁决MethodDelegation允许在一个委托类里写多个方法Byte Buddy会从这些方法里挑一个来绑定。问题是如果多个方法都能匹配参数签名它怎么选Byte Buddy的默认选择规则是“最具体匹配优先”能绑定的参数数量越多、类型越具体的方法越容易被选中。举例来说一个能接收Argument(0) String的方法通常比只接收AllArguments Object[]的方法优先。但“通常”这种东西在复杂签名下并不总是符合直觉。我在写一个既有缓存逻辑又有权限校验的拦截器时就遇到过两个方法都能匹配目标签名但Byte Buddy选中的不是我要的那个。这时候就需要显式裁决——用BindingPriority拉开差距public class OrderInterceptor { BindingPriority(10) public static Object cacheFirst(SuperCall CallableObject zuper, Argument(0) String orderId) throws Exception { String cached cache.get(orderId); if (cached ! null) { return cached; } return zuper.call(); } BindingPriority(1) public static Object auditOnly(SuperCall CallableObject zuper, Origin Method method) throws Exception { auditLog.log(method.getName()); return zuper.call(); } }BindingPriority(10)的方法会覆盖BindingPriority(1)的方法数值大者胜出。这里需要强调它只能作用于“同一个MethodDelegation内部的多个候选委托方法”不是跨intercept调用链。换句话说它解决的是“选哪一个委托方法”的问题而不是“多个advice谁先谁后”的问题。后者要用下面这种wrap方式。4.3 Advice的wrap嵌套顺序洋葱模型式的优先级控制如果你需要非常精确地控制多个增强逻辑的先后顺序推荐用Advice配合wrap嵌套。Byte Buddy的Advice.to(...)专门用于把某个Advice类的方法进入逻辑和退出逻辑织入目标方法而且它支持wrap组合builder.method(named(queryOrder)) .intercept( Advice.to(AuthAdvice.class).wrap( Advice.to(TimingAdvice.class).wrap( MethodDelegation.to(OrderHandler.class) ) ) );这里的关键是要理解wrap顺序。它的语义和中间件洋葱模型一致最外层的Advice先进入目标方法后退出目标方法最内层先被包裹最后进入但最先离开。假设AuthAdvice在Advice.OnMethodEnter里做权限校验TimingAdvice在Advice.OnMethodEnter里开启计时、在Advice.OnMethodExit里打印耗时。上面这段代码的执行顺序是AuthAdvice的enter执行校验通过才继续。TimingAdvice的enter执行开始计时。OrderHandler这个委托方法执行这里可以调用原方法或直接返回降级结果。TimingAdvice的exit执行打印耗时。AuthAdvice的exit执行如果有的话。这个模型调试起来很直观外层负责“准入”内层负责“业务”最内层决定“要不要干正事”。不过我也得提醒一句wrap嵌套到三层以上时可读性会快速下降。我的习惯是超三层就拆成一个显式的责任链对象避免读者看到一串括号直接放弃。4.4 Agent插件叠加时的顺序风险还有一种“博弈”发生在Agent插件层面。如果你在一个JVM里安装了多个基于Byte Buddy的Agent而它们恰好都想改同一个类的同一个方法最终效果取决于Agent的安装顺序和installOn的执行时机。这个问题通常不好从单个插件的代码里看出来只有联调时才会暴露某个插件先transform另一个插件再transform前面的改动很可能被后面的动作覆盖。我的应对办法是做Agent插件时一律用rebase而不是redefine。只要原方法体还保留着一份即便多个插件发生叠加至少后一个插件还能通过SuperCall拿到上一个插件处理后的“当前原方法体”损失的只是“最原始”方法的可追溯性。这个取舍在联调时能帮你少很多跨团队的扯皮。5. 完整实战给OrderService动态加方法并织入缓存降级链路5.1 需求目标与方案设计前面的规则讲了不少现在用一个完整例子把它们串起来。需求背景如下有一个OrderService类无接口不能改源码。我要在不重启、不动原代码的前提下用Byte Buddy生成一个增强类满足三个目标增加一个getMetrics()方法能返回本周期的调用次数和累计耗时。拦截queryOrder(String)方法记录调用次数和耗时。实现缓存降级同一个orderId第二次请求时直接返回缓存不再进入原方法。方案采用subclass WRAPPER加载方式因为这是普通Java项目里最容易复现的一种路径不依赖Java Agent的premain配置。5.2 核心代码实现先看原始类没有任何埋点public class OrderService { public String queryOrder(String orderId) { // 模拟一次真实的数据库查询 try { Thread.sleep(50); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } return order-detail: orderId; } }增强器的核心逻辑import net.bytebuddy.ByteBuddy; import net.bytebuddy.description.modifier.Visibility; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; import net.bytebuddy.implementation.MethodDelegation; import static net.bytebuddy.matcher.ElementMatchers.named; public class OrderEnhancer { public static void main(String[] args) throws Exception { Class? extends OrderService enhanced new ByteBuddy() .subclass(OrderService.class) .name(com.example.enhanced.OrderServiceEnhancer) .defineMethod(getMetrics, String.class, Visibility.PUBLIC) .intercept(MethodDelegation.to(MetricsDelegate.class)) .method(named(queryOrder)) .intercept(MethodDelegation.to(OrderInterceptor.class)) .make() .load(OrderEnhancer.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); OrderService service enhanced.getDeclaredConstructor().newInstance(); System.out.println(service.queryOrder(A1001)); System.out.println(service.queryOrder(A1002)); System.out.println(service.queryOrder(A1001)); String metrics (String) enhanced.getMethod(getMetrics).invoke(service); System.out.println(metrics); } }关键的拦截和统计逻辑都在OrderInterceptor里它通过BindingPriority和SuperCall实现了“缓存优先、原方法兜底、统计最后”的优先级编排import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import net.bytebuddy.implementation.bind.annotation.Argument; import net.bytebuddy.implementation.bind.annotation.SuperCall; public class OrderInterceptor { private static final MapString, String CACHE new ConcurrentHashMap(); private static final AtomicInteger CALL_COUNT new AtomicInteger(); private static final AtomicLong TOTAL_TIME new AtomicLong(); public static Object intercept(SuperCall CallableObject zuper, Argument(0) String orderId) throws Exception { CALL_COUNT.incrementAndGet(); long start System.nanoTime(); try { // 优先级最高缓存命中则直接降级返回不调用原方法 String cached CACHE.get(orderId); if (cached ! null) { return cached; } // 优先级次之执行原方法并把结果写入缓存 String result (String) zuper.call(); CACHE.put(orderId, result); return result; } finally { // 优先级兜底无论上面走哪条分支都要累计耗时 TOTAL_TIME.addAndGet(System.nanoTime() - start); } } public static String metrics() { return count CALL_COUNT.get() , totalNanos TOTAL_TIME.get(); } }MetricsDelegate只是转发了一层方便动态方法直接调用统计逻辑public class MetricsDelegate { public static String getMetrics() { return OrderInterceptor.metrics(); } }5.3 验证增强效果与优先级决策直接运行main方法输出会类似下面这样order-detail:A1001 order-detail:A1002 order-detail:A1001 count3, totalNanosroughly 100000000注意第三次查A1001时输出的字符串虽然和第一次相同但我的代码里并没有调用zuper.call()——这就是“缓存优先”生效的表现。而count3是因为每次进入拦截器都会计数无论缓存是否命中。这个设计有意为之缓存命中意味着原数据库查询被跳过了但调用行为本身仍然需要纳入监控。这个例子把整套逻辑串起来了defineMethod定义了新方法。method(named(queryOrder)).intercept(...)实现了方法拦截。SuperCall保留了调用原方法的能力。if(cached ! null) return cached实现了“是否跳过原方法”的优先级决策。finally块保证了统计逻辑在任何一个分支下都会执行这就是一种“优先级兜底”思想。这段代码放到线上需要注意的是本地缓存只适合单机演示实际生产场景往往需要换成分布式缓存并且要加过期机制。不过Byte Buddy增强的部分没有任何分布式绑定它只负责类和方法层面的改造缓存策略完全是我自己的代码这一点也让调试特别方便。6. 高频踩坑与排查思路6.1 委托方法绑定失败先查签名再查构造Byte Buddy报错最常见的一类是IllegalArgumentException: None of [...] allows for delegation翻译过来就是“目标方法没有找到任何一个合适的委托方法”。这时候我的排查顺序很固定第一检查目标方法签名。用反射把方法名和参数类型列出来核对委托方法的Argument下标和类型。尤其是基本类型和包装类型混用比如目标方法传的是int委托方法写IntegerByte Buddy在某些版本里不会自动拆箱需要保持完全一致。第二检查委托类实例化方式。如果委托方法是非静态的Byte Buddy需要创建委托类实例那么它就需要一个无参构造。一旦无参构造不可见绑定一样会失败。我的习惯是委托方法一律写成public static彻底绕开实例化问题。第三检查嵌套类访问权限。如果你把委托类写成某个类的内部类但没加static字节码层面会多一个外部类引用同样会造成绑定失败。所以我在项目里统一要求委托类必须是独立的public class或public static内部类。6.2 ClassLoader隔离导致的ClassCastException与NoSuchMethodErrorByte Buddy生成新类之后用什么ClassLoader加载是个大学问。ClassLoadingStrategy.Default.WRAPPER会在内存里新建一个隔离ClassLoader并优先委托给父加载器大多数情况下强转是正常的。但如果用了CHILD_FIRST策略目标类本身会被子加载器重新加载一份于是JVM里出现两个全限定名相同、但Class对象不同的类。典型报错是业务代码拿到动态实例后直接强转成OrderService结果抛出ClassCastException反射调用getMethod(queryOrder, ...)也可能会得到NoSuchMethodError。这个坑的根因不在Byte Buddy而在于类加载器的可见性规则——同一个全限定名在不同加载器里是不同的类型。我的处理经验是基础类比如被增强的父类和业务接口固定由父加载器加载动态生成的子类放进子加载器。如果还是不行就让动态类强制实现一个由根加载器可见的接口调用方永远只面向接口编程这样隔离问题就会被控制在最小范围。6.3 已加载类不能“新增方法”JVM的限制与应对这里必须澄清一个概念如果你今天打开一个运行中的JVM用Instrumentation对一个已经被加载的类做retransform想给它新增一个方法或字段绝大多数情况下是做不到的。JVM对redefine和retransform有一条硬性限制不能改变类的结构即不能增删方法、不能增删字段、不能改变方法签名。换句话说标题里的“动态方法定义”其实有两个落地路径在类首次加载之前通过AgentBuilder的transform钩子修改类定义这时候可以自由新增方法。在类已加载之后通过生成subclass或者新的ClassLoader来“制造”一个带方法的新类而不是直接改老类。很多网上教程写“运行时不重启给类加方法”要么走的是第一条路径类还没被加载你抢在加载前改掉了字节码要么走的是第二条路径生成子类替换实例。理解这一点之后再回去看我第5章的实战案例你就能明白为什么我最后选的是subclass WRAPPER——那是普通Java进程里最稳妥、也最容易复现的方式。6.4 调试Byte Buddy的实用手段Byte Buddy这类字节码工具最怕“黑盒运行”——方法似乎被改了但改成了什么样完全不知道。我的调试三板斧分享给你。第一板斧把生成的class落盘。在.make()之后、.load()之前把字节码写到本地文件byte[] bytes new ByteBuddy() .subclass(OrderService.class) .method(named(queryOrder)) .intercept(MethodDelegation.to(OrderInterceptor.class)) .make() .getBytes(); Files.write(Paths.get(/tmp/OrderServiceEnhancer.class), bytes);然后用javap -v -p /tmp/OrderServiceEnhancer.class看方法列表和字节码指令确认queryOrder方法体确实被替换成了委托调用。第二板斧在拦截器里加上Origin Method method用反射把方法签名和注解打印出来。这个方法成本最低几乎每次排查绑定问题时都能用上它能直观告诉你Byte Buddy到底选中了哪一个Method对象。第三板斧使用反编译工具。字节码不够直观时把class文件拖进IDE的反编译器直接读成Java代码。遇到“为什么拦截后SuperCall找不到原方法”这类问题反编译后的方法体基本一眼就能看出问题在哪。我自己的习惯是从第三板斧反着来先反编译看全局再用javap -v核字节码细节最后落到Origin打印去验证运行时行为。一套组合拳下来Byte Buddy的绝大多数“黑盒”问题都能被快速定位。老实说Byte Buddy的门槛不在于API本身而在于你脑海里有没有一套清晰的字节码模型——类结构何时能被改动、类加载器如何影响类型可见性、rebase与redefine之间那点“保留原方法”的微妙差异。把这几条主线理清楚之后动态方法定义和拦截就只剩书写代码了。希望这篇实战记录能让你少走几步弯路。
返回列表