友情支持

如果您觉得这个笔记对您有所帮助,看在D瓜哥码这么多字的辛苦上,请友情支持一下,D瓜哥感激不尽,😜

支付宝

微信

有些打赏的朋友希望可以加个好友,欢迎关注D 瓜哥的微信公众号,这样就可以通过公众号的回复直接给我发信息。

wx jikerizhi

公众号的微信号是: jikerizhi因为众所周知的原因,有时图片加载不出来。 如果图片加载不出来可以直接通过搜索微信号来查找我的公众号。

创建一个类

任何一个由 Byte Buddy 创建的类型都是通过 ByteBuddy 类的实例来完成的。通过简单地调用 new ByteBuddy() 就可以创建一个新实例,然后就可以出发了。希望你使用一个集成开发环境,这样在调用一个给定实例的方法时就能得到相应的提示。这样,你的集成开发环境就会引导你完成相应的方法调用,防止手动在 Byte Buddy 文档中查阅某个类的 API。正如之前所说,Byte Buddy 提供了一个领域特定语言,这样就可以尽可能地提高人类可读性。集成开发环境的提示在大部分情况下会指引你到正确的方向。说的够多了,让我们在 Java 编程环境中创建第一个类吧:

1
2
3
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .make();

正如前面所设想的,上面的示例代码会创建一个继承至 Object 类型的类。这个动态创建的类型与直接扩展 Object 并且没有实现任何方法、属性和构造函数的类型是等价的。你可能已经注意到,我们都没有命名动态生成的类型,通常在定义 Java 类时却是必须的。当然,你也可以很容易地明确地命名这个类型:

1
2
3
4
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .subclass(Object.class)
  .name("example.Type")
  .make();

如果没有明确的命名会怎么样呢?Byte Buddy 与 约定大于配置 息息相关,为你提供了我们认为比较方便的默认配置。至于类型命名,Byte Buddy 的默认配置提供了 NamingStrategy,它基于动态类型的超类名称来随机生成类名。此外,名称定义在与父类相同的包下,这样父类的包级访问权限的方法对动态类型也可见。如果你将示例子类命名为 example.Foo,那么生成的名称将会类似于 example.FooByteBuddy1376491271,这里的数字序列是随机的。这个规则的例外情况就是当子类是从 java.lang 包下的类扩展时,就是 Object 所在的包。Java 的安全模型不允许自定义类型存放在这个命名空间下。因此,默认命名策略下,这些类型名称将会冠以 net.bytebuddy.renamed 的前缀。

默认行为也许对你来说并不方便。感谢约定大于配置原则,你总是可以根据你的需要来选择默认行为。这正是 ByteBuddy 的优越之处。通过 new ByteBuddy() 创建实例,你就创建了整套的默认配置。通过调用在这个配置上的方法,你就可以根据你的需要来订制它。让我们试试:

1
2
3
4
5
6
7
8
9
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
  .with(new NamingStrategy.AbstractBase() {
    @Override
    public String subclass(TypeDescription superClass) {
        return "i.love.ByteBuddy." + superClass.getSimpleName();
    }
  })
  .subclass(Object.class)
  .make();

在上面这里例子中,我们创建了一个新的配置,在类型命名方面,不同于默认配置。匿名类被简单实现为连接 i.love.ByteBuddy 和基类的简要名称。当扩展 Object 类型时,动态类将被命名为 i.love.ByteBuddy.Object。当创建自己的命名策略时,需要特别小心。Java 虚拟机就是使用名字来区分不同的类型的,这正是为什么要避免命名冲突的原因。如果你需要定制命名行为,请考虑使用 Byte Buddy 内置的 NamingStrategy.SuffixingRandom,你可以通过引入比默认对你应用更有意义的前缀来定制命名行为。

领域特定语言和不变性

在看过 Byte Buddy 这种领域特定语言的实际效果之后,我们需要简要看一下这种语言的实现方式。有一个细节需要特别注意,这个语言是围绕 不可变对象 构建的。事实上,Byte Buddy 中,几乎所有的类都被构建成不可变的;极少数情况,我们不可能把对象构建成不可变的,我们会在该类的文档中明确指出。如果你为 Byte Buddy 实现自定义功能,我们建议你遵守此原则。

