Java bytecode - ByteBuddy principle and use (on)

I am participating in the "Nuggets·Starting Plan"

What is ByteBuddy

ByteBuddyIt is a runtime code generation library for java, which can help you dynamically modify the code of java classes in bytecode.

Why you need ByteBuddy

Java is a strongly typed language with an extremely strict type system. This strict type system can help to build rigorous and less corrupt code, but it also limits the application of java in some ways. However, in order to solve this problem, Java provides a set of reflection APIs to help users perceive and modify the interior of the class.

However, reflection also has its disadvantages:

  1. The obvious disadvantage of reflection is that it is slow. Before using reflection, we need to carefully consider its impact on the current performance. Only after a detailed evaluation can we use it with confidence.
  2. Reflection can bypass type safety checks. When using reflection, we need to ensure that the corresponding interface will not be exposed to external users, otherwise it may cause considerable security risks.

And ByteBuddyit can help us do what reflection can do without having to suffer from his shortcomings.

ByteBuddy uses

create a class

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

The above code creates a Objectsubclass of and toStringmethod output Hello World!. By finding the saved output class, we can see that the final class looks like this:

package net.bytebuddy.renamed.java.lang;

public class Object$ByteBuddy$tPSTnhZh {
    public String toString() {
        return "Hello World!";
    }

    public Object$ByteBuddy$tPSTnhZh() {
    }
}

It can be seen that although we have created a class, we have not named the class, and we know from the results that the final class name is net.bytebuddy.renamed.java.lang.Object$ByteBuddy$tPSTnhZh, so how did this class name come from?

If no class name is specified in ByteBuddy, it will call the default NamingStrategystrategy to generate the class name, usually

Fully qualified name of parent class + $ByteBuddy$ + random string
Example : org.example.MyTest$ByteBuddy$NsT9pB6w

If the parent class is a class in the java.lang directory, such as Object, it will become

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

Guess you like

Origin juejin.im/post/7233409321342107707