Java 字节码工程库 Byte Buddy 最新版本完全支持 Java 11 以及自 Java 8 以来引入的所有类文件和字节码新特性。其中包括新的 ConstantDynamic(有时称为 condy)特性和 Java 11 Nestmates 。
InfoQ 采访了 ByteBuddy 的作者 Rafael Winterhalter,以了解更多信息。
InfoQ:感谢你抽出宝贵时间与我们交谈。你能先介绍一下 Byte Buddy 是什么吗?它有哪些功能?可以用于什么样的应用程序?这个项目有多长时间了?开发人员该如何开始使用它?
Rafael Winterhalter: Byte Buddy 是一个代码生成库,通过简单的 Java API 定义新类或修改现有类。这个库会生成并操作 Java 字节码。通过处理字节码,它可以与使用任何 JVM 语言编写的代码进行交互,并且可以在 Java 应用程序的运行期间使用该库来修改当前要执行的代码,甚至包括自己的代码。
Byte Buddy 主要被用在其他库和框架中。例如,Hibernate 使用 Byte Buddy 来实现实体代理,Mockito 使用它来生成模拟类。Byte Buddy 也渐渐被用于开发可以改变整个应用程序行为的 Java 代理。APM 工具(例如 Instana)正在使用这类代理收集应用程序运行期间的度量指标。
我从 2014 年开始开发 Byte Buddy,并在 2015 年发布了第一个非 beta 版本。从那以后,它开始获得相当多的关注。现在,这个库每年将近有一亿次的下载量。
要想使用这个库,只需将它添加到项目中,然后使用它的 DSL 生成类。Byte Buddy 的 GitHub 页面和官网都提供了如何创建简单类的示例。网页上还提供了综合性文档,还有很多博客文章、YouTube 和 Vimeo 上的视频资料。
InfoQ:我们来谈谈新版本。最近增加了什么新特性?有没有用户特别要求的新特性?
Winterhalter:在最新的版本中,我一直在尝试支持 Java 11 和 12。不过我的大部分时间都用于添加对 Java 模块系统的支持,这是一条漫漫长路。随着最新版本的发布,对模块系统的支持变得更加稳定,Byte Buddy 甚至会在即将发布的 1.9.0 版本中添加一个 module-info.class。同时,这个库保留了对 Java 6、7 和 8 的兼容性。
用户的特性请求通常包括对较新 Java 版本的支持,因为缺乏对新版本的支持通常会影响项目在较新的 VM 上编译。类似地,还有很多特性请求要求支持 Kotlin 或 Scala 等 JVM 语言,这些语言在字节码转换方面有一些特别之处。如果 Java 语言正在添加新特性,那么就要十分小心,并且通常需要通过扩展 JVM 的功能来实现。其他语言有时会尝试模拟特定的行为,否则 Byte Buddy 会出现问题。
InfoQ:让我们来聊聊新的发布周期——作为一个工具开发者,更快的发布速度和类文件格式变更对你产生了哪些影响?
Winterhalter:对于每个 Java 版本,Byte Buddy 都需要根据 Java 类文件格式的变化进行调整。通常这些变化是很微小的,但也可能很复杂。例如,当 Java 8 发布时,Byte Buddy 必须支持接口的默认方法。这看起来像是一个很小的变化,但它需要进行大量的重构。随着 Java 发布速度的加快,我的工作变得有点令人厌烦,但我不能抱怨,因为这个项目是我当前职业生涯的焦点。
到目前为止我遇到的一个问题是,因为很多库依赖了 Byte Buddy,所以每次新的 Java 主要版本发布后,我都会被要求赶快为他们提供支持。如果我不做出更新,依赖 Byte Buddy 的库就不能使用新的 Java 版本,这阻碍了其他维护者采用 Byte Buddy。与此同时,像 Maven 这样的工具通常也需要一些时间才能支持新的 Java 版本,这也使得我很难快速做出更新。半年时间很快就过去了。但我觉得其他工具开发者已经适应了这种变化。与先前的版本相比,支持 Java 10 到 Java 12 要容易得多,因为每个版本中包含的变更更少了。
InfoQ:Java 11 将 ConstantDynamic 作为一项新特性发布。为什么要这样?它的原理是什么?它与 InvokeDynamic 有关吗?如果有,那是怎样的一种关系?
Winterhalter:Java 开发人员通常使用 static final 关键字来定义常量。不过,Java 类文件中的常量可以是通过符号引用的非类字段。有几种类型已经通过这些符号来表示字面量,例如 Java 字符串。
有了 ConstantDynamic,就可以将任意变量表示为常量池中引用的常量值。使用类文件常量的主要好处是只在第一次使用时才创建,而不是在类加载时创建,而 static final 字段是在类加载时创建的。使用 ConstantDynamic,将来可以避免在 JVM 中进行大量的加载。例如,在 JVM 启动时,它必须初始化 Locale 类,而这个类引用了 JVM 支持的所有语言。这种初始化相当昂贵并且通常是不必要的,因为大多数程序只使用默认语言。通过使用 ConstantDyanmic,未来可以改进核心库,实现更快的 JVM 启动,当然其他库也可以做类似的事情。
截至今天,Java 或其他 JVM 语言还没有公开支持 ConstantDynamic。但因为 JVM 已经提供支持,所以 Byte Buddy 也已经能够在字节码中创建这样的动态常量。动态常量是通过引导(bootstrap)方法创建的,这个方法会返回常量值。在使用常量值的位置需要引用这个引导方法。
我在博文中详细介绍了 ConstantDynamic 和 InvokeDynamic ,以及如何在 Byte Buddy 中使用它们。InvokeDynamic 实际上和 ConstantDynamic 非常类似,区别在于它使用引导方法绑定方法调用而不是常量值。当然,Byte Buddy 也为它提供了 API。
InfoQ:自 Java 8 以来,类文件格式是否有其他重大变化?ByteBuddy 如何应对这些变化?
Winterhalter:除了 ConstantDynamic 之外,JVM 还引入了嵌套伴侣(nest mate)的概念,以便为嵌套类提供更好的方法访问控制。通过在两个类文件中将它们定义为嵌套伴侣,它们就可以获得调用对方私有方法的权限。以前,javac 编译器通过添加 package-private 访问器方法来实现嵌套类的私有方法调用。
Byte Buddy 支持嵌套伴侣,但目前 DSL 不允许更改或添加嵌套伴侣。这是一个比较大的特性,我想在今年晚些时候解决这个问题。
InfoQ:未来会怎样?你对目前路线图中即将推出的特性有何看法?
Winterhalter:目前,几乎所有即将推出的 Java 特性都让我兴奋不已。我最期待的是 Loom,它将提供对 JVM continuation 的原生支持。作为一名软件顾问,我参与的项目通常使用 actor 模型或反应式回调之类的抽象来实现并发。这些应用程序通常会越变越复杂,业务逻辑被埋没在模拟并发性的仪式代码中。这导致业务代码难以重构,因为它们更多地满足了技术需求却忽略了领域逻辑。我希望 Loom 能够让我们不需要在业务代码中显式地使用并发模型。
除了 Loom 之外,我也很期待看到对 Graal 编译器的扩展支持,我认为它是 JVM 能够保持其作为世界上最好运行时之一的重要基石。除了 JIT 编译之外,AOT 编译和原生镜像创建也将扩展 Java 在未来的使用范围。我也希望 Metropolis 项目能够有助于弥合大多数 Java 开发人员的知识与 JVM 的功能之间的一些差距。
InfoQ:你有其他意见或想法想与我们的读者分享的吗?
Winterhalter:我常常觉得一部分 Java 开发人员对甲骨文的管理持怀疑态度,他们觉得模块系统或新发布周期对甲骨文来说可能意味着更大的负担而不是好处。
我认为甲骨文最终解决了很多可能危及平台长期发展的问题。JVM 现在处在一个更好的状态,很多即将发生的变化都是基于这个基础。成为 Java 开发人员是一个激动人心的时刻,我们都有很多值得期待的事情。
评论