1. 概述
ByteBuddy 是一个用于在运行时动态生成 Java 类的库。
本文我们将学习利用该框架来操作现有的类、如何创建新的Java类,以及拦截方法调用。
2. 依赖安装
对于基于 Maven 的项目,我们需要在 pom.xml
中添加以下依赖:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.6</version>
</dependency>
而对于基于 Gradle 的项目,我们需要在 build.gradle
文件中添加相同的依赖:
compile net.bytebuddy:byte-buddy:1.12.13
最新的版本可以在 Maven Central 找到。
3. 在运行时创建 Java 类
下面开始我们的第一个简单示例。
在这个例子中,我们创建了一个新的Class,它是 Object.class 的子类,并重写了 toString() 方法:
DynamicType.Unloaded unloadedType = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.isToString())
.intercept(FixedValue.value("Hello World ByteBuddy!"))
.make();
刚才我们所做的就是创建了一个 ByteBuddy 的实例。 subclass() 指定需要继承的父类,这里是 Object.class。并使用 ElementMatchers 选择了父类(Object.class)的 toString() 方法。
使用 intercept() 方法提供了 toString() 的实现,并返回一个固定的值。
最后通过 make() 方法完成了新类的生成。
此时,我们的类已经创建完成,但还没有加载到 JVM 中。它由 DynamicType.Unloaded 的一个实例表示,这是生成类型的二进制形式。
因此,在使用之前,我们需要将生成的类加载到 JVM 中:
Class<?> dynamicType = unloadedType.load(getClass()
.getClassLoader())
.getLoaded();
现在,我们可以实例化 dynamicType 并在其上调用 toString() 方法:
assertEquals(
dynamicType.newInstance().toString(), "Hello World ByteBuddy!");
注意,直接调用 dynamicType.toString() 不会起作用,因为这只会调用 ByteBuddy.class 的 toString() 实现。
newInstance() 是一个 Java 反射方法,它创建一个由这个 ByteBuddy 对象表示的类型的新实例,作用类似于 new 关键字。
到目前为止,我们学会了如何动态创建一个类,实现继承并重写方法,只不过返回的是一个固定值。在接下来的章节中,我们将探讨如何定义具有自定义逻辑的方法。
4. 方法委托与自定义逻辑
在之前的示例中,我们在 toString() 方法中返回了一个固定的值。
在实际应用中,需要的逻辑比这复杂得多。一个有效的方法是通过方法调用委托来提供自定义逻辑。
让我们创建一个动态类型,它继承自 Foo.class,并且有一个名为 sayHelloFoo() 的方法:
public String sayHelloFoo() {
return "Hello in Foo!";
}
此外,我们创建另一个名为 Bar 的类,它有一个与 sayHelloFoo() 具有相同签名和返回类型的静态方法 *sayHelloBar()*:
public static String sayHelloBar() {
return "Holla in Bar!";
}
现在,我们使用 ByteBuddy 的 DSL 将所有对 sayHelloFoo() 的调用委托给 *sayHelloBar()*。这样,我们就可以在运行时为新创建的类提供纯 Java 编写的自定义逻辑:
String r = new ByteBuddy()
.subclass(Foo.class)
.method(named("sayHelloFoo")
.and(isDeclaredBy(Foo.class)
.and(returns(String.class))))
.intercept(MethodDelegation.to(Bar.class))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance()
.sayHelloFoo();
assertEquals(r, Bar.sayHelloBar());
调用 sayHelloFoo() 将相应地调用 *sayHelloBar()*。
ByteBuddy 如何知道在 Bar.class 中调用哪个方法? 它根据方法签名、返回类型、方法名称和注解来选择匹配的方法。
sayHelloFoo() 和 sayHelloBar() 的方法名称不同,但它们具有相同的签名和返回类型。
如果 Bar.class 中存在多个具有匹配签名和返回类型的可调用方法,我们可以使用 @BindingPriority 注解来解决歧义。
@BindingPriority 接收一个整数参数 - 整数值越高,调用特定实现的优先级越高。因此,在下面的代码片段中,sayHelloBar() 将优先于 sayBar() 被调用:
@BindingPriority(3)
public static String sayHelloBar() {
return "Holla in Bar!";
}
@BindingPriority(2)
public static String sayBar() {
return "bar";
}
5. 方法和字段定义
我们已经能够覆盖动态类型超类中声明的方法。接下来,我们进一步向类中添加一个新方法(以及一个字段)。
我们将使用 Java 反射来调用动态创建的方法:
Class<?> type = new ByteBuddy()
.subclass(Object.class)
.name("MyClassName")
.defineMethod("custom", String.class, Modifier.PUBLIC)
.intercept(MethodDelegation.to(Bar.class))
.defineField("x", String.class, Modifier.PUBLIC)
.make()
.load(
getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
Method m = type.getDeclaredMethod("custom", null);
assertEquals(m.invoke(type.newInstance()), Bar.sayHelloBar());
assertNotNull(type.getDeclaredField("x"));
我们创建了一个名为 MyClassName 的类,它是 Object.class 的子类。然后,我们定义了一个返回 String 的方法 custom,并设置了 public 访问修饰符。
就像前面的示例一样,我们通过拦截对它的调用并将其委托给本教程早期创建的 Bar.class 来实现我们的方法。
6. 重新定义已存在的类
尽管我们一直在处理动态创建的类,但也可以处理已加载的类。这可以通过重新定义(或基类重置)现有的类,并使用 ByteBuddyAgent 将其重新加载到 JVM 中来实现。
首先,让我们将 ByteBuddyAgent 添加到我们的 pom.xml
:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.7.1</version>
</dependency>
最新的版本可以在 这里 找到。
现在,让我们重新定义先前在 Foo.class 中创建的 sayHelloFoo() 方法:
ByteBuddyAgent.install();
new ByteBuddy()
.redefine(Foo.class)
.method(named("sayHelloFoo"))
.intercept(FixedValue.value("Hello Foo Redefined"))
.make()
.load(
Foo.class.getClassLoader(),
ClassReloadingStrategy.fromInstalledAgent());
Foo f = new Foo();
assertEquals(f.sayHelloFoo(), "Hello Foo Redefined");
7. 总结
在这篇详尽的指南中,我们深入探讨了 ByteBuddy 库的功能以及如何使用它高效地创建动态类。
它的 文档 提供了关于库内部工作和其他方面的深入解释。
一如既往,本教程的所有完整代码片段可在 Github 查看。