ByteBuddy

1
2
3
4
5
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.11.12</version>
</dependency>

Hello World

1
2
3
4
5
6
7
8
9
10
11
Class<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World"))
.make()
.load(HelloWorldBuddy.class.getClassLoader())
.getLoaded();
Object instance = dynamicType.newInstance();
String toString = instance.toString();
System.out.println(toString);
System.out.println(instance.getClass().getCanonicalName());

image

从例子可以看出,很简单就创建了一个动态类型。ByteBuddy提供了一套流式API,从ByteBuddy实例出发,可以流畅的完成所有的操作和数据定义。
上面的示例中

  • subclass 指定了新创建的类的父类
  • method 指定了 Object 的 toString 方法
  • intercept 拦截了 toString 方法并返回固定的 value
  • 最后 make 方法生产字节码,有类加载器加载到虚拟机中

此外,Byte Buddy不仅限于创建子类和操作类,还可以转换现有代码。Byte Buddy 还提供了一个方便的 API,用于定义所谓的 Java 代理,该代理允许在任何 Java 应用程序的运行期间进行代码转换

创建动态类

1
2
3
4
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.name("com.zlk.learning.bytebuddy.DynamicType")
.make();

上面的示例代码会创建一个继承至 Object 类型的类。这个动态创建的类型与直接扩展 Object 并且没有实现任何方法、属性和构造函数的类型是等价的,如下:

1
2
3
public class DynamicTYpe {

}

在创建类的时候,还提供了更多API来支持对类的定义,包括定义字段、方法等

1
2
3
4
5
6
7
8
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.name("com.zlk.learning.bytebuddy.DynamicType")
.defineField("name", String.class, 1)
.defineField("age", Integer.class, 1)
.method(ElementMatchers.named("toString"))
.intercept(FixedValue.value("Hello World!"))
.make();

image

上面的示例代码中,我们增加了两个字段name和age,同时拦截了toString方法,使其输出固定值 “Hello World!”。

保留父类实现的接口信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37


@Override
public Object instantiate(BeanDefinition beanDefinition, String beanName, Constructor ctor, Object[] args) throws BeansException {
Class<?> beanClass = beanDefinition.getBeanClass();
ArrayList<TypeDescription.Generic> list = getGenerics(beanClass);
Class clazz = new ByteBuddy().subclass(beanClass).implement(list)
.make()
.load(getClass().getClassLoader())
.getLoaded();
try {
if (null == ctor) return clazz.getDeclaredConstructor().newInstance();
return clazz.getDeclaredConstructor(ctor.getParameterTypes()).newInstance(args);
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new BeansException("Failed to instantiate [" + clazz.getName() + "]", e);
}
}
//获取父类的接口信息
private static ArrayList<TypeDescription.Generic> getGenerics(Class<?> beanClass) {
ArrayList<TypeDescription.Generic> list = new ArrayList<>();
try {
Type[] genericInterfaces = beanClass.getGenericInterfaces();
for (Type type : genericInterfaces) {
Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
Class<?>[] classes = new Class[typeArguments.length];
for (int i = 0; i < typeArguments.length; i++) {
classes[i] = Class.forName(typeArguments[i].getTypeName());
}
Class<?> aClass = Class.forName(((ParameterizedType) type).getRawType().getTypeName());
TypeDescription.Generic listType = TypeDescription.Generic.Builder.parameterizedType(aClass, classes).build();
list.add(listType);
}
} catch (ClassNotFoundException e) {
throw new BeansException("Failed to instantiate [" + beanClass.getName() + "]", e);
}
return list;
}

加载类

上节创建的 DynamicType.Unloaded,代表一个尚未加载的类,顾名思义,这些类型不会加载到 Java 虚拟机中,它仅仅表示创建好了类的字节码,通过 DynamicType.Unloaded 中的 getBytes 方法你可以获取到该字节码。

在应用程序中,可能需要将该字节码保存到文件,或者注入的现在的 jar 文件中,因此该类型还提供了一个 saveIn(File) 方法,可以将类存储在给定的文件夹中; inject(File) 方法将类注入到现有的 Jar 文件中,另外你只需要将该字节码直接加载到虚拟机使用,你可以通过 ClassLoadingStrategy 来加载。

如果不指定ClassLoadingStrategy,Byte Buffer根据你提供的ClassLoader来推导出一个策略,内置的策略定义在枚举ClassLoadingStrategy.Default中

  • WRAPPER:创建一个新的Wrapping类加载器
  • CHILD_FIRST:类似上面,但是子加载器优先负责加载目标类
  • INJECTION:利用反射机制注入动态类型
1
2
3
Class<?> dynamicClass = dynamicType
.load(Object.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();

我们使用 WRAPPER 策略来加载适合大多数情况的类,这样生产的动态类不会被ApplicationClassLoader加载到,不会影响到项目中已经存在的类
getLoaded 方法返回一个 Java Class 的实例,它就表示现在加载的动态类

拦截方法

在之前的例子中,我们拦截了toString方法,并使其输出固定值。不过在实际开发中很少会遇到如此简单的场景,我们可以通过指定拦截方法的形式来处理复杂的逻辑

通过匹配模式拦截

ByteBuddy 通过 net.bytebuddy.matcher.ElementMatcher 来定义配置策略,可以通过此接口实现自己定义的匹配策略

1
2
3
4
5
6
7
8
9
10
11
12
Foo dynamicFoo = new ByteBuddy()
.subclass(Foo.class)
// 匹配由Foo.class声明的方法
.method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("One!"))
// 匹配名为foo的方法
.method(named("foo")).intercept(FixedValue.value("Two!"))
// 匹配名为foo,入参数量为1的方法
.method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!"))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance();

