(目录)


ByteBuddy

介绍

Byte Buddy 是一个代码生成和操作库,用于在 Java 应用程序运行时创建和修改 Java 类,而无需编译器的帮助。

除了 Java 类库附带的代码生成实用程序外,Byte Buddy 还允许创建任意类,并且不限于实现用于创建运行时代理的接口。此外,Byte Buddy 提供了一种方便的 API,可以使用 Java 代理或在构建过程中手动更改类。

官网:https://bytebuddy.net/#/tutorial-cn

ByteBuddy应用场景

运行时生成代码

Java 语言带有相对严格的类型系统。Java 要求所有变量和对象都属于特定类型,任何分配不兼容类型的尝试都会导致错误发生。 当非法转换类型时, 这些错误通常由 Java 编译器或至少由 Java运行时产生。

但是,通过强制执行其严格的类型系统,Java 强加了该语言在其他领域中范围的限制。

例如,在编写供其他 Java 应用程序使用的通用库时, 我们通常无法引用用户应用程序中定义的任何类型,因为在编译我们的库时,这些类型对我们来说是未知的

不过为了解决这个问题,java提供了一套反射的api来帮助使用者感知和修改类的内部。

不幸的是,使用反射 API 有两个明显的缺点

  1. 反射显而易见的缺点是慢。我们在使用反射之前都需要谨慎的考虑他对于当前性能的影响,唯有进过详细的评估,才能够放心的使用。
  2. 反射能够绕过类型安全检查。我们在使用反射的时候需要确保相应的接口不会暴露给外部用户,不然可能造成不小的安全隐患。

ByteBuddy可以帮助我们做到反射能做的事情,而不必受困于他的这些缺点


java编程语言代码生成库不止 Byte Buddy 一个,以下代码生成库在 Java 中也很流行:

  • Java Proxy

  • CGLIB

  • Javassist

  • Byte Buddy

推荐使用ByteBuddy,因为Byte Buddy代码生成可的性能最高,ByteBuddy 的主要侧重点在于生成更快速的代码,如下图:

image-20231114162048725


ByteBuddy语法

任何一个由 ByteBuddy 创建/增强的类型都是通过 ByteBuddy 类的实例来完成的,如下代码:

DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
        // 生成 Object的子类
        .subclass(Object.class)
        // 生成类的名称为"com.xxx.Type"
        .name("com.xxx.Type")
        .make();

动态增强代码三种方式 (subclass、rebasing、redefinition)

ByteBuddy 动态增强代码总共有三种方式:

  1. subclass: 对应 ByteBuddy.subclass() 方法。这种方式比较好理解,就是为目标类(即被增强的类)生成一个子类,在子类方法中插入动态代码

  2. rebasing: 对应 ByteBuddy.rebasing() 方法。当使用 rebasing 方式增强一个类时,Byte Buddy 保存目标类中所有方法的实现,也就是说,当 Byte Buddy 遇到冲突的字段或方法时,会将原来的字段或方法实现复制到具有兼容签名的重新命名的私有方法中,而不会抛弃这些字段和方法实现。从而达到不丢失实现的目的。这些重命名的方法可以继续通过重命名后的名称进行调用

  3. redefinition: 对应 ByteBuddy.redefine() 方法。当重定义一个类时,Byte Buddy 可以对一个已有的类添加属性和方法,或者删除已经存在的方法实现。如果使用其他的方法实现替换已经存在的方法实现,则原来存在的方法实现就会消失。

通过上述三种方式完成类的增强之后,我们得到的是 DynamicType.Unloaded 对象,表示的是一个未加载的类型。

类加载策略(WRAPPER、CHILD_FIRST、INJECTION )

通过上述三种方式完成类的增强之后,我们得到的是 DynamicType.Unloaded 对象,表示的是一个未加载的类型,我们可以使用 ClassLoadingStrategy加载此类型。Byte Buddy 提供了几种类加载策略,这些加载策略定义在 ClassLoadingStrategy.Default 中,其中:

  • WRAPPER 策略:创建一个新的 ClassLoader 来加载动态生成的类型。
  • CHILD_FIRST 策略:创建一个子类优先加载的 ClassLoader,即打破了双亲委派模型。
  • INJECTION 策略:使用反射将动态生成的类型直接注入到当前 ClassLoader 中。

官网HelloWorld例子 字节码生成分析

用于输出 HelloWorld

// 创建ByteBuddy对象
String str = new ByteBuddy()
        // subclass增强方式
        .subclass(Object.class)
        // 新类型的类名
        .name("HelloWorld") 
        // 拦截其中的toString()方法
        .method(ElementMatchers.named("toString"))
        // 让toString()方法返回固定值
        .intercept(FixedValue.value("Hello World!"))
        .make()
        // 加载新类型,默认WRAPPER策略
        .load(ByteBuddy.class.getClassLoader())
        .getLoaded()
        // 通过 Java反射创建 HelloWorld实例
        .newInstance()
        // 调用 toString()方法
        .toString(); 

System.out.println(helloWorld);  // Hello World!

他的运行结果就是一行,Hello World!

整个代码块核心功能就是通过 method(named("toString")),找到 toString 方法,再通过拦截 intercept,设定此方法的返回值。FixedValue.value("Hello World!")。到这里其实一个基本的方法就通过 Byte-buddy ,改造完成。

关注这里的 method() 方法:

method() 方法可以通过传入的 ElementMatchers 参数匹配多个需要修改的方法,这里的ElementMatchers.named("toString")即为按照方法名匹配 toString() 方法。

