Java字节码ByteBuddy使用及原理解析上

 更新时间:2023年05月18日 11:51:13   作者:骑牛上青山  
这篇文章主要为大家介绍了Java字节码ByteBuddy使用及原理解析上篇,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

什么是ByteBuddy

ByteBuddy是一个java的运行时代码生成库,他可以帮助你以字节码的方式动态修改java类的代码。

为什么需要ByteBuddy

Java是一个强类型语言,有着极为严格的类型系统。这个严格的类型系统可以帮助构建严谨,更不容易被腐化的代码,但是也在某些方面限制了java的应用。不过为了解决这个问题,java提供了一套反射的api来帮助使用者感知和修改类的内部。

不过反射也有他的缺点:

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

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

ByteBuddy使用

创建一个类

new ByteBuddy()
            .subclass(Object.class)
            .method(ElementMatchers.named("toString"))
            .intercept(FixedValue.value("Hello World!"))
            .make()
            .saveIn(new File("result"));

上述代码创建了一个Object的子类并且创建了toString方法输出Hello World!通过找到保存的输出类我们可以看到最后的类是这样的:

package net.bytebuddy.renamed.java.lang;
public class Object$ByteBuddy$tPSTnhZh {
    public String toString() {
        return "Hello World!";
    }
    public Object$ByteBuddy$tPSTnhZh() {
    }
}

可以看到我们虽然创建了一个类,但是我们没有为这个类取名,通过结果得知最后的类名是
net.bytebuddy.renamed.java.lang.Object$ByteBuddy$tPSTnhZh,那么这个类名是怎么来的呢?

在ByteBuddy中如果没有指定类名,他会调用默认的NamingStrategy策略来生成类名,一般情况下为

父类的全限定名 + $ByteBuddy$ + 随机字符串
例如: org.example.MyTest$ByteBuddy$NsT9pB6w

如果父类是java.lang目录下的类,例如Object,那么会变成

net.bytebuddy.renamed. + 父类的全限定名 + $ByteBuddy$ + 随机字符串
例如: net.bytebuddy.renamed.java.lang.Object$ByteBuddy$2VOeD4Lh

以此来规避java安全模型的限制。

类型重定义与变基

定义一个类

package org.example.bytebuddy.test;
public class MyClassTest {
    public String test() {
        return "my test";
    }
}

用这个类来验证如下的能力

类型重定义(type redefinition)

ByteBuddy支持对于已存在的类进行重定义,即可以添加或者删除类的方法。只不过当类的方法被重定义之后,那么原先的方法中的信息就会丢失。

Class<?> dynamicType = new ByteBuddy()
                .redefine(MyClassTest.class)
                .method(ElementMatchers.named("test"))
                .intercept(FixedValue.value("Hello World!"))
                .make()
                .load(String.class.getClassLoader()).getLoaded();

redefine结果是

类型变基(type rebasing)

rebase操作和redefinition操作最大的区别就是rebase操作不会丢失原先的类的方法信息。大致的实现原理是在变基操作的时候把所有的方法实现复制到重新命名的私有方法(具有和原先方法兼容的签名)中,这样原先的方法就不会丢失。

Class&lt;?&gt; dynamicType = new ByteBuddy()
                .rebase(MyClassTest.class)
                .method(ElementMatchers.named("test"))
                .intercept(FixedValue.value("Hello World!"))
                .make()
                .load(String.class.getClassLoader()).getLoaded();

rebase之后结果

可以看到原先的方法被重命名后保留了下来,并且变成了私有方法。

注意redefinition和rebasing不能修改已经被jvm加载的类,不然会报错Class already loaded

类的加载

生成了之后为了在代码中使用,必须要经过load流程。细心的读者可能已经发现了上文中已经使用到了load相关的方法。

构建了具体的动态类之后,可以选择使用saveIn将其结构体存储下来,也可以选择将它装载到虚拟机中。在类加载器的选择中,ByteBuddy提供了几种选择放在ClassLoadingStrategy.Default中:

  • WRAPPER:这个策略会创建一个新的ByteArrayClassLoader,并使用传入的类加载器为父类。
  • WRAPPER_PERSISTENT:该策略和WRAPPER大致一致,只是会将所有的类文件持久化到类加载器中
  • CHILD_FIRST:这个策略是WRAPPER的改版,其中动态类型的优先级会比父类加载器中的同名类高,即在此种情况下不再是类加载器通常的父类优先,而是“子类优先”
  • CHILD_FIRST_PERSISTENT:该策略和CHILD_FIRST大致一致,只是会将所有的类文件持久化到类加载器中
  • INJECTION:这个策略最为特殊,他不会创建类加载器,而是通过反射的手段将类注入到指定的类加载器之中。这么做的好处是用这种方法注入的类对于类加载器中的其他类具有私有权限,而其他的策略不具备这种能力。

