1. 原理
动态代码生成就是运行时通过编码的方式定义类的限定名、属性和方法等,并将其转化为可以被 ClassLoader 直接加载的 Java 字节码。
下面是从类的声明到使用的过程:
在 IDE 中创建 Java 类,声明类的构造函数、属性或方法等,Java 类以 .java 文件的形式存储;
通过 maven 命令或其他打包工具进行编译打包,.java 文件被编译成 .class 文件,.class 文件又打包成可执行的 jar 包或 war 包;
通过 Java 相关命令执行 jar 包或部署在 web 容器上运行,.class 文件转化为字节数组,被 ClassLoader 加载;
在实际使用时,由于类已经被 ClassLoader 加载,我们就可以创建类对象,使用类的相关方法实现某些业务功能。
而如果使用 ByteBuddy ,ByteBuddy 可以直接生成字节数组替换 .class 转化的所有过程。
2. 类的定义和加载
下面是我期望创建的类,该类只有一个属性 name,有 getName 和 setName 方法,重写 toString 返回 name:
复制代码
使用 ByteBuddy 如何创建呢?
复制代码
动态创建类的声明,之后将类的定义输出到文件中,下面是输出结果:
在此过程中有两个问题没有解决:
定义属性 name 后通过 value 方法设置初始值没有效果;
定义 setName 方法的参数名,想将 var1 设置为 name。
大家有思路的,欢迎留言指点。
类定义完毕后,就可以加载使用了:
复制代码
3. AOP
在调用 ByteBuddyTest -> toString 方法的前后,记录日志:
复制代码
步骤 1:声明一个静态方法,入参加上 @SuperCall 注解,使用 Callable 作为类型,里面的范型 T 就是方法实际的返回值。
复制代码
步骤 2:创建 ByteBuddy 实例匹配方法,创建 AOP 处理逻辑
复制代码
输出结果:
4. 重新加载类
对于一个已经加载的类,依然可以重新加载该类,改变类的既有实现。
复制代码
执行后的结果:
想想这功能也太 Bug 了,感觉有点反面向对象设计,除非特定场景,建议还是不要使用的好。
5. Java Agent 代理
ByteBuddy 还有个更有价值的用途:用于 Java Agent 的实现,通过 ByteBuddy 你可以全方位的监控拦截对象的创建和方法的调用。
你需要使用 ElementMatchers 进行匹配:
.type((ElementMatchers.any())):匹配所有类;
.type((ElementMatchers.nameContains("say"))):匹配包含特定方法的类;
.method(ElementMatchers.any()):匹配所有方法;
.method(ElementMatchers.nameContains("say")):匹配特定方法。
5.1 编写拦截器:方法调用前后的处理逻辑
开始前,你需要单独创建 Java Agent 项目。
复制代码
5.2 编写 Agent:匹配类、方法以及和拦截器的绑定
复制代码
5.3 构建架包
pom.xml 文件中添加打包插件:
复制代码
将所有依赖打成 1 个架包:
复制代码
5.4 应用
下面这个类用于测试,验证 Agent 是否可以工作, 类的构造函数和方法实际执行时会停留短暂时间:
复制代码
测试时添加 JVM 参数: :
开始执行,Agent 已经开始工作了:
6. ByteBuddy 在 Presto 中的使用
ByteBuddy 是构建在 ASM 之上的一款强大的 Java 字节码操纵工具,许多开源项目对其都有依赖,例如 Presto、Spark、Flink 和 Cassandra 等。
在 Presto 中 SQL 语句经过词法分析、语法分析会转变成一棵 AST 树,之后使用递归的方式结合访问者模式遍历树上的所有节点,访问者在访问的过程中会涉及到动态代码的生成,底层使用的就是 airlift.bytecode。
领取专属 10元无门槛券
私享最新 技术干货