正如您所知,Bean Validation 2.0 已于几个月前发布,而 Hibernate Validator 6.0 是其参考实现。
Hibernate Validator 并非唯一一个 Bean Validation 实现,我们还有一个(友好的)竞争对手,称为 Apache BVal。
Apollo BVal 尚未赶上 Bean Validation 2.0,但根据Bean Validation 领域的最后一次基准测试是在 2010 年(还记得“Machete don’t text”吗?),我认为是时候重新审视这个基准测试并获取一些新的数据。
特别是我们对 6.0 版本进行的所有优化工作。
参赛者
我们的想法是比较各种 Bean Validation 实现,并展示 Hibernate Validator 6.0 的进步。
我们决定基准测试 3 个实现
-
Hibernate Validator 6.0.4.Final(发布于 2017 年 10 月 25 日)
-
Hibernate Validator 5.4.2.Final(发布于 2017 年 10 月 19 日)
-
Apollo BVal 1.1.2(发布于 2016 年 11 月 3 日)
Hibernate Validator 5.4 和 Apollo BVal 1.1 是 Bean Validation 1.1 的实现,因此在这个基准测试中,我们不会测试 Bean Validation 2.0 的新功能,而只是测试两个规范版本共有的功能。
请注意,Bean Validation 2.0 只添加新功能,不会删除现有的任何功能。
基准测试
单元基准测试
在 Hibernate Validator 中,我们维护一系列 JMH 基准测试,我们可以针对 Bean Validation 实现的各种版本运行这些测试。
这些基准测试非常简单:它们不执行复杂的验证,因为整个目的是测量验证引擎的开销。
对于这个基准测试系列,我们将运行两个不同的基准测试
-
SimpleValidation:我们只是测试了一个具有几个约束的简单Bean的验证。没有太多花哨的地方,只是普通的Bean验证。
-
CascadedValidation:这里的想法是测试级联验证的开销 - Bean只有一个非常简单的约束,但会级联到几个同类型的其他Bean。
结论
Hibernate Validator比以往任何时候都要快:6.0.4.Final比5.4.2.Final和Apache BVal快2到3倍
,无论是在单元基准测试还是在更实际的测试中。
两件更了不起的事情
-
在更现实的基准测试中,结果甚至更好。
-
在这个过程中,我们还减少了Hibernate Validator的内存占用。
最终,Hibernate Validator 6.0是一个非常推荐的升级,特别是如果你大量使用Bean验证(例如在批处理中)。
我们所做的更改的一些示例
关于我们做出的性能改进,这里有两个示例
-
初始化的验证器被缓存在全局映射中,并从那里获取。我们保留了此缓存,因为它在你在两个位置之间共享验证器时很有用(例如,当你在不同的属性上有
@NotNull
约束时),但我们现在也在ConstraintTree
中保留了对验证器的引用,从而避免了映射查找。 -
获取注释的属性相当慢,所以我们现在构建一个存储这些属性一次并一直保存在手边的
AnnotationDescriptor
。上述映射的键是使用注释及其属性,即使使用hashCode()
缓存,我们也有相当大的开销。
请注意,这两个更改单独就带来了30%的速度提升。
内存占用的减少不是这篇博客的主题,但这里有一些我们为了减少Hibernate Validator存储的元数据的大小所做的事情
-
尽可能减少集合到空/单例集合 - 这在我们的情况下不是微不足道的,因为我们有相当多的这些。
-
创建静态默认实例以管理默认情况 - 在大多数情况下,您最终会得到一个不包含特定于情况信息的相同对象,识别这种默认情况并重用相同的实例是非常有价值的;
-
优化非约束性bean/方法/属性的元数据。
请注意,我们的元数据不可变对于优化有很大帮助。
经验总结
所以,就像任何性能改进工作一样,我们学到了一些经验教训
-
这是一段有趣的旅程!
-
创建基准测试,衡量,衡量,再衡量:做某事最快的方法可能不是你所想的。
-
在不同场景下进行衡量:有时你在某个地方改善了情况,但它在其他地方变得更糟。
-
反射很慢 - 这不是什么新鲜事 - 但请注意,即使看起来最无害的操作也可能比预期的慢(通常
Method.getParameters()
不是一个简单的getter,它有成本)。 -
在热点路径上,即使是最轻微的实例创建也可能引入一些不希望的开销。
-
映射查找的成本远非微不足道。即使缓存了
hashCode()
。即使你的equals()
很快。 -
大量的微观优化可以带来巨大的改进。
-
在引入新功能时,在早期阶段对后果进行一些原始的衡量可能是一个好主意。有时,为了非常狭窄的使用案例添加的功能会引入很多开销或使后续优化变得非常复杂。
我们取得了很大的进步,但我们仍然有一个大问题尚未解决:在验证阶段,我们创建了大量的 PathImpl
和 NodeImpl
实例,这种情况显然应该得到改善。不幸的是,这并不像听起来那么简单。
引导成本与运行时成本
Bean Validation实现必须收集被验证bean的大量元数据。这通常在引导时完成,以避免在运行时产生这种开销。
在这个基准测试中,我们只关注运行时成本,因为在应用程序的生命周期中,引导成本通常是微不足道的。
为了公平起见,根据我们的观察,Hibernate Validator在引导时通常比Apache BVal慢一些,因为它收集了更多信息。但在实际场景中,您甚至可能不会注意到这一点。