仅仅几天又有一个丰富的版本发布

在前一个版本发布后的仅仅几天,我们发布了旗舰项目的新版本 5.4.12.Final

加速发布时间的原因是我们前一个版本中有一个回归:新的增强实体扫描器如果在任何地方有 module-info.class 文件将会失败;这让我们感到尴尬,以至于立即发布了修复。

这让我想到我们没有介绍前一个版本中的某些增强功能;除此之外,我们还引入了一个有趣的新 hibernate-graalvm 项:让我来告诉你这些。

基于Jandex的实体扫描器

当Hibernate ORM启动时,你可以显式地提供映射实体列表,或者我们需要找到带有任何映射注解的对象,例如 @Entity

这个内部服务由 org.hibernate.boot.archive.scan.spi.Scanner 的实现提供:一些框架和容器在需要特别控制扫描过程时会插件自己的实现,但我们也有一个默认实现。

任何扫描器实现都需要快速:在类路径上搜索所有对象可能需要很长时间,具体取决于你如何做。

直到Hibernate ORM 5.4.10.Final,我们提供的默认实现是基于Javassist的,因为它提供了一个高效的解决方案;这个实现为我们服务了很长时间,但它需要Javassist,而我们在努力限制我们的依赖。当默认的字节码增强器实现是Javassist时,这并不是什么大问题,但自从我们切换到Byte Buddy之后,它也需要在类路径上,仅为此一个用例,感觉就像是一个不必要的负担。

公平地说,确保这些库定期更新以兼容快速发展的JDK版本,也给团队带来了一定的负担:减少我们依赖的这类库的数量,应该有助于我们维护所有这些库的能力。

那么,为什么选择Jandex呢?

Jandex是一个专注于扫描的小型库,强调效率;我们在其他环境中已经成功使用过它,所以它是我们更新扫描器实现的理想候选。因此,Javassist现在成为一个可选依赖项:这是Hibernate 6最终移除它的又一小步。

Jandex还有一个好处:它允许将索引存储在jar文件中。您可以考虑将Jandex构建时间插件添加到您的项目中以利用这一优势。

GraalVM原生镜像的元数据

可以使用GraalVM的native-image工具将Java应用程序编译成原生代码;对于大多数Java代码来说,这很简单,但当你使用Hibernate ORM等库时,它就会变得相当复杂。

我在Devoxx和QCon等会议中详细讨论了这些挑战;如果您想了解细节,一些这些会议的内容在youtube上是免费提供的。但最好先阅读官方文档中的简介!

简而言之,编译器需要完全分析可能的执行路径,才能应用一些出色的优化,包括移除所有它可以证明在运行时实际上不需要的符号。这很好,因为它显著减小了二进制文件的大小,并且通常还会改善运行时性能,例如,数据结构变得更小,分支更少,需要考虑的多态调用类型更少,等等。当您有“正常”的代码流时,运行此分析是完全自动的,但当所讨论的库期望能够在运行时生成或修改字节码,或者甚至只是使用反射时,编译器就再也无法确定哪些符号是安全的可以移除的,需要一点明确的帮助。

幸运的是,告诉它您打算反射调用的方法相当直接;有多种选项,包括一个简单的json文件。有关更多信息,请参阅反射

但是,库能否包含这样的元数据呢?

这就变得复杂了。Hibernate ORM可以包含一个列表,但这是一个静态列表,这引发了一系列问题。

例如,它是否应该包括支持UUID身份生成器的代码?

如果它包括,那么这些符号将包含在依赖此元数据的任何应用程序中——包括那些不使用UUID作为标识符的应用程序。您可能认为这是一个小代价,但您会感到惊讶:考虑原生代码需要包含任何您的类所依赖的所有代码;在这种情况下,这意味着还需要包含大量允许JDK生成UUID实例的代码:支持随机数生成器,甚至一些加密代码。

如果我们决定不在这样的列表中包含UUID身份生成器的支持,那么用户仍然可以选择显式将其添加到自己的json文件中;这当然从效率优化的角度来看是更好的,但不够方便。

这只是一个单一的身份生成器!很容易看出,错误的方法会导致巨大的开销,当问题扩展到所有内部组件的选择和排列时。

最佳解决方案是什么?显然,这可以自动化:消除最终用户的麻烦,并且应用程序可以处理更大范围的组合。Hibernate ORM足够复杂,但问题更严重:在实际编译应用程序时,您需要创建一组编译器标志,以满足所有代码及其依赖项。

这种数量和复杂性使得即使是人类也很难正确管理所有内容,即使给出精确的指示。

Quarkus中,Hibernate ORM扩展将简单地分析应用程序代码,并确定Hibernate ORM提供的许多内部组件中哪些实际上是必需的;它更进一步,通过针对这些组件将要使用的特定配置进行优化;基于扩展的架构确保每个模块对其支持的每个库都有深入的专业知识,同时由一个中央核心协调,帮助生成一组连贯的元数据和编译器标志。

然后,将此自动、定制的构建元数据馈送到GraalVM的native-image编译器,生成完美的图像:轻量级、快速且功能强大。

尽管这个解决方案非常优雅,但其适用范围仅限于基于Quarkus的应用程序。如果您想从命令行构建自己的本地图像,以便试验GraalVM提供的各种选项呢?

公平地说,这并不容易,但我们可以尝试让它更简单。

一个问题可能是,Hibernate ORM可能还需要在运行时生成代理(取决于配置),并且很可能需要在您的代码上运行一些反射操作:至少它需要使用反射在映射类上以了解它们具有哪些属性、哪些注释。我没有这个问题的解决方案,但如果是有人想这样做,或者只是手动列出域模型作为在json文件中注册以供反射访问的附加类型:可能有点繁琐,但肯定可行。

将用户域模型添加到列表中,选择一些内部组件,我认为这是“动态元数据”需要做的事情:因为它们随着应用程序的不同而不同。还有一个“静态列表”:独立于您的配置和模型,您始终需要注册Hibernate的一些关键内部类。

明确地说,我是创建Quarkus的初始研发团队之一:其大部分设计源于在保持使用简单的前提下解决这些问题的需求;所以,如果您觉得这一切“只需工作”且无需过多麻烦,我的建议是使用Quarkus,因为我不清楚还有其他什么方法可以解决,例如,在运行时生成代理的需求。我们很高兴讨论此类方面的更多细节,因为我们都希望这些解决方案进一步发展,但请记住,这绝对是为高级用户、研究人员和框架开发者准备的。

介绍hibernate-graalvm

为了帮助人们探索此类问题的替代解决方案,我决定从Quarkus中提取“静态元数据”,并将其作为Hibernate ORM核心jar的伴随jar发布。

请随意将此依赖项添加到您的项目中

<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-graalvm</artifactId>
   <version>5.4.12.Final</version>
</dependency>

如果您不使用native-image进行编译,此工件将没有任何用处;好消息是它无害。但是,如果您要实验GraalVM,编译器将自动将其拾取并注册所有需要通过反射实例化的内部组件。

一句忠告:由于基于我们目前的知识和从Quarkus及其社区中获得的经验,此列表可能不完整;它肯定遗漏了您的实体,所以请将其视为有用的起点,但不要将其视为详尽的列表。

希望这能让更多人更容易地进行实验!

发布变更日志

您可以在此处找到本版本的完整更改列表。

获取Hibernate ORM 5.4.12.Final

所有详细信息均可在hibernate.org上的专用页面上找到,并且是最新的。

反馈、问题、想法?

要联系,请使用常规渠道


返回顶部