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的资料请关注脚本之家其它相关文章!

相关文章

  • 浅析我对 String、StringBuilder、StringBuffer 的理解

    浅析我对 String、StringBuilder、StringBuffer 的理解

    StringBuilder、StringBuffer 和 String 一样,都是用于存储字符串的。这篇文章谈谈小编对String、StringBuilder、StringBuffer 的理解,感兴趣的朋友跟随小编一起看看吧
    2020-05-05
  • IDEA中查看类继承图和类源码的骚操作

    IDEA中查看类继承图和类源码的骚操作

    这篇文章主要介绍了IDEA中查看类继承图和类源码的骚操作,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-02-02
  • Java中如何判断中文字符串长度

    Java中如何判断中文字符串长度

    这篇文章主要介绍了Java中如何判断中文字符串长度问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-05-05
  • 通过实例了解cookie机制特性及使用方法

    通过实例了解cookie机制特性及使用方法

    这篇文章主要介绍了通过实例了解cookie机制特性及使用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-09-09
  • SpringBoot项目整合jasypt实现过程详解

    SpringBoot项目整合jasypt实现过程详解

    这篇文章主要介绍了SpringBoot项目整合jasypt实现过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-08-08
  • Java调用第三方接口示范的实现

    Java调用第三方接口示范的实现

    这篇文章主要介绍了Java调用第三方接口示范的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • java byte数组与16进制间相互转换的示例

    java byte数组与16进制间相互转换的示例

    这篇文章主要介绍了java byte数组与16进制间相互转换的示例,帮助大家更好的理解和使用Java,感兴趣的朋友可以了解下
    2020-10-10
  • 详解Java中类与对象的关系

    详解Java中类与对象的关系

    这篇文章主要介绍了详解Java中类与对象的关系,类的关键字是class,在Java编程里,类的作用相当于机械师手中的构造图,如果没有构造图,就无法打造武器,同样如果没有类,就无法实例化,需要的朋友可以参考下
    2023-05-05
  • 详解在Java中如何创建多线程程序

    详解在Java中如何创建多线程程序

    今天给大家带来的是关于Java的相关知识,文章围绕着在Java中如何创建多线程程序展开,文中有非常详细的介绍及代码示例,需要的朋友可以参考下
    2021-06-06
  • Mybatis MapperScannerConfigurer自动扫描Mapper接口生成代理注入到Spring的方法

    Mybatis MapperScannerConfigurer自动扫描Mapper接口生成代理注入到Spring的方法

    这篇文章主要给大家介绍了关于Mybatis MapperScannerConfigurer自动扫描将Mapper接口生成代理注入到Spring的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2019-03-03

最新评论