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:
package com.ice;
public class ByteBuddyTest {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
复制代码
使用 ByteBuddy 如何创建呢?
package com.ice;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FieldAccessor;
import java.io.File;
import java.lang.reflect.Modifier;
class ByteBuddy001 {
public static void main(String[] args) throws Exception {
// 类的定义:通过创建 ByteBuddy 的实例流式风格的进行定义
DynamicType.Unloaded dynamicType = new ByteBuddy(ClassFileVersion.JAVA_V8)
// 1. 该类继承 Object 类
.subclass(Object.class)
// 2. 类的限定名称
.name("com.ice.ByteBuddyTest")
// 3. 定义属性 name
.defineField("name", String.class, Modifier.PRIVATE)
// 4. 定义 getName 方法
.defineMethod("getName", String.class, Visibility.PUBLIC)
// 5. 定义方法返回值
.intercept(FieldAccessor.ofField("name"))
// 6. 定义 setName 方法
.defineMethod("setName", void.class, Visibility.PUBLIC)
// 7. 该方法只有一个参数
.withParameters(String.class)
// 8. 方法里面的实现
.intercept(FieldAccessor.ofField("name").setsArgumentAt(0))
.make();
dynamicType.saveIn(new File("./"));
}
}
复制代码
动态创建类的声明,之后将类的定义输出到文件中,下面是输出结果:
在此过程中有两个问题没有解决:
定义属性 name 后通过 value 方法设置初始值没有效果;
定义 setName 方法的参数名,想将 var1 设置为 name。
大家有思路的,欢迎留言指点。
类定义完毕后,就可以加载使用了:
class ByteBuddy002 {
public static void main(String[] args) throws Exception {
// 参考 ByteBuddy001 中的例子
DynamicType.Unloaded dynamicType = new ByteBuddy(ClassFileVersion.JAVA_V8)
...
// 使用当前类的 ClassLoader 加载
Object instance = dynamicType.load(ByteBuddy002.class.getClassLoader())
.getLoaded()
.newInstance();
// 通过反射设置 name 名称
Method method = instance.getClass().getMethod("setName", String.class);
method.invoke(instance, "Hello ByteBuddy");
System.out.println(instance);
}
}
复制代码
3. AOP
在调用 ByteBuddyTest -> toString 方法的前后,记录日志:
class ByteBuddyTest {
@Override
public String toString() {
return "Hello ByteBuddy";
}
}
复制代码
步骤 1:声明一个静态方法,入参加上 @SuperCall 注解,使用 Callable 作为类型,里面的范型 T 就是方法实际的返回值。
class ByteBuddy003 {
public static String log(@SuperCall Callable<String> callable) throws Exception {
System.out.println("invoking");
String result = callable.call();
System.out.println("Invoked: " + result);
return result;
}
}
复制代码
步骤 2:创建 ByteBuddy 实例匹配方法,创建 AOP 处理逻辑
public static void main(String[] args) throws Exception {
ByteBuddyTest byteBuddyTest = new ByteBuddy()
.subclass(ByteBuddyTest.class)
// 监控 toString 方法
.method(ElementMatchers.named("toString"))
// AOP 中的处理
.intercept(MethodDelegation.to(ByteBuddy003.class))
.make()
.load(ByteBuddy003.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION)
.getLoaded()
.newInstance();
byteBuddyTest.toString();
}
复制代码
输出结果:
4. 重新加载类
对于一个已经加载的类,依然可以重新加载该类,改变类的既有实现。
class Man{
@Override
public String toString() {
return "A man";
}
}
class Woman{
@Override
public String toString() {
return "A woman";
}
}
class ByteBuddy004{
public static void main(String[] args) {
/**
* 需要引入 ByteBuddyAgent
* <dependency>
* <groupId>net.bytebuddy</groupId>
* <artifactId>byte-buddy-agent</artifactId>
* <version>1.10.22</version>
* </dependency>
*/
ByteBuddyAgent.install();
// 1. 该类已经加载
Man man = new Man();
System.out.println("I am a " + man + ", hashCode=" + man.hashCode());
// 2. 该类重新定义,重新加载
new ByteBuddy()
.redefine(Woman.class)
.name(Man.class.getName())
.make()
.load(Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
System.out.println("I am a " + man + ", hashCode=" + man.hashCode());
}
}
复制代码
执行后的结果:
想想这功能也太 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 项目。
package com.ice;
import net.bytebuddy.asm.Advice;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public class BytebuddyAOP {
private static final String DATE_FORMAT = "yyyy-MM-dd: HH:mm:ss.SSS";
// 方法开始调用
@Advice.OnMethodEnter
public static long start(@Advice.Origin String clsMethod) {
long now = System.currentTimeMillis();
DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
System.out.printf("[%s] Invoking %s \n", dateFormat.format(now), clsMethod);
return now;
}
// 方法结束调用
// @Advice.Origin 用来标识事件源
// @Advice.Enter 用来接收 @Advice.OnMethodEnter 方法返回的结果
@Advice.OnMethodExit
public static void stop(@Advice.Origin String clsMethod, @Advice.Enter long start) {
long now = System.currentTimeMillis();
DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
System.out.printf("[%s] Invoked %s cost %s \n",
dateFormat.format(now),
clsMethod,
now - start);
}
}
复制代码
5.2 编写 Agent:匹配类、方法以及和拦截器的绑定
package com.ice;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.instrument.Instrumentation;
public class BytebuddyAgent {
public static void premain(String arguments, Instrumentation instrumentation) {
new AgentBuilder.Default()
.with(new AgentBuilder.InitializationStrategy.SelfInjection.Eager())
.type((ElementMatchers.any()))
.transform((builder, typeDescription, classLoader, module) ->
builder.constructor(ElementMatchers.any())
.intercept(Advice.to(BytebuddyAOP.class))
.method(ElementMatchers.any())
.intercept(Advice.to(BytebuddyAOP.class))
).installOn(instrumentation);
}
}
复制代码
5.3 构建架包
pom.xml 文件中添加打包插件:
<dependencies>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.10.22</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.10.22</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Premain-Class>com.ice.BytebuddyAgent</Premain-Class>
</manifestEntries>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
复制代码
将所有依赖打成 1 个架包:
mvn clean package shade:shade
复制代码
5.4 应用
下面这个类用于测试,验证 Agent 是否可以工作, 类的构造函数和方法实际执行时会停留短暂时间:
package com.ice;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class BytebuddyAgentTest {
public static void main(String[] args) {
int index = 0;
while (true) {
Hello hello = new Hello("[Client]Hello: " + UUID.randomUUID() + "_" + index++);
hello.say();
}
}
public static class Hello {
private String message;
public Hello(String message){
this.message = message;
pause();
}
public void say() {
pause();
System.out.println(message);
}
private void pause(){
try{
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
}catch (Exception e){
}
}
}
}
复制代码
测试时添加 JVM 参数: -javaagent:BytebuddyAgent.jar
:
开始执行,Agent 已经开始工作了:
如果想更全面的了解 ByteBuddy 可参考官网:https://bytebuddy.net/#/tutorial-cn
6. ByteBuddy 在 Presto 中的使用
ByteBuddy 是构建在 ASM 之上的一款强大的 Java 字节码操纵工具,许多开源项目对其都有依赖,例如 Presto、Spark、Flink 和 Cassandra 等。
在 Presto 中 SQL 语句经过词法分析、语法分析会转变成一棵 AST 树,之后使用递归的方式结合访问者模式遍历树上的所有节点,访问者在访问的过程中会涉及到动态代码的生成,底层使用的就是 airlift.bytecode。
评论