Bean验证反模式 - reflectoring.io


Bean验证是在Java生态系统中实施验证逻辑的事实上的标准,它是一个很好的工具。
但是,在最近的项目中,我对Bean验证进行了更深入的思考,并确定了一些我认为是反模式的实践。

反模式免责声明
就像每一次关于模式和反模式的讨论一样,都涉及一些观点和个人经验。在一种情况下使用反模式很可能是在另一种情况下的最佳实践(反之亦然),因此,请不要将下面的讨论视为宗教规则,而应将其作为对该主题进行思考和进行建设性讨论的触发点。

反模式1:仅在持久层中进行验证
使用Spring,在持久层中设置Bean验证非常容易。假设我们有一个带有一些bean验证批注的实体以及一个关联的Spring Data存储库:

@Entity
public class Person {

  @Id
  @GeneratedValue
  private Long id;

  @NotEmpty
  private String name;

  @NotNull
  @Min(0)
  private Integer age;

  // getters and setters omitted

}

public interface PersonRepository extends CrudRepository<Person, Long> {

  // default CRUD methods provided by CrudRepository

}

只要我们在类路径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提供了称为验证组的功能。此功能使我们可以将验证注释与某些组相关联,以便我们可以选择要验证的组:

public class Person {

  @Null(groups = ValidateForCreate.class)
  @NotNull(groups = ValidateForUpdate.class)
  private Long id;

  @NotEmpty
  private String name;

  @NotNull
  @Min(value = 18, groups = ValidateForAdult.class)
  @Min(value = 0, groups = ValidateForChild.class)
  private int age;

  // getters and setters omitted

}

当一个Person创建时,Id可以为空,但是修改时,不能为空。

首先,我们故意违反“单一责任原则”。
其次,它很难阅读。
我提议不使用验证组。
特定于用例的语义使用代码验证,并且模型代码不依赖于用例。业务规则使用代码实现,成为“丰富”充血领域模型的一部分,并且可以通过查询方法进行访问。

有意识地验证
Bean验证是一个触手可及的好工具,但好的工具会带来很大的责任感(听起来有些陈词滥调,但是如果您问我的话,这很重要)。
我们应该有一个清晰的验证策略,告诉我们在哪里进行验证以及何时使用哪种工具进行验证,而不是对所有内容都使用Bean验证并在各处进行验证。
我们应该将句法验证与语义验证分开。语法验证是Bean验证批注支持的声明式样式的完美用例,而语义验证在纯代码中更易读。