类的重载

前面提到过,rebase和redefine通常没办法重新加载已经存在的类,但是由于jvm的热替换(HotSwap)机制的存在,使得ByteBuddy可以在加载后也能够重新定义类。

class Foo {
  String m() { return "foo"; }
}
class Bar {
  String m() { return "bar"; }
}

我们通过ByteBuddy的ClassRelodingsTrategy即可完成热替换。

ByteBuddyAgent.install();
Foo foo = new Foo();
new ByteBuddy()
  .redefine(Bar.class)
  .name(Foo.class.getName())
  .make()
  .load(Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());

需要注意的是热替换机制必须依赖Java Agent才能使用。Java Agent是一种可以在java项目运行前或者运行时动态修改类的技术。通常可以使用-javaagent参数引入java agent。

处理尚未加载的类

ByteBuddy除了可以处理已经加载完的类,他也具备处理尚未被加载的类的能力。

ByteBuddy对java的反射api做了抽象,例如Class实例就被表示成了TypeDescription实例。事实上,ByteBuddy只知道如何通过实现TypeDescription接口的适配器来处理提供的 Class。这种抽象的一大优势是类信息不需要由类加载器提供,可以由任何其他来源提供。

ByteBuddy中可以通过TypePool获取类的TypeDescription,ByteBuddy提供了TypePool的默认实现TypePool.Default。这个类可以帮助我们把java字节码转换成TypeDescription

Java的类加载器只会在类第一次使用的时候加载一次,因此我们可以在java中以如下方式安全的创建一个类:

package foo;
class Bar { }

但是通过如下的方法,我们可以在Bar这个类没有被加载前就提前生成我们自己的Bar,因此后续jvm就只会使用到我们的Bar

参考文章

[1] https://bytebuddy.net/#/tutorial

以上就是Java字节码ByteBuddy使用及原理解析上的详细内容,更多关于Java字节码ByteBuddy的资料请关注脚本之家其它相关文章!

相关文章

  • 详解如何实现nacos的配置的热更新

    详解如何实现nacos的配置的热更新

    这篇文章主要为大家详细介绍了如何实现nacos的配置的热更新,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以跟随小编一起学习一下
    2023-12-12
  • Java基本数据类型(动力节点java学院整理)

    Java基本数据类型(动力节点java学院整理)

    Java数据类型(type)可以分为两大类:基本类型(primitive types)和引用类型(reference types)。下面是动力节点给大家整理java基本数据类型相关知识,感兴趣的朋友一起学习吧
    2017-03-03
  • Java 十大排序算法之插入排序刨析

    Java 十大排序算法之插入排序刨析

    插入排序(InsertionSort),一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增 1 的有序表
    2021-11-11
  • MyBatisPlus @TableName表名注解的基本操作

    MyBatisPlus @TableName表名注解的基本操作

    @TableName注解是MyBatis-Plus中用于指定实体类与数据库表映射关系的重要工具,通过使用@TableName注解,可以明确地指定表名,支持多级表名和动态表名,本文给大家介绍MyBatisPlus @TableName表名注解的相关操作,感兴趣的朋友跟随小编一起看看吧
    2025-09-09
  • SpringBoot请求参数接收控制指南分享

    SpringBoot请求参数接收控制指南分享

    这篇文章主要介绍了SpringBoot请求参数接收控制指南,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-04-04
  • Java中ThreadLocalMap解决Hash冲突的实现方式

    Java中ThreadLocalMap解决Hash冲突的实现方式

    本文主要介绍了Java中ThreadLocalMap解决Hash冲突的实现方式,主要方式是使用线性探测法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-04-04
  • Java如何基于wsimport调用wcf接口

    Java如何基于wsimport调用wcf接口

    这篇文章主要介绍了Java如何基于wsimport调用wcf接口,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-06-06
  • SpringBoot集成ffmpeg实现视频转码播放示例详解

    SpringBoot集成ffmpeg实现视频转码播放示例详解

    这篇文章主要为大家介绍了SpringBoot集成ffmpeg实现视频转码播放示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-07-07
  • Docker容器使用宿主机上的mongod/redis等服务详解

    Docker容器使用宿主机上的mongod/redis等服务详解

    这篇文章主要介绍了Docker容器使用宿主机上的mongod/redis等服务详解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11
  • 以银行取钱为例模拟Java多线程同步问题完整代码

    以银行取钱为例模拟Java多线程同步问题完整代码

    这篇文章主要介绍了以银行取钱为例模拟Java多线程同步问题完整代码,具有一定借鉴价值,需要的朋友可以参考下。
    2017-12-12

最新评论