ByteBuddy -- 简单介绍和入门

本文最后更新于:2023年4月15日 晚上

什么是ByteBuddy

github仓库: https://github.com/raphw/byte-buddy

Byte Buddy is a code generation and manipulation library for creating and modifying Java classes during the runtime of a Java application and without the help of a compiler. Other than the code generation utilities that ship with the Java Class Library, Byte Buddy allows the creation of arbitrary classes and is not limited to implementing interfaces for the creation of runtime proxies. Furthermore, Byte Buddy offers a convenient API for changing classes either manually, using a Java agent or during a build.

Byte Buddy就是一个可以在Java运行时动态生成java class的类库

存在的意义

对于一个工具的入门, 我们首先希望知道这个工具的出现是为了解决什么问题

据官方所言:

At first sight, runtime code generation can appear to be some sort of black magic that should be avoided and only few developers write applications that explicitly generate code during their runtime. However, this picture changes when creating libraries that need to interact with arbitrary code and types that are unknown at compile time. In this context, a library implementer must often choose between either requiring a user to implement library-proprietary interfaces or to generate code at runtime when the user’s types becomes first known to the library. Many known libraries such as for example Spring or Hibernate choose the latter approach which is popular among their users under the term of using Plain Old Java Objects. As a result, code generation has become an ubiquitous concept in the Java space. Byte Buddy is an attempt to innovate the runtime creation of Java types in order to provide a better tool set to those relying on code generation.

乍一看,运行时代码生成似乎是某种应该避免的黑魔法,只有少数开发者编写了在运行时显式生成代码的应用程序。但是,当创建需要与编译时未知的任意代码和类型交互的库时,情况会发生变化。在这种情况下,库实现者通常必须在要求用户实现库专有接口或在库首次知道用户类型时在运行时生成代码之间做出选择。许多已知的库(例如Spring或Hibernate)选择后一种方法,这种方法在使用POJO的用户中很流行. 因此,代码生成已成为 Java 领域中无处不在的概念。Byte Buddy 尝试创新 Java 类型的运行时创建,以便为那些依赖代码生成的人提供更好的工具集。

入门示例

1 | 创建一个覆写toString的类

下面的代码通过一个简单的示例展示了ByteBuddy是如何动态创建一个类的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testByteBuddyWithToStringMethod() throws Exception {
// 字节码修改相关技术
// byte-buddy: 动态操作运行时java类
// subclass: 动态创建的类继承自Object
// ElementMatchers: 筛选器, 选中Object.isToString方法
// intercept: 拦截器, 和上面的筛选器一起使用, 表示返回一个固定值"Hack of Object"
// make: 生成一个新类
DynamicType.Unloaded unloadedType = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.isToString())
.intercept(FixedValue.value("Hack of Object"))
.make();
// 上述方法创建了一个新的类, 但是没有加载到JVM
// 加载类:
Class<?> dynamicType = unloadedType.load(getClass().getClassLoader()).getLoaded();
// 测试, 实例化这个类, 看看它的toString方法是否被修改了
assertEquals(dynamicType.newInstance().toString(), "Hack of Object");
}

2 | 方法代理

我们通过两个类来展示方法代理的步骤:

  • Foo.java
1
2
3
4
5
public class Foo {
public String sayHello() {
return "hello in foo!";
}
}
  • Bar.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Bar {

public String sayHello() {
return "The delegate will invoke this";
}

public static String sayHelloBar() {
return "Hello this is bar";
}

public static Integer oneReturn() {
return 1;
}
}

静态方法代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testByteBuddyWithCustomFunctionStatic() throws Exception {
// 我们创建一个Foo和一个Bar类
// 目的: 将所有的sayHelloFoo()代理到sayHelloBar()上
// 代理将根据 方法签名 < 返回值类型 < 方法名 < 注解顺序来匹配方法
// 调用Bar的静态方法

String r = new ByteBuddy()
.subclass(Foo.class)
.method(ElementMatchers.nameContains("Hello"))
.intercept(MethodDelegation.to(Bar.class))
.make()
.load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded()
.newInstance()
.sayHello();

assertEquals(r, "Hello this is bar");
}

