- 能够基于Java Agent编写出普通类的代理
- 理解Byte Buddy的作用
- 能够基于Byte Buddy编写动态代理
1 Byte Buddy
Byte Buddy
是一个代码生成和操作库,用于在 Java
应用程序运行时创建和修改 Java
类,而无需编译器的帮助。除了 Java
类库附带的代码生成实用程序外,Byte Buddy
还允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外,Byte Buddy
提供了一种方便的 API,可以使用 Java
代理或在构建过程中手动更改类。
- 无需理解字节码指令,即可使用简单的 API 就能很容易操作字节码,控制类和方法。
- 已支持Java 11,库轻量,仅取决于Java字节代码解析器库ASM的访问者API,它本身不需要任何其他依赖项。
- 比起JDK动态代理、cglib、Javassist,Byte Buddy在性能上具有一定的优势。
1.1 Byte Buddy应用场景
Java 是一种强类型的编程语言,即要求所有变量和对象都有一个确定的类型,如果在赋值操作中出现类型不兼容的情况,就会抛出异常。强类型检查在大多数情况下是可行的,然而在某些特殊场景下,强类型检查则成了巨大的障碍。
我们在做一些通用工具封装的时候,类型检查就成了很大障碍。比如我们编写一个通用的Dao实现数据操作,我们根本不知道用户要调用的方法会传几个参数、每个参数是什么类型、需求变更又会出现什么类型,几乎没法在方法中引用用户方法中定义的任何类型。我们绝大多数通用工具封装都采用了反射机制,通过反射可以知道用户调用的方法或字段,但是Java反射有很多缺陷:
1:反射性能很差 | |
2:反射能绕开类型安全检查,不安全,比如权限暴力破解 |
java编程语言代码生成库不止 Byte Buddy 一个,以下代码生成库在 Java 中也很流行:
- Java Proxy
Java Proxy 是 JDK 自带的一个代理工具,它允许为实现了一系列接口的类生成代理类。Java Proxy 要求目标类必须实现接口是一个非常大限制,例如,在某些场景中,目标类没有实现任何接口且无法修改目标类的代码实现,Java Proxy 就无法对其进行扩展和增强了。
- CGLIB
CGLIB 诞生于 Java 初期,但不幸的是没有跟上 Java 平台的发展。虽然 CGLIB 本身是一个相当强大的库,但也变得越来越复杂。鉴于此,导致许多用户放弃了 CGLIB 。
- Javassist
Javassist 的使用对 Java 开发者来说是非常友好的,它使用Java 源代码字符串和 Javassist 提供的一些简单 API ,共同拼凑出用户想要的 Java 类,Javassist 自带一个编译器,拼凑好的 Java 类在程序运行时会被编译成为字节码并加载到 JVM 中。Javassist 库简单易用,而且使用 Java 语法构建类与平时写 Java 代码类似,但是 Javassist 编译器在性能上比不了 Javac 编译器,而且在动态组合字符串以实现比较复杂的逻辑时容易出错。
- Byte Buddy
Byte Buddy 提供了一种非常灵活且强大的领域特定语言,通过编写简单的 Java 代码即可创建自定义的运行时类。与此同时,Byte Buddy 还具有非常开放的定制性,能够应付不同复杂度的需求。
上面所有代码生成技术中,我们推荐使用Byte Buddy,因为Byte Buddy代码生成可的性能最高,Byte Buddy 的主要侧重点在于生成更快速的代码,如下图:
1.2 Byte Buddy学习
我们接下来详细讲解一下Byte Buddy Api,对重要的方法和类进行深度剖析。
1.2.1 ByteBuddy语法
任何一个由 Byte Buddy 创建/增强的类型都是通过 ByteBuddy 类的实例来完成的,我们先来学习一下ByteBuddy类,如下代码:
DynamicType.Unloaded<?> dynamicType = new ByteBuddy() | |
// 生成 Object的子类 | |
.subclass(Object.class) | |
// 生成类的名称为"com.itheima.Type" | |
.name("com.itheima.Type") | |
.make(); |
Byte Buddy 动态增强代码总共有三种方式:
subclass:对应 ByteBuddy.subclass() 方法。这种方式比较好理解,就是为目标类(即被增强的类)生成一个子类,在子类方法中插入动态代码。 | |
rebasing:对应 ByteBuddy.rebasing() 方法。当使用 rebasing 方式增强一个类时,Byte Buddy 保存目标类中所有方法的实现,也就是说,当 Byte Buddy 遇到冲突的字段或方法时,会将原来的字段或方法实现复制到具有兼容签名的重新命名的私有方法中,而不会抛弃这些字段和方法实现。从而达到不丢失实现的目的。这些重命名的方法可以继续通过重命名后的名称进行调用。 | |
redefinition:对应 ByteBuddy.redefine() 方法。当重定义一个类时,Byte Buddy 可以对一个已有的类添加属性和方法,或者删除已经存在的方法实现。如果使用其他的方法实现替换已经存在的方法实现,则原来存在的方法实现就会消失。 |
通过上述三种方式完成类的增强之后,我们得到的是 DynamicType.Unloaded
对象,表示的是一个未加载的类型,我们可以使用 ClassLoadingStrategy
加载此类型。Byte Buddy
提供了几种类加载策略,这些加载策略定义在 ClassLoadingStrategy.Default
中,其中:
- WRAPPER 策略:创建一个新的 ClassLoader 来加载动态生成的类型。
- CHILD_FIRST 策略:创建一个子类优先加载的 ClassLoader,即打破了双亲委派模型。
- INJECTION 策略:使用反射将动态生成的类型直接注入到当前 ClassLoader 中。
实现如下:
Class<?> dynamicClazz = new ByteBuddy() | |
// 生成 Object的子类 | |
.subclass(Object.class) | |
// 生成类的名称为"com.itheima.Type" | |
.name("com.itheima.Type") | |
.make() | |
.load(Demo.class.getClassLoader(), | |
//使用WRAPPER 策略加载生成的动态类型 | |
ClassLoadingStrategy.Default.WRAPPER) | |
.getLoaded(); |
前面动态生成的 com.itheima.Type
类型只是简单的继承了 Object 类,在实际应用中动态生成新类型的一般目的就是为了增强原始的方法,下面通过一个示例展示 Byte Buddy 如何增强 toString()
方法:
// 创建ByteBuddy对象 | |
String str = new ByteBuddy() | |
// subclass增强方式 | |
.subclass(Object.class) | |
// 新类型的类名 | |
.name("com.itheima.Type") | |
// 拦截其中的toString()方法 | |
.method(ElementMatchers.named("toString")) | |
// 让toString()方法返回固定值 | |
.intercept(FixedValue.value("Hello World!")) | |
.make() | |
// 加载新类型,默认WRAPPER策略 | |
.load(ByteBuddy.class.getClassLoader()) | |
.getLoaded() | |
// 通过 Java反射创建 com.xxx.Type实例 | |
.newInstance() | |
// 调用 toString()方法 | |
.toString(); |
首先需要关注这里的 method() 方法,method() 方法可以通过传入的 ElementMatchers 参数匹配多个需要修改的方法,这里的ElementMatchers.named("toString")
即为按照方法名匹配 toString() 方法。如果同时存在多个重载方法,则可以使用 ElementMatchers 其他 API 描述方法的签名,如下所示:
// 指定方法名称 | |
ElementMatchers.named("toString") | |
// 指定方法的返回值 | |
.and(ElementMatchers.returns(String.class)) | |
// 指定方法参数 | |
.and(ElementMatchers.takesArguments(0)); |
接下来需要关注的是 intercept()
方法,通过 method()