方法委托

使用MethodDelegation可以将方法调用委托给任意POJO。Byte Buddy不要求Source(被委托类)、Target类的方法名一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Source {
public String hello(String name) { return null; }
}

class Target {

public static String hello(String name) {
return "Hello " + name + "!";
}
}

String helloWorld = new ByteBuddy()
.subclass(Source.class)
.method(named("hello"))
// 此处委托 类只能委托静态方法 对象可使用非静态方法
.intercept(MethodDelegation.to(Target.class))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance()
.hello("World");

其中 Target 还可以如下实现:

1
2
3
4
5
class Target {
public static String intercept(String name) { return "Hello " + name + "!"; }
public static String intercept(int i) { return Integer.toString(i); }
public static String intercept(Object o) { return o.toString(); }
}

前一个实现因为只有一个方法,而且类型也匹配,很好理解,那么后一个呢,Byte Buddy到底会委托给哪个方法?Byte Buddy遵循一个最接近原则:

  • intercept(int)因为参数类型不匹配,直接Pass
  • 另外两个方法参数都匹配,但是 intercept(String)类型更加接近,因此会委托给它

同时需要注意的是被拦截的方法需要声明为 public,否则没法进行拦截增强。除此之外,还可以使用 @RuntimeType 注解来标注方法

1
2
3
4
5
6
7
8
9
@RuntimeType
public Object interceptor(@This Object proxy, @Origin Method method,
@SuperMethod Method superMethod,
@AllArguments Object[] args) throws Exception {
System.out.println("bytebuddy delegate proxy2 before sing ");
Object ret = superMethod.invoke(proxy, args);
System.out.println("bytebuddy delegate proxy2 after sing ");
return ret;
}

参数绑定

可以在拦截器(Target)的拦截方法 intercept 中使用注解注入参数,ByteBuddy 会根据注解给我们注入对于的参数值。比如

1
2
3
void intercept(Object o1, Object o2)
// 等同于
void intercept(@Argument(0) Object o1, @Argument(1) Object o2)

常用注解有以下这些:

  • @Argument 绑定单个参数
  • @AllArguments 绑定所有参数的数组
  • @This 当前被拦截的、动态生成的那个对象
  • @DefaultCall 调用默认方法而非super的方法
  • @SuperCall 用于调用父类版本的方法
  • @Origin 被拦截的源方法
  • @RuntimeType 可以用在返回值、参数上,提示ByteBuddy禁用严格的类型检查
  • @Super 当前被拦截的、动态生成的那个对象的父类对象
  • @FieldValue 注入被拦截对象的一个字段的值

Agent

Java 从 1.5 开始提供了 java.lang.instrument包,该包为检测 Java 程序提供 API,比如用于监控、收集性能信息、诊断问题。通过 java.lang.instrument 实现工具被称为 Java Agent。Java Agent 可以修改类文件的字节码,通常是,在字节码方法插入额外的字节码来完成检测

和通过ByteBuddy实例创建动态类型一样,bytebuddy也提供了AgentBuilder类使我们在agent中更优雅地编写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ToStringAgent {
public static void premain(String arguments, Instrumentation instrumentation) {
new AgentBuilder.Default()
.type(isAnnotatedWith(ToString.class))
.transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder transform(DynamicType.Builder builder,
TypeDescription typeDescription,
ClassLoader classloader) {
return builder.method(named("toString"))
.intercept(FixedValue.value("transformed"));
}
}).installOn(instrumentation);
}
}
  • type 通过ElementMatcher 来匹配我们加载的class,匹配到之后,将会使用
  • transform 指定的转换器来对匹配到的class进行操作

ElementMatcher

ElementMatcher可以定义匹配class的规则,在bytebuddy中,ElementMatchers类提供了许多常规的匹配方式,可以按照class name、注解、类型等来进行匹配,上面的实例中就是使用注解匹配的方式

Junction继承自ElementMatcher接口,定义了and 和 or 方法,可以使我们在定义Matcher时通过链式定义一连串的匹配规则

1
2
3
4
5
6
7
8
9
10
new AgentBuilder.Default()
.type(ElementMatchers.isAnnotatedWith(ToString.class)).and(ElementMatchers.isSubTypeOf(DynamicClass.class)).or(ElementMatchers.named("DynamicClass"))
.transform(new AgentBuilder.Transformer() {
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
return builder
.method(ElementMatchers.named("hello"))
.intercept(MethodDelegation.to(MyServiceInterceptor.class))
;
}
}).installOn(instrumentation);

Transformer

Transformer 接口定义了 transform方法,会传入DynamicType.Builder实例,通过该builder,就可以对匹配到的类进行操作,就和上面讲的 ByteBuddy创建动态类型时类似操作,可以定义字段以及对方法进行拦截操作等,上面的例子就是对匹配到的类的hello方法进行了方法委托,在调用hello方法时,将会委托给 MyServiceInterceptor类

1
2
3
4
5
6
7
8
9
10
public class MyServiceInterceptor {

@RuntimeType
public static Object intercept(@Origin Method method, @SuperCall Callable<?> callable) throws Exception {
System.out.println("intercept:拦截了" + method.getName());
return callable.call();
}

}

END

参考文章
bytebuddy官方文档 https://bytebuddy.net/#/tutorial

https://juejin.cn/post/6844903965553852423#heading-12​​