写点什么

Java 字节码 - ByteBuddy 原理与使用(上)

作者:骑牛上青山
  • 2023-05-16
    上海
  • 本文字数:2882 字

    阅读完需:约 9 分钟

什么是 ByteBuddy

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

为什么需要 ByteBuddy

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


不过反射也有他的缺点:


  1. 反射显而易见的缺点是慢。我们在使用反射之前都需要谨慎的考虑他对于当前性能的影响,唯有进过详细的评估,才能够放心的使用。

  2. 反射能够绕过类型安全检查。我们在使用反射的时候需要确保相应的接口不会暴露给外部用户,不然可能造成不小的安全隐患。


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策略来生成类名,一般情况下为


父类的全限定名 + + 随机字符串

例如: org.example.MyTestNsT9pB6w


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


net.bytebuddy.renamed. + 父类的全限定名 + + 随机字符串

例如: net.bytebuddy.renamed.java.lang.Object2VOeD4Lh


以此来规避 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<?> 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中:


  1. WRAPPER:这个策略会创建一个新的ByteArrayClassLoader,并使用传入的类加载器为父类。

  2. WRAPPER_PERSISTENT:该策略和WRAPPER大致一致,只是会将所有的类文件持久化到类加载器中

  3. CHILD_FIRST:这个策略是WRAPPER的改版,其中动态类型的优先级会比父类加载器中的同名类高,即在此种情况下不再是类加载器通常的父类优先,而是“子类优先”

  4. CHILD_FIRST_PERSISTENT:该策略和CHILD_FIRST大致一致,只是会将所有的类文件持久化到类加载器中

  5. 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


TypePool typePool = TypePool.Default.ofSystemLoader();    Class bar = new ByteBuddy()      .redefine(typePool.describe("foo.Bar").resolve(),                ClassFileLocator.ForClassLoader.ofSystemLoader())      .defineField("qux", String.class)      .make()      .load(ClassLoader.getSystemClassLoader(), ClassLoadingStrategy.Default.INJECTION)      .getLoaded();
复制代码


参考文章


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


发布于: 刚刚阅读数: 3
用户头像

还未添加个人签名 2021-05-18 加入

还未添加个人简介

评论

发布
暂无评论
Java字节码 - ByteBuddy原理与使用(上)_Java_骑牛上青山_InfoQ写作社区