关于后端:Java字节码-ByteBuddy原理与使用上

43次阅读

共计 3858 个字符,预计需要花费 10 分钟才能阅读完成。

什么是 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 后果是

类型变基(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

正文完
 0