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.classtoString() 实现。

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 查看。


« 上一篇: Vaadin入门介绍