写点什么

Presto 设计与实现(四):动态代码生成 ByteBuddy

作者:冰心的小屋
  • 2023-08-19
    北京
  • 本文字数:5952 字

    阅读完需:约 20 分钟

Presto 设计与实现(四):动态代码生成 ByteBuddy

1. 原理

动态代码生成就是运行时通过编码的方式定义类的限定名、属性和方法等,并将其转化为可以被 ClassLoader 直接加载的 Java 字节码。


下面是从类的声明到使用的过程:

  1. 在 IDE 中创建 Java 类,声明类的构造函数、属性或方法等,Java 类以 .java 文件的形式存储;

  2. 通过 maven 命令或其他打包工具进行编译打包,.java 文件被编译成 .class 文件,.class 文件又打包成可执行的 jar 包或 war 包;

  3. 通过 Java 相关命令执行 jar 包或部署在 web 容器上运行,.class 文件转化为字节数组,被 ClassLoader 加载;

  4. 在实际使用时,由于类已经被 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("./")); }}
复制代码


动态创建类的声明,之后将类的定义输出到文件中,下面是输出结果:


在此过程中有两个问题没有解决:

  1. 定义属性 name 后通过 value 方法设置初始值没有效果;

  2. 定义 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。


发布于: 刚刚阅读数: 3
用户头像

分享技术上的点滴收获! 2013-08-06 加入

一杯咖啡,一首老歌,一段代码,欢迎做客冰屋,享受编码和技术带来的快乐!

评论

发布
暂无评论
Presto 设计与实现(四):动态代码生成 ByteBuddy_数据湖_冰心的小屋_InfoQ写作社区