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

我正在参加「掘金·启航计划」

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

父类的全限定名 + $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结果是

redefine.png

类型变基(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之后结果

rebase.png

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

注意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] bytebuddy.net/#/tutorial

猜你喜欢

转载自juejin.im/post/7233409321342107707