https://notes.diguage.com/byte-buddy-tutorial/
官网及版本库
本文档的版本库使用 Git 管理。另外,单独发布阅读版。
- “地瓜哥”博客网
-
http://www.diguage.com/ 。D瓜哥的个人博客。欢迎光临,不过,内容很杂乱,请见谅。不见谅,你来打我啊,???? - 本文档官网
-
http://notes.diguage.com/byte-buddy-tutorial/ 。为了方便阅读,这里展示了处理好的文档。阅读请点击这个网址。 - 本文档版本库
-
https://github.com/diguage/byte-buddy-tutorial 。反正也没啥保密可言,版本库分分钟给出。??
文本中斜体字表示翻译很不满意,还需要再三斟酌来探究更好的翻译。希望感兴趣的小伙伴在阅读的时候多多留意。更希望发送 PR 过来。谢谢!! |
为什么需要在运行时生成代码?
Java 语言带有一套比较严格的类型系统。Java 要求所有变量和对象都有一个确定的类型,并且任何赋值不兼容类型的尝试都会抛出错误。这些错误通常都会被编译器检查出来,或者极少情况下,在非法转换类型的时候由Java运行时抛出。如此严格的类型限制在大多数情况下是可取的,比如在编写业务应用时。在业务领域,通常可以以明确的方式去描述其中任何元素,各个元素都有自己明确的类型。通过这种方式,我们可以用 Java 构建具有非常强可读性和稳定性的应用,应用中的错误也非常贴近源码。除此之外,Java 严格的类型系统造就 Java 在企业编程中的普及。
然而,通过强制实施其严格的类型系统,Java 限制了自己在其他领域的应用范围。 比如,当编写一个供其他 Java 应用使用的通用库时,我们通常不能引用用户应用中定义的任何类型,因为当这个通用库被编译时,我们还不知道这些类型。为了调用用户未知代码的方法或者访问其属性,Java 类库提供了一套反射 API。使用这套反射 API,我们就可以反省未知类型,进而调用方法或者访问属性。不幸的是,这套反射 API 的用法有两个明显的缺点:
- 相比硬编码的方法调用,使用 反射 API 非常慢:首先,需要执行一个相当昂贵的方法查找来获取描述特定方法的对象。同时,当一个方法被调用时,这要求 Java 虚拟机去运行本地代码,相比直接调用,这需要一个很长的运行时间。然而,现代 Java 虚拟机知道一个被称为“类型膨胀”的概念:基于 JNI 的方法调用会被动态生成的字节码给替换掉,而这些方法调用的字节码被注入到一个动态生成的类中。(即使 Java 虚拟机自身也使用代码生成!)毕竟,Java 的类型膨胀系统仍存在生成非常一般的代码的缺点,例如,仅能使用基本类型的装箱类型以至于性能缺陷不能完全解决。
- 反射 API 能绕过类型安全检查:即使 Java 虚拟机支持通过反射进行代码调用,但反射 API 自身并不是类型安全的。当编写一个类库时,只要我们不需要把反射 API 暴露给库的用户,就不会有什么大问题。毕竟,当我们编译类库时,我们不知道用户代码,而且也不能校验我们的库与用户类型是否匹配。有时,需要通过让一个库为我们自己调用我们自己的方法之一来向用户显示反射 API 示例。这是使用反射 API 变得有问题的地方,因为 Java 编译器将具有所有信息来验证我们的程序的类型安全性。例如,当实现方法级安全库时,这个库的用户将希望这个库做到强制执行安全限制才能调用方法。为此,在用户传递过来方法所需的参数后,这个库将反射性地调用方法。这样,就没有编译时类型检查这些方法参数是否与方法的反射调用相匹配。方法调用依然会校验,只是被推迟到了运行时。这样做,我们就错失了 Java 编程语言的一大特性。
这正是运行时代码生成能帮助我们的地方。它允许我们模拟一些只有使用动态编程语言编程才有的特性,而且不丢失 Java 的静态类型检查。这样,我们就可以两全其美并且还可以提高运行时性能。为了更好地理解这个问题,让我们实现一个方法级安全库。
编写一个安全的库
业务应用程序可能会增长,有时很难在我们的应用程序中概述调用堆栈。当我们在应用程序中使用至关重要的方法时,而这些方法只能在特定条件下调用,这可能会变得有问题。 设想一下,实现重置功能的业务应用程序可以从应用程序的数据库中删除所有内容。
class Service {
void deleteEverything() {
// delete everything ...
}
}
这样的复位操作当然只能由管理员执行,而不是由应用程序的普通用户执行。通过分析源代码,我们当然可以确保这将永远不会发生。但是,我们期望我们的应用能够在未来发展壮大。因此,我们希望实现更紧密的安全模型,其中通过对应用程序的当前用户的显式检查来保护方法调用。我们通常会使用一个安全框架来确保该方法从不被除管理员外的任何人调用。
为此,假设我们使用具有公共 API 如下的安全框架:
@Retention(RetentionPolicy.RUNTIME)
@interface Secured {
String user();
}
class UserHolder {
static String user;
}
interface Framework {
<T> T secure(Class<T> type);
}
在此框架中,Secured
注解应用于标记只能由给定用户访问的方法。UserHolder
用于在全局范围内定义当前登录到应用程序的用户。Framework
接口允许通过调用给定类型的默认构造函数来创建安全实例。当然,这个框架过于简单,但是,从本质上来说,即使流行的安全框架,例如 Spring Security,也是这样实现的。这个安全框架的一个特点是我们过滤用户的类型。通过调用我们框架的接口,我们承诺返回给用户任何类型 T
的实例。幸亏这样,用户能够透明地他自己的类型进行交互,就像安全框架根本不存在一样。在测试环境中,用户甚至可以创建其类型的不安全实例,使用这些实例来代替安全实例。你会同意这真的很方便!已知这种框架使用 POJO,普通的旧 Java 对象进行交互,这是一种用于描述不侵入框架的术语,这些框架不会将自己的类型强加给用户。
现在,想象一下,假如我们知道传递给 Framework
的类型只能是 T = Service
,而且 deleteEverything
方法用 @Secured("ADMIN")
注解。这样,我们可以通过简单的子类化来轻松实现这种特定类型的安全版本:
class SecuredService extends Service {
@Override
void deleteEverything() {
if(UserHolder.user.equals("ADMIN")) {
super.deleteEverything();
} else {
throw new IllegalStateException("Not authorized");
}
}
}
通过这个额外的类,我们可以实现框架如下:
class HardcodedFrameworkImpl implements Framework {
@Override
public <T> T secure(Class<T> type) {
if(type == Service.class) {
return (T) new SecuredService();
} else {
throw new IllegalArgumentException("Unknown: " + type);
}
}
}
当然这个实现并没有太多的用处。通过标注 secure
方法签名,我们建议该方法可以为任何类型提供安全性,但实际上,一旦遇到其他事情,我们将抛出一个异常,然后是已知的 Service
。此外,当编译库时,这将需要我们的安全库知道有关此特定 Service
类型的信息。显然,这不是实现框架的可行解决方案。那么我们如何解决这个问题呢?好吧,由于这是一个关于代码生成库的教程,你可能已经猜到答案:当通过调用 secure
方法, Service
类第一次被我们安全框架知道时,我们会在运行时后台地创建一个子类。通过使用代码生成,我们可以使用任何给定的类型,在运行时将其子类化,并覆盖我们要保护的方法。在我们的例子中,我们覆盖所有被 @Secured
注解标注的方法,并从注解的 user
属性中读取所需的用户。许多流行的 Java 框架都使用类似的方法实现。
基本信息
在学习代码生成和 Byte Buddy 之前,请注意,应该谨慎使用代码生成。Java 类型对于 Java 虚拟机来说,是相当特别的东西,通常不能当做垃圾被回收。因此,不应该过度使用代码生成,而应该只在生成代码是解决问题的唯一出路时使用。但是,如果需要像上面的示例那样增强未知类型时,则代码生成很可能是你唯一的选择。用于安全性,事务管理,对象关系映射或类型模拟(mock)等框架是代码生成库的典型用户。
当然,Byte Buddy 不是 Java 虚拟机上第一个代码生成库。不过,我们认为 Byte Buddy 拥有其他框架没有的技巧。Byte Buddy 的总体目标是通过专注于其领域特定语言和注解的使用来声明式地进行工作。据我们所知,没有其他针对 Java 虚拟机的代码生成库以这种方式工作。不过,你可能希望看一下其他代码生成框架,以找出最适合你的套件。以下库在 Java 中很流行:
- Java Proxy
-
Java 类库自带了一个代理工具,它允许为实现了一系列接口的类创建代理。这个内置的代理供应商非常方便,但局限性也特别明显。 上面提到的安全框架就不能用这样的方式来实现的,因为我们想扩展是类而不是扩展接口。 - cglib
-
代码生成库(注:这里指cglib
)诞生于 Java 初期,但不幸的是没有跟上 Java 平台的发展。然而,cglib 仍然是一个相当强大的库,但其积极的开发却变得相当模糊。鉴于此,其许多用户已经离开了 cglib。 - Javassist
-
该库附带一个编译器,它使用包含 Java 源代码的字符串,这些字符串在应用程序的运行时被转换为 Java 字节码。这是非常有前途的,本质上是一个好主意,因为 Java 源代码显然是描述 Java 类的好方法。但是,Javassist 编译器在功能上比不了 javac 编译器,并且在动态组合字符串以实现比较复杂的逻辑时容易出错。此外,Javassist 还提供了一个类似于 Java 类库中的代理工具,但允许扩展类,并不限于接口。然而,Javassist 的代理工具的范围在其 API 和功能上仍然受到限制。
即使评估完这些框架,但我们相信 Byte Buddy 提供了功能和便利,可以减少徒劳地搜索。Byte Buddy 提供了一种具有表现力的领域特定语言,允许通过编写简单的 Java 代码和使用强大的类型为你自己的代码创建非常自定义的运行时类。与此同时,Byte Buddy 还具有非常开放的定制性,并不限制开箱即用的功能。如果需要,你甚至可以为任何实现的方法定义自定义字节码。但即使不知道什么字节代码是或它如何工作,你可以做很多,而不深入到框架。你有没有看看 Hello World!
example?,使用 Byte Buddy 是如此简单。
当然,在选择代码生成库时,一个愉快的 API 不是唯一需要考虑的特性。对于许多应用程序,生成代码的运行时特性更有可能确定最佳选择。而在生成的代码本身的运行时间之外,用于创建动态类的运行时也是一个问题。声称“我们是最快的!”很容易,但是为库的速度提供有效的评比指标却很难。不过,我们希望提供这样的指标作为基本方向。但是,请注意,这些结果并不一定会转化为更具体的用例,此时你应该采用单独的指标。
在讨论我们的指标之前,让我们来看一下原始数据。下表显示了一个操作的平均运行时间,以纳秒为单位,标准偏差在括号内附加:
基线 | Byte Buddy | cglib | Javassist | Java proxy | |
---|---|---|---|---|---|
简单的类创建 |
0.003 (0.001) |
142.772 (1.390) |
515.174 (26.753) |
193.733 (4.430) |
70.712 (0.645) |
接口实现 |
0.004 (0.001) |
1‘126.364 (10.328) |
960.527 (11.788) |
1‘070.766 (59.865) |
1‘060.766 (12.231) |
方法调用 |
0.002 (0.001) |
0.002 (0.001) |
0.003 (0.001) |
0.011 (0.001) |
0.008 (0.001) |
类型扩展 |
0.004 (0.001) |
885.983 (7.901) |
1‘632.730 (52.737) |
683.478 (6.735) |
5‘408.329 (52.437) |
父类方法调用 |
0.004 (0.001) |
0.004 (0.001) |
0.021 (0.001) |
0.025 (0.001) |
0.004 (0.001) |
与静态编译器类似,代码生成库在生成快速代码和快速生成代码之间面临着折衷。当在这些冲突的目标之间进行选择时,Byte Buddy 的主要侧重点在于以最少的运行时生成代码。通常,类型创建或操作不是任何程序中的常见步骤,并不会对任何长期运行的应用程序产生重大影响;特别是因为类加载或类构建(class instrumentation)是运行此类代码时最耗时且不可避免的步骤。
按照这个逻辑,D瓜哥觉得应该选择“生成快速代码”,毕竟很少生成而且只生成一次,但是生成的代码却可能运行多次。不过,考虑到 Java 虚拟机的优化,选择“生成快速代码”是否是更好的选择呢? |
上表中的第一个基准测试测量一个库在运行时子类化类,并且不实现或覆盖任何方法。这给我们一个库在代码生成时的一般开销的印象。在这个基准测试中,Java 代理执行得比其他库更好,这是因为存在着一种优化,假设总是扩展接口。Byte Buddy 还会检查类的泛型和注解类别,从而导致额外的运行时间。这个性能开销在创建类的其他基准中也是可见的。基准(2a)展示了运行时创建类,这个类实现了一个有 18 个方法的接口;(2b)显示为此类生成的方法的执行时间。类似地,(3a)显示了扩展类的基准,这个拥有相同的 18 种被实现的方法。 Byte Buddy 提供了两个基准测试,因为对于总是执行超类方法的拦截器来说,可能的优化是可能的。除了在类创建期间花费一段时间,Byte Buddy 创建类的执行时间通常达到基线,这意味着构建根本不会产生开销。应该注意的是,如果元数据处理被禁用,则在类创建期间,Byte Buddy 也会胜过任何其他代码生成库。由于代码生成的运行时间与程序的总运行时间相比微乎其微,所以这种性能优化是不可取的,因为它虽然获得了极少的性能,但却使库代码复杂很多。
最后,请注意,我们这些衡量 Java 代码性能的测试,都由 Java 虚拟机即时编译器优化过。如果你的代码只能偶尔执行,那么性能将会比上述表格指标略差。在这种情况下,你的代码并不是性能攸关的开始。这些性能测试代码与 Byte Buddy 一起发布,你可以在自己的计算机上运行这些指标,其中可能会根据你的机器的处理能力对上述数字进行涨跌。因此,不要绝对地解释上述数字,而是将它们视为不同库的对比方式。当进一步开发 Byte Buddy 时,我们希望监控这些指标,以避免在添加新功能时造成性能损失。
在下面的教程中,我们将会逐步说明 Byte Buddy 的功能。我们将从其更广泛的功能开始,这些功能最有可能被大多数用户使用。然后,我们将考虑越来越多的高级主题,并简要介绍 Java 字节码和类文件格式。即使你快速跳过这以后的材料,也不要灰心!你可以通过使用 Byte Buddy 的标准 API 来完成任何操作,而无需了解任何 JVM 规范。要了解标准 API,只需继续阅读。
创建一个类
任何一个由 Byte Buddy 创建的类型都是通过 ByteBuddy
类的实例来完成的。通过简单地调用 new ByteBuddy()
就可以创建一个新实例,然后就可以出发了。希望你使用一个集成开发环境,这样在调用一个给定实例的方法时就能得到相应的提示。这样,你的集成开发环境就会引导你完成相应的方法调用,防止手动在 Byte Buddy 文档中查阅某个类的 API。正如之前所说,Byte Buddy 提供了一个领域特定语言,这样就可以尽可能地提高人类可读性。集成开发环境的提示在大部分情况下会指引你到正确的方向。说的够多了,让我们在 Java 编程环境中创建第一个类吧:
DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
.subclass(Object.class)
.make();
正如前面所设想的,上面的示例代码会创建一个继承至 Object
类型的类。这个动态创建的类型与直接扩展 Object
并且没有实现任何方法、属性和构造函数的类型是等价的。你可能已经注意到,我们都没有命名动态生成的类型,通常在定义 Java 类时却是必须的。当然,你也可以很容易地明确地命名这个类型:
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()
创建实例,你就创建了整套的默认配置。通过调用在这个配置上的方法,你就可以根据你的需要来订制它。让我们试试:
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
实例时,一定要小心。你也许会犯下面的错误:
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瓜哥注
|
到目前为止,我们仅仅演示了如何使用 Byte Buddy 来创建已知类的子类。相同的 API 还可用于增强已有类。增加已有类有两种方式:
- 类型重定义(type redefinition)
-
当重定义一个类时,Byte Buddy 可以对一个已有的类添加属性和方法,或者删除已经存在的方法实现。如果使用其他的方法实现替换已经存在的方法实现,则原来存在的方法实现就会消失。例如,我们重定义下面这个类型class Foo { String bar() { return "bar"; } }
从
bar
方法返回"qux"
,那么该方法原来返回的"bar"
等信息就会都被丢失掉。 - 类型重定基底(type rebasing)
-
当重定基底一个类时,Byte Buddy 保存基底类所有方法的实现。当 Byte Buddy 如执行类型重定义时,它将所有这些方法实现复制到具有兼容签名的重新命名的私有方法中,而不是抛弃重写的方法。这样,就没有实现会被丢失。重定义的方法可以继续通过它们重命名过的名称调用原来的方法。通过这种方式,上述Foo
类就会被重定义成下面这个样子:class Foo { String bar() { return "foo" + bar$original(); } private String bar$original() { return "bar"; } }
原来返回
bar
的方法被保存到了另外一个方法里,因此还可以访问。当重定基底一个类时,Byte Buddy 对待所有方法定义就像你定义一个子类一样,例如,如果你想调用重定义方法的超类方法是,它会调用被重定义的方法。相反,它最终将这个假设的超级类别变成了上面显示的重定义类型。
任何重定基底、重定义或子类都是使用相同的 API 来执行,接口由 DynamicType.Builder
来定义。这样,可以将类定义为子类,然后更改代码来替换重定类。你只需要修改 Byte Buddy 领域特定语言的一个单词就能达到这个目的。这样,在定义的未来阶段,你就可以透明地切换任何一种方法:
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.Foo
和example.Bar
。如果我们将example.Foo
注入到一个现有的类加载器中,这个类加载器可能会尝试找到example.Bar
。然而,这种查找会失败,因为后一类是动态创建的,对于我们刚注入example.Foo
类的类加载器是不可访问的。因此,反射方法不能用于在类加载期间变得有效的循环依赖性的类。幸运的是,大多数 Java 虚拟机实现会在第一次主动使用时惰性地解析引用的类,这就是为什么类注入通常可以工作而没有这些限制。另外在实践中,由 Byte Buddy 创建的类通常不会受到这种循环的影响。
您可能会考虑到遇到循环依赖关系的可能性与您一次创建一个动态类型相关联。但是,动态创建类型可能会触发所谓的辅助类型的创建。这些类型由 Byte Buddy 自动创建,以提供对您正在创建的动态类型来访问。我们在下一节中详细了解辅助类型,现在不用担心。但是,由于这个原因,我们建议您通过创建一个特定的 ClassLoader
来加载动态创建的类,而不是将它们注入现有类。
- 创建
DynamicType.Unloaded
之后,可以使用ClassLoadingStrategy
加载此类型。如果没有提供这样的策略,Byte Buddy 会根据提供的类加载器推测出这样的策略,并为引导类加载器创建一个新的类加载器,其中不能使用反射注入类型,否则为默认值。Byte Buddy提供了几种类加载策略,其中每种都遵循上述概念之一。这些策略定义在ClassLoadingStrategy.Default
中,其中WRAPPER
策略创建一个新的包装ClassLoader
;CHILD_FIRST
策略创建一个类似于第一个子类优先的类加载器;INJECTION
策略使用反射注入动态类型。WRAPPER
和CHILD_FIRST
策略也可以在所谓的清单版本中使用,即使在加载类之后,类型的二进制格式也被保留。这些替代版本使得类加载器的类的二进制表示可以通过 `ClassLoader -
getResourceAsStream` 方法访问。但是,请注意,这需要这些类加载器来维护对类的完整二进制表示的引用,这将占用 Java 虚拟机堆上的空间。因此,如果您打算实际访问二进制格式,则应仅使用清单版本。由于INJECTION
策略通过反射工作,并且无法更改ClassLoader :: getResourceAsStream
方法的语义,因此它在清单版本中自然不可用。
我们来看看类加载的实际操作: 、
Class<?> type = new ByteBuddy()
.subclass(Object.class)
.make()
.load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
在上面的例子中,我们创建并加载了一个类。我们使用 WRAPPER
策略来加载适合大多数情况的类,就像我们之前提到的那样。最后,getLoaded
方法返回一个 Java Class
的实例,它就表示现在加载的动态类。
请注意,加载类时,通过应用当前执行上下文的 ProtectionDomain
来执行预定义的类加载策略。或者,所有默认策略通过调用 withProtectionDomain
方法来提供明确保护域的规范。使用安全管理员(security manager)或使用已签名的 Jar 中定义的类时,定义显式保护域很重要。
重新加载类
class Foo {
String m() { return "foo"; }
}
class Bar {
String m() { return "bar"; }
}
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"));
操作没有加载的类
package foo;
class Bar { }
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
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 应用中加载类
使用泛型类
属性和方法
String toString = new ByteBuddy()
.subclass(Object.class)
.name("example.Type")
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance() // Java reflection API
.toString();
String toString = new ByteBuddy()
.subclass(Object.class)
.name("example.Type")
.method(named("toString")).intercept(FixedValue.value("Hello World!"))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance()
.toString();
named("toString").and(returns(String.class)).and(takesArguments(0))
class Foo {
public String bar() { return null; }
public String foo() { return null; }
public String foo(Object o) { return null; }
}
Foo dynamicFoo = new ByteBuddy()
.subclass(Foo.class)
.method(isDeclaredBy(Foo.class)).intercept(FixedValue.value("One!"))
.method(named("foo")).intercept(FixedValue.value("Two!"))
.method(named("foo").and(takesArguments(1))).intercept(FixedValue.value("Three!"))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance();
深入细看一个固定值
new ByteBuddy()
.subclass(Foo.class)
.method(isDeclaredBy(Foo.class)).intercept(FixedValue.value(0))
.make();
委托方法调用
class Source {
public String hello(String name) { return null; }
}
class Target {
public static String hello(String name) {
return "Hello " + name + "!";
}
}
String helloWorld = new ByteBuddy()
.subclass(Source.class)
.method(named("hello")).intercept(MethodDelegation.to(Target.class))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance()
.hello("World");
class Target {
public static String intercept(String name) { return "Hello " + name + "!"; }
public static String intercept(int i) { return Integer.toString(i); }
public static String intercept(Object o) { return o.toString(); }
}
void foo(Object o1, Object o2)
void foo(@Argument(0) Object o1, @Argument(1) Object o2)
class MemoryDatabase {
public List<String> load(String info) {
return Arrays.asList(info + ": foo", info + ": bar");
}
}
class LoggerInterceptor {
public static List<String> log(@SuperCall Callable<List<String>> zuper)
throws Exception {
System.out.println("Calling database");
try {
return zuper.call();
} finally {
System.out.println("Returned from database");
}
}
}
MemoryDatabase loggingDatabase = new ByteBuddy()
.subclass(MemoryDatabase.class)
.method(named("load")).intercept(MethodDelegation.to(LoggerInterceptor.class))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance();
class LoggingMemoryDatabase extends MemoryDatabase {
private class LoadMethodSuperCall implements Callable {
private final String info;
private LoadMethodSuperCall(String info) {
this.info = info;
}
@Override
public Object call() throws Exception {
return LoggingMemoryDatabase.super.load(info);
}
}
@Override
public List<String> load(String info) {
return LoggerInterceptor.log(new LoadMethodSuperCall(info));
}
}
class ChangingLoggerInterceptor {
public static List<String> log(String info, @Super MemoryDatabase zuper) {
System.out.println("Calling database");
try {
return zuper.load(info + " (logged access)");
} finally {
System.out.println("Returned from database");
}
}
}
class Loop {
public String loop(String value) { return value; }
public int loop(int value) { return value; }
}
class Interceptor {
@RuntimeType
public static Object intercept(@RuntimeType Object value) {
System.out.println("Invoked method with: " + value);
return value;
}
}
interface Forwarder<T, S> {
T to(S target);
}
class ForwardingLoggerInterceptor {
private final MemoryDatabase memoryDatabase; // constructor omitted
public List<String> log(@Pipe Forwarder<List<String>, MemoryDatabase> pipe) {
System.out.println("Calling database");
try {
return pipe.to(memoryDatabase);
} finally {
System.out.println("Returned from database");
}
}
}
MemoryDatabase loggingDatabase = new ByteBuddy()
.subclass(MemoryDatabase.class)
.method(named("load")).intercept(MethodDelegation.withDefaultConfiguration()
.withBinders(Pipe.Binder.install(Forwarder.class)))
.to(new ForwardingLoggerInterceptor(new MemoryDatabase()))
.make()
.load(getClass().getClassLoader())
.getLoaded()
.newInstance();
调用超类方法
new ByteBuddy()
.subclass(Object.class)
.make()
new ByteBuddy()
.subclass(Object.class, ConstructorStrategy.Default.IMITATE_SUPER_TYPE)
.make()
调用默认方法
interface First {
default String qux() { return "FOO"; }
}
interface Second {
default String qux() { return "BAR"; }
}
new ByteBuddy(ClassFileVersion.JAVA_V8)
.subclass(Object.class)
.implement(First.class)
.implement(Second.class)
.method(named("qux")).intercept(DefaultMethodCall.prioritize(First.class))
.make()
调用特定方法
public class SampleClass {
public SampleClass(int unusedValue) {
super();
}
}
new ByteBuddy()
.subclass(Object.class, ConstructorStrategy.Default.NO_CONSTRUCTORS)
.defineConstructor(Arrays.<Class<?>>asList(int.class), Visibility.PUBLIC)
.intercept(MethodCall.invoke(Object.class.getDeclaredConstructor()))
.make()
访问属性
class UserType {
public String doSomething() { return null; }
}
interface Interceptor {
String doSomethingElse();
}
interface InterceptionAccessor {
Interceptor getInterceptor();
void setInterceptor(Interceptor interceptor);
}
interface InstanceCreator {
Object makeInstance();
}
Class<? extends UserType> dynamicUserType = new ByteBuddy()
.subclass(UserType.class)
.method(not(isDeclaredBy(Object.class)))
.intercept(MethodDelegation.toField("interceptor"))
.defineField("interceptor", Interceptor.class, Visibility.PRIVATE)
.implement(InterceptionAccessor.class).intercept(FieldAccessor.ofBeanProperty())
.make()
.load(getClass().getClassLoader())
.getLoaded();
InstanceCreator factory = new ByteBuddy()
.subclass(InstanceCreator.class)
.method(not(isDeclaredBy(Object.class)))
.intercept(MethodDelegation.construct(dynamicUserType))
.make()
.load(dynamicUserType.getClassLoader())
.getLoaded().newInstance();
class HelloWorldInterceptor implements Interceptor {
@Override
public String doSomethingElse() {
return "Hello World!";
}
}
UserType userType = (UserType) factory.makeInstance();
((InterceptionAccessor) userType).setInterceptor(new HelloWorldInterceptor());
杂项
注解
@Retention(RetentionPolicy.RUNTIME)
@interface RuntimeDefinition { }
class RuntimeDefinitionImpl implements RuntimeDefinition {
@Override
public Class<? extends Annotation> annotationType() {
return RuntimeDefinition.class;
}
}
new ByteBuddy()
.subclass(Object.class)
.annotateType(new RuntimeDefinitionImpl())
.make();
new ByteBuddy()
.subclass(Object.class)
.annotateType(new RuntimeDefinitionImpl())
.method(named("toString"))
.intercept(SuperMethodCall.INSTANCE)
.annotateMethod(new RuntimeDefinitionImpl())
.defineField("foo", Object.class)
.annotateField(new RuntimeDefinitionImpl())
类型注解
Byte Buddy 暴露并编写了类型注解,它们被引入到 Java 8,并成为其中的一部分。
属性附加器
class AnnotatedMethod {
@SomeAnnotation
void bar() { }
}
new ByteBuddy()
.subclass(AnnotatedMethod.class)
.method(named("bar"))
.intercept(StubMethod.INSTANCE)
.attribute(MethodAttributeAppender.ForInstrumentedMethod.INSTANCE)
定制化仪表
LDC 10 // stack contains 10
LDC 50 // stack contains 10, 50
IADD // stack contains 60
IRETURN // stack is empty
12 00 01
12 00 02
60
AC
enum IntegerSum implements StackManipulation {
INSTANCE; // singleton
@Override
public boolean isValid() {
return true;
}
@Override
public Size apply(MethodVisitor methodVisitor,
Implementation.Context implementationContext) {
methodVisitor.visitInsn(Opcodes.IADD);
return new Size(-1, 0);
}
}
enum SumMethod implements ByteCodeAppender {
INSTANCE; // singleton
@Override
public Size apply(MethodVisitor methodVisitor,
Implementation.Context implementationContext,
MethodDescription instrumentedMethod) {
if (!instrumentedMethod.getReturnType().asErasure().represents(int.class)) {
throw new IllegalArgumentException(instrumentedMethod + " must return int");
}
StackManipulation.Size operandStackSize = new StackManipulation.Compound(
IntegerConstant.forValue(10),
IntegerConstant.forValue(50),
IntegerSum.INSTANCE,
MethodReturn.INTEGER
).apply(methodVisitor, implementationContext);
return new Size(operandStackSize.getMaximalSize(),
instrumentedMethod.getStackSize());
}
}
enum SumImplementation implements Implementation {
INSTANCE; // singleton
@Override
public InstrumentedType prepare(InstrumentedType instrumentedType) {
return instrumentedType;
}
@Override
public ByteCodeAppender appender(Target implementationTarget) {
return SumMethod.INSTANCE;
}
}
abstract class SumExample {
public abstract int calculate();
}
new ByteBuddy()
.subclass(SumExample.class)
.method(named("calculate"))
.intercept(SumImplementation.INSTANCE)
.make()
创建自定义分配器
enum ToStringAssigner implements Assigner {
INSTANCE; // singleton
@Override
public StackManipulation assign(TypeDescription.Generic source,
TypeDescription.Generic target,
Assigner.Typing typing) {
if (!source.isPrimitive() && target.represents(String.class)) {
MethodDescription toStringMethod = new TypeDescription.ForLoadedType(Object.class)
.getDeclaredMethods()
.filter(named("toString"))
.getOnly();
return MethodInvocation.invoke(toStringMethod).virtual(sourceType);
} else {
return StackManipulation.Illegal.INSTANCE;
}
}
}
new ByteBuddy()
.subclass(Object.class)
.method(named("toString"))
.intercept(FixedValue.value(42)
.withAssigner(new PrimitiveTypeAwareAssigner(ToStringAssigner.INSTANCE),
Assigner.Typing.STATIC))
.make()
创建自定义参数绑定器
@Retention(RetentionPolicy.RUNTIME)
@interface StringValue {
String value();
}
enum StringValueBinder
implements TargetMethodAnnotationDrivenBinder.ParameterBinder<StringValue> {
INSTANCE; // singleton
@Override
public Class<StringValue> getHandledType() {
return StringValue.class;
}
@Override
public MethodDelegationBinder.ParameterBinding<?> bind(AnnotationDescription.Loaded<StringValue> annotation,
MethodDescription source,
ParameterDescription target,
Implementation.Target implementationTarget,
Assigner assigner,
Assigner.Typing typing) {
if (!target.getType().asErasure().represents(String.class)) {
throw new IllegalStateException(target + " makes illegal use of @StringValue");
}
StackManipulation constant = new TextConstant(annotation.loadSilent().value());
return new MethodDelegationBinder.ParameterBinding.Anonymous(constant);
}
}
class ToStringInterceptor {
public static String makeString(@StringValue("Hello!") String value) {
return value;
}
}
new ByteBuddy()
.subclass(Object.class)
.method(named("toString"))
.intercept(MethodDelegation.withDefaultConfiguration()
.withBinders(StringValueBinder.INSTANCE)
.to(ToStringInterceptor.class))
.make()
附录 A: 版权声明
Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don‘t include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
原文地址:https://www.cnblogs.com/sea520/p/12230802.html