实例方法代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testByteBuddyWithCustomFunctionInstance() throws Exception {
// 同上面的案例, 但是代理到实例化方法而不是静态方法
String r = new ByteBuddy()
.subclass(Foo.class)
.method(ElementMatchers.nameContains("Hello"))
.intercept(MethodDelegation.to(new Bar()))
.make()
.load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded()
.newInstance()
.sayHello();

assertEquals(r, "The delegate will invoke this");
}

添加新方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testByteBuddAddAttribute() throws Exception {
// 创建动态类同时添加新方法
// defineMethod: (方法名叫custom, 返回Integer, 可见性是public)
// 同时代理此方法到Bar
// 定义同时一个叫x的成员变量
Class<?> type = new ByteBuddy()
.subclass(Object.class)
.name("NewFoo")
.defineMethod("custom", Integer.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.oneReturn());
assertNotNull(type.getDeclaredField("x"));
}

关于@RuntimeType注解

表示ByteBuddy会将返回值动态改变为我们intercept方法的返回值类型

比如对于上面这个Bar.java

如果我们对其加@Runtimetype:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Bar {
// RuntimeType的作用: By declaring the @RuntimeType annotation on the method, Byte Buddy finally casts the returned value to the return value of the intercepted method if this is necessary.
// In doing so, Byte Buddy also applies automatic boxing and unboxing.
// 自动装箱和自动拆箱: 自动装箱调用valueOf将原始类型转换为对象, 拆箱调用intValue, doubleValue等将对象转换为原始类型

@RuntimeType
public String sayHello() {
return "The delegate will invoke this";
}

@RuntimeType
public static String sayHelloBar() {
return "Hello this is bar";
}

@RuntimeType
public static Integer oneReturn() {
return 1;
}
}

对于这个方法上面<添加新方法>的案例, 则会报错:

1
java.lang.IllegalArgumentException: Cannot resolve ambiguous delegation of public java.lang.Integer NewFoo.custom() to net.bytebuddy.implementation.bind.MethodDelegationBinder$MethodBinding$Builder$Build@f535e794 or net.bytebuddy.implementation.bind.MethodDelegationBinder$MethodBinding$Builder$Build@c333a93f

表示代理冲突, 因为加入了@RuntimeType的返回值已经不是由方法本身决定的了

由于我们intercept的是:

1
2
3
// ..
.defineMethod("custom", Integer.class, Modifier.PUBLIC)
// ..

所以所有的代理方法都会默认返回值为Integer, 并且ByteBuddy会尝试用自动拆装箱来实现这一点

这时需要将上述测试改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  @Test
public void testByteBuddAddAttribute() throws Exception {
// 创建动态类同时添加新方法
// defineMethod: (方法名叫custom, 返回Integer, 可见性是public)
// 同时代理此方法到Bar
// 定义同时一个叫x的成员变量
Class<?> type = new ByteBuddy()
.subclass(Object.class)
.name("NewFoo")
.defineMethod("custom", Integer.class, Modifier.PUBLIC)
// .intercept(MethodDelegation.to(Bar.class))
.intercept(MethodDelegation.withEmptyConfiguration().filter(ElementMatchers.named("oneReturn")).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.oneReturn());
assertNotNull(type.getDeclaredField("x"));

即选中一个指定的方法, 才能保证代理方法的选择不产生冲突

附录

ByteBuddy入门教程

ByteBuddy教程(官方文档中文版)

A Guide to Byte Buddy


ByteBuddy -- 简单介绍和入门
https://blog.roccoshi.top/posts/60007/
作者
RoccoShi
发布于
2022年8月25日
许可协议