前言

本文档是 D瓜哥 在学习 Byte Buddy Tutorial 时随手翻译的。水平有限,难免失误,欢迎随时发生 PR。谢谢!

友情支持

如果您觉得这个笔记对您有所帮助,看在 D瓜哥 码字的辛苦上,请友情支持一下。屌丝逆袭,赢取白富美,走向人生巅峰就靠这个了,😜 😜

支付宝

微信

官网及版本库

本文档的版本库使用 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瓜哥注

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

到目前为止,我们仅仅演示了如何使用 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.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 策略也可以在所谓的清单版本中使用,即使在加载类之后,类型的二进制格式也被保留。这些替代版本使得类加载器的类的二进制表示可以通过 `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.