Java Bean验证是一种反模式 - Code-Held


使用Bean验证是一种反模式的做法。它隐藏了与业务相关的约束,它将验证发生时的选择权留给了其他框架代码,我甚至看到了这样的情况:开发者期望验证 "必须发生",但它从未发生。
当然,也没有对其进行测试。而说到测试:测试这些与业务相关的约束条件也是很痛苦的。
下面是 Spring/JPA代码:

@Entity
class AppUser(
    @Email
    var email: String,
) {
    @Id
    val id: UUID = UUID.randomUUID()
}

业务约束是只允许AppUser使用有效的电子邮件地址。
@Email注解是javax.validation包的一部分,它指示验证器检查该属性是否是一个有效的电子邮件地址。
但当我们运行这段代码时,没有抛出任何异常:

@Service
class AppUserService(private val userRepository: AppUserRepository) {
    @EventListener(ApplicationStartedEvent::class)
    fun createUser() {
        userRepository.save(AppUser("This is not an email address"))
    }
}

因此,我们最终得到了一个业务需求--只有有效的电子邮件地址应该被存储--作为代码的读者,我们也期望这是事实,但实际上什么也没有发生。
当然,也没有测试来检查这个要求是否得到满足。
 
测试是困难的
说到测试--测试是很难的。由于你把验证你的业务需求的责任转移到了框架的 "某个 "部分,所以你不能写一个单元测试。你必须写一个集成测试来检查你的验证是否达到了你的期望。这由于很多原因是不好的。特别是由于集成测试很重。而且为了这样一个小需求而启动你的应用程序是一种矫枉过正的做法。
 
Kotlin和Bean验证
但我并没有回答为什么不进行上述检查。与Kotlin结合的问题是,我们没有注解支持属性,而是注解了构造参数。为了解决这个问题,我们实际上需要用@field:Email来注解这个属性。在我看来,这个例子变得更糟糕了。我们没有保护我们的代码不使用无效的数据来构建实体,我们甚至建立了读者的期望,即这只能包含有效的电子邮件地址,但这实际上从未被检查。当然,我们也从未测试过它。

以上所有的思考让我得出了一个结论:

完全不要使用Java Bean Validation!
 

这个JSR是不必要的。验证并不是我们应该隐藏的东西。我们验证我们的数据是因为它可以检查某些业务约束。这就是业务代码。它属于我们应用程序的主流程。我们不应该把它移到一些神奇的库中,而依赖于框架。我们甚至不能通过使用这些注解来节省很多。大部分的检查都是超级简单的if语句。而这正是它应该做的!

相反,在构造函数中做你的验证。你的代码必须不允许用无效的数据构造一个对象。所以上面的实体我会这样改造。

@Entity
class AppUser(
    var email: String,
) {
    @Id
    val id: UUID = UUID.randomUUID()

    init {
        if (!EmailValidator.isValid(email)) {
            throw IllegalArgumentException("Email address is invalid")
        }
    }
}

它很容易阅读,很简单,而且从运行时的角度来看,是不含糊的。为这个约束条件写一个单元测试也是不费吹灰之力的。
Java Bean验证是一种反模式!