作为所提到的不可变性的含义,例如配置 ByteBuddy 实例时,一定要小心。你也许会犯下面的错误:

1
2
3
ByteBuddy byteBuddy = new ByteBuddy();
byteBuddy.withNamingStrategy(new NamingStrategy.SuffixingRandom("suffix"));
DynamicType.Unloaded<?> dynamicType = byteBuddy.subclass(Object.class).make();

你或许希望使用 new NamingStrategy.SuffixingRandom("suffix") 来自定义动态类型的命名策略。不是修改存储在 byteBuddy 变量中的实例,调用 withNamingStrategy 方法返回一个自定义的 ByteBuddy 实例,但是它却直接被丢弃了。结果,还是使用原来创建的默认配置来创建动态类型。

重新定义或者重定基底已经存在的类

D瓜哥注

type rebasing 不知如何翻译是好,暂且翻译为“重定基底”。下文中,根据语句通顺需要,偶尔也翻译成“重定义”。如果有好的翻译,欢迎给发PR。

到目前为止,我们仅仅演示了如何使用 Byte Buddy 来创建已知类的子类。相同的 API 还可用于增强已有类。增加已有类有两种方式:

类型重定义(type redefinition)

当重定义一个类时,Byte Buddy 可以对一个已有的类添加属性和方法,或者删除已经存在的方法实现。如果使用其他的方法实现替换已经存在的方法实现,则原来存在的方法实现就会消失。例如,我们重定义下面这个类型

1
2
3
class Foo {
  String bar() { return "bar"; }
}

bar 方法返回 "qux",那么该方法原来返回的 "bar" 等信息就会都被丢失掉。

类型重定基底(type rebasing)

当重定基底一个类时,Byte Buddy 保存基底类所有方法的实现。当 Byte Buddy 如执行类型重定义时,它将所有这些方法实现复制到具有兼容签名的重新命名的私有方法中,而不是抛弃重写的方法。这样,就没有实现会被丢失。重定义的方法可以继续通过它们重命名过的名称调用原来的方法。通过这种方式,上述 Foo 类就会被重定义成下面这个样子:

1
2
3
4
class Foo {
  String bar() { return "foo" + bar$original(); }
  private String bar$original() { return "bar"; }
}

原来返回 bar 的方法被保存到了另外一个方法里,因此还可以访问。当重定基底一个类时,Byte Buddy 对待所有方法定义就像你定义一个子类一样,例如,如果你想调用重定义方法的超类方法是,它会调用被重定义的方法。相反,它最终将这个假设的超级类别变成了上面显示的重定义类型。

任何重定基底、重定义或子类都是使用相同的 API 来执行,接口由 DynamicType.Builder 来定义。这样,可以将类定义为子类,然后更改代码来替换重定类。你只需要修改 Byte Buddy 领域特定语言的一个单词就能达到这个目的。这样,在定义的未来阶段,你就可以透明地切换任何一种方法:

1
2
3
new ByteBuddy().subclass(Foo.class)
new ByteBuddy().redefine(Foo.class)
new ByteBuddy().rebase(Foo.class)

这在本教程的其余部分都有所解释。因为定义子类对于 Java 开发人员来说是如此地熟悉,所以,接下来的所有解释以及 Byte Buddy 领域特定语言的实例都是用创建子类来演示。但是,请记住,所有类可以类似地通过重定义或重定基类来定义。

加载类

到目前为止,我们只定义并创建了一个动态类型,但是我们没有使用它。由 Byte Buddy 创建的类型使用 DynamicType.Unloaded 的实例表示。顾名思义,这些类型不会加载到 Java 虚拟机中。相反,由 Byte Buddy 创建的类以二进制,Java 类文件格式形式表示。这样,您可以决定要使用生成的类型做什么。例如,您可能希望从构建脚本运行 Byte Buddy,该脚本仅在部署之前生成 Java 类以增强应用程序。为此,DynamicType.Unloaded 类允许提取表示动态类型的字节数组。为方便起见,该类型还提供了一个 saveIn(File) 方法,可以将类存储在给定的文件夹中。此外,它允许您使用 inject(File) 方法将类注入到现有的 Jar 文件中。

