字节码增强技术,不止有 Java Proxy、 Cglib 和 Javassist 还有 Byte Buddy

2024年 1月 30日 33.6k 0

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

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

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

Byte Buddy

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

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

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

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

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

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

    测试

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

    
        net.bytebuddy
        byte-buddy
        1.14.6
    

    创建一个类,并覆盖 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 unloaded = new ByteBuddy()
                        .subclass(Object.class)
                        .method(ElementMatchers.named("toString"))
                        .intercept(FixedValue.value("Hello World!"))
                        .make();
                DynamicType.Loaded 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 加载,还不能使用;

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

    等到加载到 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 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 的子类,并且调用了 Agent 的 agentSaySomething 方法。

    图片图片

    总结

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

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论