例子
目标
我们有一个类 EchoA, 它有一个方法 echo
返回值是一个字符串 "A"
:
1 | public class EchoA { |
我们想要通过 ByteBuddy 来动态生成一个类, 它的 echo 方法返回字符串 "B"
.
ByteBuddy 实现
我们可以这么写:
1 | public static void main(String[] args) throws Exception { |
上面这个 main 方法动态生成了一个类, 并调用这个类的 echo
方法,返回了字符串 "B"
.
原理
字节码视角: 改变了什么
ByteBuddy 可以生成字节码,但是不会生成 Java 源码,我们就从字节码层面看看, ByteBuddy 做了什么.
先看下原始的 EchoA 字节码(部分):
echo 方法的字节码如下:
1 | 0 ldc #8 <B> |
ldc 指令会将常量池中的常量入栈, 这里是将常量池第八个常量(也就是"A"
) 入栈. areturn 指令将栈中数据作为返回值返回.
生成的新类, 也是类似的逻辑:
源码视角: 怎么实现的
ByteBuddy 是一个对 ASM 字节码框架的封装. ByteBuddy 对字节码操作做了多个层次的封装, 用户可以很方便的使用它, 但要理解它的运行原理, 有比较陡峭的学习曲线. 下面我们管中窥豹:
1 | DynamicType.Unloaded<EchoA> newClass = new ByteBuddy() |
我们着重分析这段代码:subclass
,method
,intercept
这三个方法都是对 ByteBuddy 对象进行设置,没有进行具体字节码操作.
由于 BtyeBuddy 框架中用了很多不可变类, 这三个方法的返回值都是一个新的 ByteBuddy 实例.
最后的 make
方法将会进行字节码增强,产生新的类的字节码.我们具体看下 make
方法内部.
net.bytebuddy.dynamic.DynamicType.Builder.AbstractBase.UsingTypeWriter#make(net.bytebuddy.dynamic.TypeResolutionStrategy):
1 | public DynamicType.Unloaded<U> make(TypeResolutionStrategy typeResolutionStrategy, TypePool typePool) { |
make
方法内部使用会构造一个 TypeWriter 去做具体的 make
操作.
构造 TypeWriter 的过程参考下面代码的注释
1 | protected TypeWriter<T> toTypeWriter(TypePool typePool) { |
构造完成 TypeWriter 之后, 通过 make 方法最终构造出新的类的字节码, make 方法调用net.bytebuddy.dynamic.scaffold.TypeWriter.Default.ForCreation#create
create 方法, create 方法调用 ASM 框架的方法, 将上述准备好的 ByteCodeAppender 应用到被增强的类上.
附录
ByteBuddy 官方教程: 如果想要快速上手 ByteBuddy, 看这个官方教程最直接.
The Java® Virtual Machine Specification Java SE 11 Edition 想了解字节码可以看这个文档