尽管直接访问类的二进制形式简单直接,但不幸的是,加载类型却非常复杂。在 Java 中,所有的类都使用 ClassLoader 来加载。这种类加载器的一个例子是引导类加载器,它负责加载 Java 类库中发布的类。另一方面,系统类加载器负责加载在 Java 应用程序的类路径上的类。显然,这些先前存在的类加载器都不知道我们创建的任何动态类。为了解决这个问题,我们必须找到能加载运行时生成类的其他可能性。 Byte Buddy 通过不同的方法提供解决方案:

  • 我们简单地创建一个新的 ClassLoader,并明确地告知它一个特定动态创建的类的存在位置。因为 Java 类加载器是以层次结构组织的,所以我们将此类加载器定义为运行中的 Java 应用程序中已经存在的给定类加载器的子类。这样,运行的Java程序的所有类型对于使用新的 ClassLoader 加载的动态类型都是可见的。

  • 通常,Java 类加载器在尝试直接加载给定名称的类型之前查询其双亲 ClassLoader这意味着类加载器通常不会加载类型,以防其父类加载程序知道具有相同名称的类型。为了这个目的,Byte Buddy提供了一个子类优先的类加载器的创建功能,它尝试在查询父类之前自己加载一个类型。除此之外,这种方法类似于上述方法。请注意,此方法不会覆盖父类加载器的类型,而是影响此其他类型。

  • 最后,我们可以使用反射来将类型注入到现有的 ClassLoader 中。通常,类加载器被要求以其名称提供给定类型。使用反射,我们可以围绕这个原理,并调用一个protected方法,将类添加到类加载器中,而类加载器实际上并不知道如何定位这个动态类。

不幸的是,上面的方式有两个缺点:

  • 如果我们创建一个新的 ClassLoader,这个类加载器就会定义一个新的命名空间。有意义的是,可以加载两个具有相同名称的类,只要这些类由两个不同的类加载器加载即可。即使这两个类代表相同的类实现,这两个类也不会被 Java 虚拟机视为相等。这个等式的规则也适用于Java包。这意味着一个类 example.Foo 不能访问另一个类 example.Bar 的包私有级的方法,如果两个类不是由相同的类加载器加载的话。另外,如果 exam​​ple.Bar 扩展 example.Foo,任何覆盖的包私有级的方法将变得不起作用,将会委托给原始实现。

  • 每当加载类时,一旦引用另一种类型的代码段被解析,其类加载器就会查找该类中引用的任何类型。这个查找会委托给同一个类加载器。想象一下,我们动态创建两个类 example.Fooexample.Bar。如果我们将 example.Foo 注入到一个现有的类加载器中,这个类加载器可能会尝试找到 example.Bar。然而,这种查找会失败,因为后一类是动态创建的,对于我们刚注入 example.Foo 类的类加载器是不可访问的。因此,反射方法不能用于在类加载期间变得有效的循环依赖性的类。幸运的是,大多数 Java 虚拟机实现会在第一次主动使用时惰性地解析引用的类,这就是为什么类注入通常可以工作而没有这些限制。另外在实践中,由 Byte Buddy 创建的类通常不会受到这种循环的影响。

您可能会考虑到遇到循环依赖关系的可能性与您一次创建一个动态类型相关联。但是,动态创建类型可能会触发所谓的辅助类型的创建。这些类型由 Byte Buddy 自动创建,以提供对您正在创建的动态类型来访问。我们在下一节中详细了解辅助类型,现在不用担心。但是,由于这个原因,我们建议您通过创建一个特定的 ClassLoader 来加载动态创建的类,而不是将它们注入现有类。

