Bean验证是在Java生态系统中实施验证逻辑的事实上的标准,它是一个很好的工具。
但是,在最近的项目中,我对Bean验证进行了更深入的思考,并确定了一些我认为是反模式的实践。
反模式免责声明
就像每一次关于模式和反模式的讨论一样,都涉及一些观点和个人经验。在一种情况下使用反模式很可能是在另一种情况下的最佳实践(反之亦然),因此,请不要将下面的讨论视为宗教规则,而应将其作为对该主题进行思考和进行建设性讨论的触发点。
反模式1:仅在持久层中进行验证
使用Spring,在持久层中设置Bean验证非常容易。假设我们有一个带有一些bean验证批注的实体以及一个关联的Spring Data存储库:
@Entity <b>public</b> <b>class</b> Person { @Id @GeneratedValue <b>private</b> Long id; @NotEmpty <b>private</b> String name; @NotNull @Min(0) <b>private</b> Integer age; <font><i>// getters and setters omitted</i></font><font> } </font>
<b>public</b> <b>interface</b> PersonRepository <b>extends</b> CrudRepository<Person, Long> { <font><i>// default CRUD methods provided by CrudRepository</i></font><font> } </font>
只要我们在类路径classpath上有一个像Hibernate Validator这样的bean验证实现库包,每次调用存储库方法save()时,都会触发一次验证。如果传入对象根据bean验证注释判断是无效的,将抛出ConstraintViolationException
持久层是验证的正确地方吗?
我认为至少它不是唯一可以验证的地方。
在常见的Web应用程序中,持久层是最底层。我们通常在上面有一个业务层和一个Web层。数据通过业务层流入Web层,最后到达持久层。
如果仅在持久层中进行验证,那么我们将承担Web和业务层使用无效数据的风险!
无效的数据可能会导致业务层中的严重错误(如果我们希望业务层中的数据有效)或导致超级防御性的编程,并且需要在整个业务层中进行手动验证检查(一旦我们了解到其中的数据业务层不能被信任)。
总之,对业务层的输入应该已经有效。这样,在持久层中的验证就可以充当附加的安全网,但不是唯一的验证位置。
反模式2:验证太多
除了验证得过少之外,我们会验证得太多。这不是特定于Bean验证的问题,而是通常具有验证功能都会具有的问题。
在通过Web层进入系统之前,使用Bean验证对数据进行验证。Web控制器将传入的数据转换为可以传递给业务服务的对象。业务服务不信任Web层,因此它使用Bean验证再次验证该对象。
在执行实际的业务逻辑之前,业务服务将以编程方式检查我们能想到的每个约束,以便绝对不会出错。最后,持久层在将数据存储到数据库之前再次对其进行验证。
这好像是一种不错的防御性验证方法,但它带来的问题多于我的经验。
首先,如果我们在很多地方使用Bean验证,那么到处都会有Bean验证注释。如有疑问,我们将向对象添加Bean验证批注,即使它毕竟可能不会得到验证。最后,我们花时间在添加和修改可能根本不执行的验证规则上。
其次,到处进行验证会导致意图明确但最终导致错误的验证规则。
想象一下,我们正在验证一个人的名字和姓氏,以使其至少包含三个字符。这不是必需的,但是无论如何我们都添加了此验证,因为在我们的环境中,不验证是不礼貌的。有一天,我们会收到一个错误报告,称一个名为“ Ed Sheeran”的人未能在我们的系统中注册,并且刚刚在推特上引发了一场狗屎风暴。
第三,到处验证会减慢开放速度。
如果我们在整个代码库中散布了验证规则,其中一些在Bean验证批注中,而另一些在纯代码中,则其中的某些可能会妨碍我们正在构建的新功能。但是我们不能仅仅删除那些验证,毕竟,有人将它们放在那里一定有道理。我们放慢了脚步,因为我们必须仔细考虑每个验证,然后才能应用更改。
最后,由于验证规则遍及整个代码,如果遇到意外的验证错误,我们将不知道在哪里寻找解决方案。
简而言之,我们应该有一个明确而集中的验证策略,而不是在任何地方验证所有内容。
反模式3:使用验证组进行用例验证
Bean验证JSR提供了称为 验证组 的功能。此功能使我们可以将验证注释与某些组相关联,以便我们可以选择要验证的组:
<b>public</b> <b>class</b> Person { @Null(groups = ValidateForCreate.<b>class</b>) @NotNull(groups = ValidateForUpdate.<b>class</b>) <b>private</b> Long id; @NotEmpty <b>private</b> String name; @NotNull @Min(value = 18, groups = ValidateForAdult.<b>class</b>) @Min(value = 0, groups = ValidateForChild.<b>class</b>) <b>private</b> <b>int</b> age; <font><i>// getters and setters omitted</i></font><font> } </font>
当一个Person创建时,Id可以为空,但是修改时,不能为空。
首先,我们故意违反“单一责任原则”。
其次,它很难阅读。
我提议不使用验证组。
特定于用例的语义使用代码验证,并且模型代码不依赖于用例。 业务规则使用代码实现,成为“丰富”充血领域模型的一部分,并且可以通过查询方法进行访问。
有意识地验证
Bean验证是一个触手可及的好工具,但好的工具会带来很大的责任感(听起来有些陈词滥调,但是如果您问我的话,这很重要)。
我们应该有一个清晰的验证策略,告诉我们在哪里进行验证以及何时使用哪种工具进行验证,而不是对所有内容都使用Bean验证并在各处进行验证。
我们应该将句法验证与语义验证分开。语法验证是Bean验证批注支持的声明式样式的完美用例,而语义验证在纯代码中更易读。