字节码不止有 Java Proxy、 Cglib 和 Javassist 还有 ByteBuddy

提到字节码增强技术,相信用过 Spring 的小伙伴都会知道 Java ProxyCglib

毕竟面试准备的八股文中说过,Spring 的动态代理有两种实现方式,在有接口存在的时候使用 Java Proxy,当没有接口的时候使用的是 Cglib

这两种方式的区别不在本文的讨论范围之内,今天想给大家介绍了是另一个字节码增强技术 Byte Buddy

Byte Buddy

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

Byte Buddy 提供一套简单易用的 API,可以很方便的使用 Java 流式编程的形式来动态创建类或者创建接口的实现类,这一点跟 Java ProxyCglib 不一样。

使用 Byte Buddy 的方式也非常简单,只要直接引入 Maven 依赖即可,没有其他繁琐的依赖。总的来说,使用 Byte Buddy 有下面的优势:

  1. 无需理解字节码格式,简单易用的 API 能很容易操作字节码;
  2. 支持 Java 任何版本,库轻量,仅取决于 Java 字节代码解析器库 ASM 的访问者 API,它本身不需要任何其他依赖项。
  3. 比起 JDK 动态代理、cglibJavassistByte Buddy 在性能上具有优势。
0-5

这一份测试报告是官网提供的,表中的每一行分别为,类的创建、接口实现、方法调用、类型扩展、父类方法调用的性能结果。

从性能报告中可以看出,Byte Buddy 在一些场景是有优势的,但是在有些场景也不见得特别有优势,不过整体来看还是不错的。

测试

说了那么多,下面给大家演示一下,如果使用 Byte Buddy,首先我们需要引入 Maven 依赖,我这里用的版本是 1.14.6,也可以使用其他版本。

<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy</artifactId>
    <version>1.14.6</version>
</dependency>

创建一个类,并覆盖 toString

public static void test1() {
        try {
            Class<?> dynamicType = new ByteBuddy().
                    subclass(Object.class)
                    .method(ElementMatchers.named("toString"))
                    .intercept(FixedValue.value("Hello World!"))
                    .make()
                    .load(ByteBuddyDemo.class.getClassLoader())
                    .getLoaded();
            System.out.println(dynamicType.newInstance().toString());
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }

public static void test2() {
        try {
            DynamicType.Unloaded<Object> unloaded = new ByteBuddy()
                    .subclass(Object.class)
                    .method(ElementMatchers.named("toString"))
                    .intercept(FixedValue.value("Hello World!"))
                    .make();
            DynamicType.Loaded<Object> load = unloaded.load(ByteBuddyDemo.class.getClassLoader());
            System.out.println(load.getLoaded().newInstance().toString());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

整个代码的思路是通过 Byte Buddy,构造出一个 Class 对象,然后调用 Class 对象的 newInstance() 方法,再执行 toString() 方法。上面两个方式的功能是一样的,写出来更方便大家理解。

其中各个方法的含义如下:

subClass:表示构造的类是 Object 的子类;

method:表示要构造的具体方法,类似于过滤的功能;

intercept:表示对过滤后的方法进行拦截;

FixedValue.value("Hello World!"):表示构造返回一个”Hello World!“ 字符串;

make:创建 DynamicType.Unloaded 对象,此时这个对象被构造出来,但是还没有被 JVM 加载,还不能使用;

loadgetLoaded:加载当前类的构造器,并进行加载;

等到加载到 JVM 过后,就可以使用 newInstance().toString() 进行调用了。

代理方法

上面的例子是创建一个简单的类和方法,下面我们介绍一个代理方法的使用,这里我们有一个目标类 Target 和一个方法 saySomething() 方法,有一个代理类 Agent,里面有一个代理方法 agentSaySomething(),如下所示:

public class Target {
    public String saySomething() {
        return "Hello target";
    }
}

public class Agent {
    public static String agentSaySomething() {
        System.out.println("agentSaySomething");
        return "hello agent";
    }
}


public static void test4() {
        try {
            DynamicType.Unloaded<Target> agent = new ByteBuddy()
                    .subclass(Target.class)
                    .method(named("saySomething")
                            .and(isDeclaredBy(Target.class)
                                    .and(returns(String.class))))
                    .intercept(MethodDelegation.to(Agent.class))
                    .make();
            // 将 agent 字节码写入文件中
            outputClazz(agent.getBytes());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static void outputClazz(byte[] bytes) {
        FileOutputStream out = null;
        try {
            String pathName = ByteBuddyDemo.class.getResource("/").getPath() + "AgentTarget.class";
            out = new FileOutputStream(new File(pathName));
            System.out.println("类输出路径:" + pathName);
            out.write(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != out) try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

   public static void main(String[] args) {
        test4();
    }

运行过后我们可以看到生成了一个 class 文件,通过查看代码如下,可以看到是创建了一个 Target 的子类,并且调用了 AgentagentSaySomething 方法。

0-6

总结

Byte BuddyAPI 很丰富,这里只是很简单的给大家使用了几个 API,还有包括方法,字段的设定等等,感兴趣的小伙伴可以继续去学习学习。

评论