创建 DynamicType.Unloaded 之后,可以使用 ClassLoadingStrategy 加载此类型。如果没有提供这样的策略,Byte Buddy 会根据提供的类加载器推测出这样的策略,并为引导类加载器创建一个新的类加载器,其中不能使用反射注入类型,否则为默认值。Byte Buddy提供了几种类加载策略,其中每种都遵循上述概念之一。这些策略定义在 ClassLoadingStrategy.Default 中,其中 WRAPPER 策略创建一个新的包装 ClassLoaderCHILD_FIRST 策略创建一个类似于第一个子类优先的类加载器;INJECTION 策略使用反射注入动态类型。 WRAPPERCHILD_FIRST 策略也可以在所谓的清单版本中使用,即使在加载类之后,类型的二进制格式也被保留。这些替代版本使得类加载器的类的二进制表示可以通过 ClassLoade::getResourceAsStream 方法访问。但是,请注意,这需要这些类加载器来维护对类的完整二进制表示的引用,这将占用 Java 虚拟机堆上的空间。因此,如果您打算实际访问二进制格式,则应仅使用清单版本。由于 INJECTION 策略通过反射工作,并且无法更改 ClassLoader::getResourceAsStream 方法的语义,因此它在清单版本中自然不可用。

我们来看看类加载的实际操作:

1
2
3
4
5
Class<?> type = new ByteBuddy()
  .subclass(Object.class)
  .make()
  .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
  .getLoaded();

在上面的例子中,我们创建并加载了一个类。我们使用 WRAPPER 策略来加载适合大多数情况的类,就像我们之前提到的那样。最后,getLoaded 方法返回一个 Java Class 的实例,它就表示现在加载的动态类。

请注意,加载类时,通过应用当前执行上下文的 ProtectionDomain 来执行预定义的类加载策略。或者,所有默认策略通过调用 withProtectionDomain 方法来提供明确保护域的规范。使用安全管理员(security manager)或使用已签名的 Jar 中定义的类时,定义显式保护域很重要。

重新加载类

在前面章节,我们学习了如何使用 Byte Buddy 去重定义或者重定基底一个已存在的类。然而,在 Java 程序的执行过程中,通常不可能保证特定的类没有被加载。(此外,Byte Buddy目前只将加载的类作为它的参数,这将在未来的版本中改变,现有的API可以用于同等地处理未加载的类。)由于 Java 虚拟机的“热替换(HotSwap)”特性,即使在加载后也可以重新定义现有类。通过 Byte Buddy 的 ClassRelodingsTrategy 即可使用此功能。让我们通过重新定义类 Foo 来演示这种策略:

1
2
3
4
5
6
7
class Foo {
  String m() { return "foo"; }
}

class Bar {
  String m() { return "bar"; }
}

使用 Byte Buddy,我们现在可以轻松地将类 Foo 重新定义为 Bar。使用热替换,这个重定义甚至可以应用于先前存在的实例:

1
2
3
4
5
6
7
8
9
ByteBuddyAgent.install();
Foo foo = new Foo();
new ByteBuddy()
  .redefine(Bar.class)
  .name(Foo.class.getName())
  .make()
  .load(Foo.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());

assertThat(foo.m(), is("bar"));
为了方便对比原文与翻译,以求有志之士改进翻译,以下增加英语原文,原文下面增加翻译。

HotSwap is only accessible using a so-called Java agent. Such an agent can be installed by either specifying it on the startup of the Java virtual machine by using the -javaagent parameter where the parameter’s argument needs to be Byte Buddy’s agent jar which can be downloaded from Maven Central. However, when a Java application is run from a JDK-installation of the Java virtual machine, Byte Buddy can load a Java agent even after application startup by ByteBuddyAgent.installOnOpenJDK(). Because class redefinition is mostly used to implement tooling or testing, this can be a very convenient alternative. Since Java 9, an agent installation is also possible at runtime without a JDK-installation.

