ByteBuddy
- 一、简介
- 二、常用API
- 1、入门使用
- 2、对类插桩
- 3、对方法插桩
- 4、插桩插入
- 5、方法委托
- 6、动态修改入参
- 7、清空方法体
- 三、Java agent
- 1、原生JDK实现
- 2、ByteBuddy实现
- 四、框架应用
一、简介
ByteBuddy是基于 ASM (ow2.io)实现的字节码操作类库。比起ASM,ByteBuddy的API更加简单易用。开发者无需了解 class file format知识,也可通过ByteBuddy完成字节码编辑。
- ByteBuddy使用java5实现,并且支持生成JDK6及以上版本的字节码(由于jdk6和jdk7使用未加密的HTTP类库, 作者建议至少使用jdk8版本)
- 和其他字节码操作类库一样,ByteBuddy支持生成类和修改现存类
- 与与静态编译器类似,需要在快速生成代码和生成快速的代码之间作出平衡,ByteBuddy主要关注以最少的运行时间生成代码
JIT优化后的平均ns纳秒耗时(标准差) | 基线 | Byte Buddy | cglib | Javassist | Java proxy |
普通类创建 | 0.003 (0.001) | 142.772 (1.390) | 515.174 (26.753) | 193.733 (4.430) | 70.712 (0.645) |
接口实现 | 0.004 (0.001) | 1’126.364 (10.328) | 960.527 (11.788) | 1’070.766 (59.865) | 1’060.766 (12.231) |
stub方法调用 | 0.002 (0.001) | 0.002 (0.001) | 0.003 (0.001) | 0.011 (0.001) | 0.008 (0.001) |
类扩展 | 0.004 (0.001) | 885.983 5’408.329 (7.901) (52.437) | 1’632.730 (52.737) | 683.478 (6.735) | – |
super method invocation | 0.004 (0.001) | 0.004 0.004 (0.001) (0.001) | 0.021 (0.001) | 0.025 (0.001) | – |
上表通过一些测试,对比各种场景下,不同字节码生成的耗时。对比其他同类字节码生成类库,Byte Buddy在生成字节码方面整体耗时还是可观的,并且生成后的字节码运行时耗时和基线十分相近。
1) Java 代理
Java 类库自带的一个代理工具包,它允许创建实现了一组给定接口的类。这个内置的代理很方便,但是受到的限制非常多。 例如,上面提到的安全框架不能以这种方式实现,因为我们想要扩展类而不是接口。
2) cglib
该代码生成库是在 Java 开始的最初几年实现的,不幸的是,它没有跟上 Java 平台的发展。尽管如此,cglib仍然是一个相当强大的库, 但它是否积极发展变得很模糊。出于这个原因,许多用户已不再使用它。
(cglib目前已不再维护,并且github中也推荐开发者转向使用Byte Buddy)
3) Javassist
该库带有一个编译器,该编译器采用包含 Java 源码的字符串,这些字符串在应用程序运行时被翻译成 Java 字节码。 这是非常雄心勃勃的,原则上是一个好主意,因为 Java 源代码显然是描述 Java 类的非常的好方法。但是, Javassist 编译器在功能上无法与 javac 编译器相比,并且在动态组合字符串以实现更复杂的逻辑时容易出错。此外, Javassist 带有一个代理库,它类似于 Java 的代理程序,但允许扩展类并且不限于接口。然而, Javassist 代理工具的范围在其API和功能方面同样受限限制。
二、常用API
1、入门使用
1)引入maven依赖
2)创建代理类
因为我们只进行了代理,所以代理对象内部只有一个代理方法
2、对类插桩
1)不指定任何特别的参数, 只声明为JDK自带类的子类
生成代理类全限定类名路径:net.bytebuddy.renamed.java.lang.Object
2)指定父类为非JDK自带类, 不指定命名策略和其他参数
新建ByteBuddyHandler类,路径:pers.mobian.bytebuddydemo.handler.ByteBuddyHandler
生成代理类全限定类名路径:pers.mobian.bytebuddydemo.handler.ByteBuddyHandler
3)指定父类为JDK自带类(ArrayList), 指定命名策略
使用官方教程建议的Byte Buddy自带的命名策略 (NamingStrategy.SuffixingRandom)
生成代理类全限定类名路径:net.bytebuddy.renamed.java.util.ArrayList$mobian$9sA0CzVt
4)父类非JDK自带类, 指定命名策略和具体类名
生成代理类全限定类名路径:pers.mobian.bytebuddydemo.ByteBuddyHandlerWrapper
5)将生成的字节码, 注入一个jar包中
生成代理类全限定类名路径:com.example.ByteBuddyHandlerWrapper
6)修改/增强现有类主要有3种方法
- .subclass(目标类.class):继承目标类,以子类的形式重写超类方法,达到增强效果
- .rebase(目标类.class):变基,原方法变为private,并且方法名增加&origanl&{随机字符串}后缀,目标方法体替换为指定逻辑
- .redefine(目标类.class):重定义,原方法体逻辑直接替换为指定逻辑
其中rebase无法直接在class中展示,具体效果需要通过bytecode进行查询
3、对方法插桩
结果:
4、插桩插入
1)插入新方法
- .defineMethod(方法名, 方法返回值类型, 方法访问描述符): 定义新增的方法
- .withParameters(Type…): 定义新增的方法对应的形参类型列表
- .intercept(XXX): 和修改/增强现有方法一样,对前面的方法对象的方法体进行修改
2)插入新属性
5、方法委托
方法委托,可简单理解将目标方法的方法体逻辑修改为调用指定的某个辅助类方法。
1)委托给相同签名的静态方法/实例方法
- .intercept(MethodDelegation.to(Class<?> type)):将被拦截的方法委托给指定的增强类,增强类中需要定义和目标方法一致的方法签名,然后多一个static访问标识
- .intercept(MethodDelegation.to(Object target)):将被拦截的方法委托给指定的增强类实例,增强类可以指定和目标类一致的方法签名,或通过@RuntimeType指示 Byte Buddy 终止严格类型检查以支持运行时类型转换。
2)自定义方法
- @RuntimeType:指示ByteBuddy终止严格类型检查以支持运行时类型转换
- @This Object targetObj:表示被拦截的目标对象, 只有拦截实例方法时可用(无法拦截静态方法)
- @Origin Method targetMethod:表示被拦截的目标方法, 只有拦截实例方法或静态方法时可用
- @Origin Class clazz:获取静态方法所处的Class对象
- @AllArguments Object[] targetMethodArgs:目标方法的参数
- @Super Object targetSuperObj:表示被拦截的目标对象, 只有拦截实例方法时可用 (可用来调用目标类的super方法,无法拦截静态方法)。若明确知道具体的超类(父类类型),这里Object可以替代为具体超类(父类)
- @SuperCall Callable<?> zuper:用于调用目标方法
其中调用目标方法时,需要通过Object result = zuper.call()调用。不能直接通过反射的Object result = targetMethod.invoke(targetObj,targetMethodArgs)进行原方法调用。因为后者会导致无限递归进入当前增强方法逻辑。
补充:
- @SuperCall仅在原方法仍存在的场合能够正常使用,比如subclass超类方法仍为目标方法,而rebase则是会重命名目标方法并保留原方法体逻辑;但redefine直接替换掉目标方法,所以@SuperCall不可用
- rebase和redefine都可以修改目标类静态方法,但是若想在原静态方法逻辑基础上增加其他增强逻辑,那么只有rebase能通过@SuperCall或@Morph调用到原方法逻辑;redefine不保留原目标方法逻辑
3)构造方法
- .constructor(ElementMatchers.any()): 表示拦截目标类的任意构造方法
- .intercept(SuperMethodCall.INSTANCE.andThen(Composable implementation): 表示在实例构造方法逻辑执行结束后再执行拦截器中定义的增强逻辑
- @This: 被拦截的目标对象this引用,构造方法也是实例方法,同样有this引用可以使用
6、动态修改入参
@Morph和@SuperCall功能基本一致,主要区别在于@Morph支持传入参数。
使用@Morph时,需要在拦截方法注册代理类/实例前,指定install注册配合@Morph使用的函数式接口,其入参必须为Object[]类型,并且返回值必须为Object类型。
7、清空方法体
三、Java agent
1、原生JDK实现
1)引入maven依赖&指定打包方式
2)创建被调用接口
3)编写Agent类
4)Agent启动类
5)当前项目maven完成打包,生产jar包
6)启动目标项目,启动vm参数指定agent的jar
-javaagent:/Users/mobian/person/ByteBuddyDemo/ByteBuddyDemo-0.0.1-SNAPSHOT.jar=name=mobian
2、ByteBuddy实现
四、框架应用
SkyWalking、Mockito、Spring、Arthas等