如果同时存在多个重载方法,则可以使用 ElementMatchers 其他 API 描述方法的签名,如下所示:

// 指定方法名称
ElementMatchers.named("toString")
    // 指定方法的返回值
    .and(ElementMatchers.returns(String.class))
    // 指定方法参数
    .and(ElementMatchers.takesArguments(0));

接下来需要关注的是 intercept() 方法,通过 method()方法拦截到的所有方法会由 Intercept() 方法指定的 Implementation 对象决定如何增强。这里的 FixValue.value() 会将方法的实现修改为固定值,上例中就是固定返回 “Hello World!” 字符串。

Byte Buddy 中可以设置多个 method()Intercept() 方法进行拦截和修改,Byte Buddy 会按照栈的顺序来进行拦截。


接下来的这一段主要是用于加载生成后的 Class 和执行,以及调用方法 toString()

也就是最终我们输出了想要的结果,通过字节码输出到文件,看下具体被改造后的样子,如下:

public class HelloWorld {
    public String toString() {
        return "Hello World!";
    }

    public HelloWorld() {
    }
}

在官网来看,这是一个非常简单并且能体现 Byte buddy 的例子。但是与我们平时想创建出来的 main 方法相比,还是有些差异。


实践 ByteCode

创建一个项目agent-demo,添加ByteBuddy依赖

<dependencies>
    <dependency>
        <groupId>net.bytebuddy</groupId>
        <artifactId>byte-buddy</artifactId>
        <version>1.9.2</version>
    </dependency>
    <dependency>
        <groupId>net.bytebuddy</groupId>
        <artifactId>byte-buddy-agent</artifactId>
        <version>1.9.2</version>
    </dependency>
</dependencies>

创建测试类及方法

我们先创建一个普通类,再为该类创建代理类,创建代理对方法进行拦截做处理。

public class UserService {

    //方法1
    public String username(){
        System.out.println("username().....");
        return "张三";
    }

    //方法2
    public String address(String username){
        System.out.println("address(String username).....");
        return username+"来自 【xxxx】";
    }

    //方法3
    public String address(String username,String city){
        System.out.println("address(String username,String city).....");
        return username+"来自 【北京"+city+"】";
    }
}

编写拦截器

2)创建拦截器LogInterceptor,编写拦截器方法:


public class LogInterceptor {

    @RuntimeType //将返回值转换成具体的方法返回值类型,加了这个注解 intercept 方法才会被执行
    public  Object intercept(
            // 被拦截的目标对象 (动态生成的目标对象)
            @This  Object target,
            // 正在执行的方法Method 对象(目标对象父类的Method)
            @Origin Method method,
            // 正在执行的方法的全部参数
            @AllArguments Object[] argumengts,
            // 目标对象的一个代理
            @Super  Object delegate,
            // 方法的调用者对象 对原始方法的调用依靠它
            @SuperCall Callable<?> callable) throws Exception {
        //目标方法执行前执行日志记录
        System.out.println("准备执行Method="+method.getName());
        // 调用目标方法
        Object result = callable.call();
        //目标方法执行后执行日志记录
        System.out.println("方法执行完成Method="+method.getName());
        return result;
    }

}

在程序中我们 用到ByteBuddyMethodDelegation对象,它可以将拦截的目标方法委托给其他对象处理,这里有几个注解我们先进行说明:

  • @RuntimeType:不进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法。
  • @This:注入被拦截的目标对象(动态生成的目标对象)。
  • @Origin:注入正在执行的方法Method 对象(目标对象父类的Method)。如果拦截的是字段的话,该注解应该标注到 Field 类型参数。
  • @AllArguments:注入正在执行的方法的全部参数。
  • @Super:注入目标对象的一个代理
  • @SuperCall:这个注解比较特殊,我们要在 intercept() 方法中调用 被代理/增强 的方法的话,需要通过这种方式注入,与 Spring AOP 中的 ProceedingJoinPoint.proceed() 方法有点类似,需要注意的是,这里不能修改调用参数,从上面的示例的调用也能看出来,参数不用单独传递,都包含在其中了。另外,@SuperCall 注解还可以修饰 Runnable 类型的参数,只不过目标方法的返回值就拿不到了。

测试

public static void main(String[] args) throws IllegalAccessException, InstantiationException {
        Class<? extends UserService> aClass = new ByteBuddy()
                // 创建一个UserService 的子类
                .subclass(UserService.class)
                //指定类的名称
                .name("UserServiceImpl")
                // 指定要拦截的方法
                //.method(ElementMatchers.isDeclaredBy(UserService.class))
        .method(ElementMatchers.named("address").and(ElementMatchers.returns(String.class).and(ElementMatchers.takesArguments(1))))
                // 为方法添加拦截器 如果拦截器方法是静态的 这里可以传 LogInterceptor.class
                .intercept(MethodDelegation.to(new LogInterceptor()))
                // 动态创建对象,但还未加载
                .make()
                // 设置类加载器 并指定加载策略(默认WRAPPER)
                .load(ByteBuddy.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
                // 开始加载得到 Class
                .getLoaded();
        UserService userService = aClass.newInstance();

        System.out.println(userService.username());
        System.out.println(userService.address("李四"));
        System.out.println(userService.address("李五","西城区"));
    }

运行测试结果:

准备执行Method=username
username().....
方法执行完成Method=username
张三

准备执行Method=address
address(String username).....
方法执行完成Method=address
李四来自 【xxxx】

address(String username,String city).....
李五来自 【北京西城区】