热替换仅能通过所谓的 Java Agent 进行访问。可以通过使用 -javaagent 参数在Java 虚拟机的启动时指定 Java Agent,其中参数值是使用 Byte Buddy 开发的 Jar 包,而 Byte Buddy 相关依赖则可以从 Maven Central 下载。然而,当 Java 应用程序是在从 JDK 安装的 Java 虚拟机中运行时,Byte Buddy 甚至可以通过 ByteBuddyAgent.installOnOpenJDK() 在启动应用程序后加载 Java Agent。因为类重定义主要用于实现工具或测试,所以这是一种非常方便的替代方法。从 Java 9 开始,在运行时不需要 JDK 安装也可以进行代理安装。

One thing that might first appear counter-intuitive about the above example is the fact that Byte Buddy is instructed to redefine the Bar type where the Foo type is eventually redefined. The Java virtual machine identifies types by their name and a class loader. Therefore, by renaming Bar to Foo and applying this definition, we eventually redefine the type we renamed Bar into. It is of course equally possible to redefine Foo directly without renaming a different type.

关于上面的示例,首先看起来可能与直觉相反的一件事是,Byte Buddy 被指示重新定义 Bar 类型,而 Foo 类型最终被重新定义。Java 虚拟机通过类型的名称和类加载器来识别类型。因此,通过将 Bar 重命名为 Foo 并应用此定义,我们最终重新定义了将 Bar 重命名为的类型。当然,同样可以直接重新定义 Foo,而无需重命名其他类型。

Using Java’s HotSwap feature, there is however one huge drawback. Current implementations of HotSwap require that the redefined classes apply the same class schema both before and after a class redefinition. This means that it is not allowed to add methods or fields when reloading classes. We already discussed that Byte Buddy defines copies of the original methods for any rebased class such that class rebasing does not work for the ClassReloadingStrategy. Also, class redefinition does not work for classes with an explicit class initializer method (a static block within a class) because this initializer needs to be copied into an extra method as well. Unfortunately OpenJDK has withdrawn from extending HotSwap functionality, so there is no way to work around this limitation using the HotSwap feature. In the mean time, Byte Buddy’s HotSwap support can be used for corner-cases where it seems useful. Otherwise, class rebasing and redefinition can be a convenient feature when enhancing existing classes from for example a build script.

然而,使用 Java 的热交换功能,有一个巨大的缺点。当前的热替换实现要求重定义的类在重定义之前和之后都应用相同的模式。这意味着在重载类时不允许添加方法或字段。我们已经讨论过,Byte Buddy 为任何重定基类定义了原始方法的副本,这样重定基底的类,对于 ClassReloadingStrategy 来说,都是不适用的。同样,类重定义不适用于具有显式类初始化器方法(类中的静态块)的类,因为这个初始化器还需要复制到一个额外的方法中。不幸的是,OpenJDK 已经退出了 对热替换功能的扩展,因此使用热替换功能就无法绕过这一限制。同时,Byte Buddy的热替换对于一些极端情况,似乎特别有用。同时,当增加现有类时,比如一个构建脚本,类的重定基底和类的重定义将是一个非常方便的特效。

操作没有加载的类

1
2
package foo;
class Bar { }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class MyApplication {
  public static void main(String[] args) {
    TypePool typePool = TypePool.Default.ofClassPath();
    new ByteBuddy()
      .redefine(typePool.describe("foo.Bar").resolve(), // do not use 'Bar.class'
                ClassFileLocator.ForClassLoader.ofClassPath())
      .defineField("qux", String.class) // we learn more about defining fields later
      .make()
      .load(ClassLoader.getSystemClassLoader());
    assertThat(Bar.class.getDeclaredField("qux"), notNullValue());
  }
}

创建 Java Agents

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class ToStringAgent {
  public static void premain(String arguments, Instrumentation instrumentation) {
    new AgentBuilder.Default()
        .type(isAnnotatedWith(ToString.class))
        .transform(new AgentBuilder.Transformer() {
      @Override
      public DynamicType.Builder transform(DynamicType.Builder builder,
                                              TypeDescription typeDescription,
                                              ClassLoader classloader) {
        return builder.method(named("toString"))
                      .intercept(FixedValue.value("transformed"));
      }
    }).installOn(instrumentation);
  }
}

在 Android 应用中加载类

使用泛型类