【技术分享】Bean Validation使用篇

2020-07-09马平凡(鲁吉英)

Bean Validation 使用篇

在开始文章之前,先讲一下为什么会写这篇文章,以及阅读后的相应收益。

Bean Validation看起来似乎很简单,很常见,似乎每个学过基于Spring开发web应用的程序员都会用。但今天之所以写下这篇文章是因为我看到项目中有小伙伴误用了Bean Validation,从而导致线上问题。

 

常见的问题有

1. @Valid / @Validated 注解傻傻分不清,两者是否有区别,使用场景是否一致;

2. 参数(字段)上标注了相应约束注解,但是实际情况并不符合预期;

3. 项目抛出

javax.validation.UnexpectedTypeException:HV000030: No validator could be found for constraint;

 

以上大概列举了笔者所在项目组遇到的一些实际问题。作为一个程序猿,追根溯源是一个好的习惯,为此写下这篇文章,一方面是对自己学习的总结,另一方面也希望小伙伴能够正确运用Bean Validation。下文若不特殊说明,参数校验等价于Bean Validation。

 

文章排布:

- 基础使用篇:主要关注如何正确运用、以及如何用好

- 进阶使用篇:主要关注参数校验背后的原理

 

预期收益:

- 知晓如何正确进行参数校验

- 知晓参数校验的最佳实践

 

一、概述

Bean Validation源于JSR-303 ,而JSR303是 Java EE 6 中的一项子规范。JSR349、JSR380是其升级版,添加了一些新的特性。

Oracle公司传统艺能,一流公司定标准,它们只定义了一些校验注解(Constraint),如@Null/@NotNull/@Pattern,位于javax.validation.constraints包下,只提供规范不提供实现。

Hibernate Validator是对这个规范的实现(不要和数据库ORM框架Hibernate联系在一起),并增加了一些自定义校验注解,如@Email/@Length/@Range,位于org.hibernate.validator.constraints包下。

 

1.1 基础使用篇

Bean Validation并非要结合Spring一起使用,对于不依赖的 Spring  的项目,可以手动导入如下依赖:

valid 和 validated的使用小结

在实际的web项目开发中,我们无需手动引入依赖。当依赖spring-boot-starter-web这个starter时,会自动传递相应的Bean Validation依赖。但有一点需要注意,在更新版本的SpringBoot中,默认移除了Bean Validtion相关依赖。具体的对应关系可以参照如下表格:

valid 和 validated的使用小结

 

Controller层校验

假设我们实现了一个Spring REST控制器,想要验证由客户端传入的参数。根据请求方式、携带的内容以及实际应用场景,一般有三类:

- POST Request Body;

- GET PathVariable (如/foos/{id});

- GET Query Param(如url?q=param)

上面三种基本覆盖了大部分的开发场景

 

1.验证Request Body

示例:

valid 和 validated的使用小结

valid 和 validated的使用小结注意此时注解标注的位置,必须放在方法参数上,放在类上会导致校验不生效,行为不符合预期。此外,针对这种情形@Valid 和@Validated两个注解可以混用。

如果校验失败,会抛出一个MethodArgumentNotValidException异常,Spring默认会把这个转为400(Bad Request)请求。

在实际项目开发中,通常会用 ExceptionHandler处理该异常,包裹返回一个更友好的提示:

valid 和 validated的使用小结

通过一个简单的单元测试来验证校验是否生效,是否符合预期行为:

valid 和 validated的使用小结

2. 校验PathVariable/RequestParam

开发中,如果参数个数小于三个,倾向于不写Java Bean来封装参数,而是平铺写到方法入参中。

对于这种情况,需要在入参上直接声明约束注解(如@Min),类上标注@Validated注解。

示例:

valid 和 validated的使用小结

注意:在类级别上标注@Validated注解告诉Spring需要校验方法参数上的约束。

 

在这种场景里@Validated注解修饰类。不同于针对Request Body的校验,此处失败会触发ConstraintViolationException 异常。

valid 和 validated的使用小结

然后通过一个简单的单测来验证下:

valid 和 validated的使用小结

Service层校验

除了在控制器级别验证输入之外,通常我们还习惯在Service层进行验证。此处 Service层是一个笼统的概念,也有称之为的 Business层、Loigc层,本文统称为 Service层,不做详细区分。

 

复杂对象示例

valid 和 validated的使用小结

上面给出了5种注解标注位置,只有第一种标注是正确的,其余全部是误用。这会导致标注在RequestParam类字段上的校验全部没有生效。校验没有生效会导致Service层拿着无效的数据进行计算,也可能导致DAO层存储了脏数据。

注意在 Service层进行校验,需要组合两个注解一起使用。

下面通过一个单元测试来验证下:

valid 和 validated的使用小结

简单参数示例

valid 和 validated的使用小结

同样需要注意标注的位置。

还是通过单元测试来验证下

valid 和 validated的使用小结

小结

最后以一张图小结。至于为什么凡是在Class上标注@Validated注解的看起来用法都一样,这个疑问留到下一篇文章中解答。

valid 和 validated的使用小结

1.2 进阶使用篇

从这里开始将会介绍一些高级特性:

- 嵌套校验

- 自定义校验器

- 分组校验

- 手动校验

- Fail Fast

 

嵌套校验

上文提到过针对Java Bean的校验,里面的字段都是非嵌套。实际的业务场景中,对象内字段类型也是对象的场景并不罕见。

示例:

valid 和 validated的使用小结

可以看到此处的 Input有一个 person字段,该字段指向另一个Java Bean。针对这种场景,需要在person字段上标注@Valid注解,并且该字段指向的类同样需要标注约束注解。

代码中经常会看到有的同学这样写:

valid 和 validated的使用小结

此时person字段上只会校验非空,Person类中标注的注解并不会生效,从而不符合预期。

通过一个简单的单元测试验证一下:

valid 和 validated的使用小结

 

valid 和 validated的使用小结

自定义校验

有时候,官方提供的约束注解并不能满足所有业务需求场景,这个时候可以通过自定义注解来满足自定义需求。整个过程的核心是定义约束注解,并实现 ConstraintValidator接口,提供对应校验注解的处理类。

以一个具体的例子来看:

valid 和 validated的使用小结

 

 

 

 

valid 和 validated的使用小结

valid 和 validated的使用小结valid 和 validated的使用小结valid 和 validated的使用小结分组校验

通常,某些Java Bean在不同的请求之间共享。以典型的CRUD操作为例:Create请求和Update请求很可能都采用相同的对象类型作为输入。但是,在不同的情况下可能会触发不同的验证。

valid 和 validated的使用小结

valid 和 validated的使用小结

valid 和 validated的使用小结

此处提供的示例都是在Controller层完成,如果想要在Service层完成分组校验,需要注意@Validated注解标注的位置。下面简单提供一个示例:

valid 和 validated的使用小结

密切注意此处注解标注的位置,以及使用了三个注解。

 

手动校验

在某些情况下,我们可能希望以编程方式(注解对应声明式)调用验证,并且不依赖于Spring内置的Bean Validation支持。在这种情况下,我们可以手动创建一个验证器,调用它来触发验证:

valid 和 validated的使用小结

或者有时候我们想偷点懒,不想写一些样板代码,此外上述手动获取到的validator并非单例,借助下Spring的能力也是可以接受的,那么可以这样做:

valid 和 validated的使用小结

Fail Fast

Bean Validation默认会校验完所有字段,然后才抛出异常。可通过一些简单的配置,开启Fali Fast模式,一旦校验失败就立即返回。

参考如下配置:

valid 和 validated的使用小结

最佳实践

Bean Validation只要用对了,符合预期就是没有问题的。但是实际生产实践中,经常会遇到一些不优雅的做法,此处试图给出一些通用的最佳实践。

 

不要在持久化层进行校验

根据上面的知识其实可以知道,其实在任何一层进行Bean Validation都是可以的,但是上面举例特意避开了持久化层的校验。

这是为什么呢?

在一个常见的web应用程序中,持久层是最下面的,上面通常还有业务层和表示层。数据传到表示层,通过业务层,最后到达持久层。如果我们只在持久层进行验证,上面两层就会使用无效、非法的数据进行运算,这些计算可能大部分场景都是无意义的。

问题越早暴露越好,对于校验来说也是如此,这一点也契合Fail Fast Principle。

 

不要散弹枪式校验

有时候校验太少会有问题,但实际开发中,校验太多同样会导致问题。

数据在通过表示层进入系统之前已经使用Bean Validation进行验证。表示层将传入的数据转换为可以传递给业务层的对象。但是经常会遇到业务层不信任上一层,因此它使用再次验证该对象。在实际项目中,经常可以看到在执行实际的业务逻辑之前,业务层又以编程方式检查我们能想到的每个约束,以此来能保证业务层安全。更极端的是,持久层在数据存储到数据库之前再次验证数据。

上面的流程似乎很好地践行了防御式编程,看起来似乎很美好。但与之而来的问题是校验逻辑散落四处,既增加维护难度,又浪费了CPU。很多校验是冗余的,这种防御式编的确程能让我们写出安全的代码,但不能写出优雅的代码。

简而言之,我们应该有一个清晰且集中的验证策略,而不是在任何地方验证所有的东西。

最后解答下标题,何为散弹枪式校验。

散弹枪可以同时射向多个角度,对应我们代码散落四处的冗余校验。对于用枪高手,可能一把精致的手枪同样可以一击毙敌;作为程序员是否能够做到一处校验,四处安全呢?

有意识的校验

Bean Validation是一个很好的工具。但是有了好的工具,能用好也同样重要。

我们应该有一个清晰的验证策略,告诉我们在哪里验证,什么时候使用哪个工具进行验证,而不是对所有的东西都使用Bean Validation。

一般来说,对于跟业务耦合较小的字段(如字段判空,手机号,IP地址,Email地址)尽可能前移到Controller层。基于声明式注解来完成这种校验是相对比较优雅且合适的。

对于需要结合具体业务才能完成的校验(如判断房源编码是否存在)统一收口到业务代码中。

 

总结

 

行文至此,简单回顾下开头提出的三个问题:

1. @Valid / @Validated 注解傻傻分不清,两者是否有区别,使用场景是否一致;

2. 参数(字段)上标注了相应注解,但是实际情况并不符合预期;

3. 项目抛出

javax.validation.UnexpectedTypeException:HV000030: No validator could be found for constraint

可以看到,本篇文章中只解答了第二个问题,对于第一个、第三个并未过多涉及。这篇文章的侧重点是如何正确使用,而不是探究原理性的知识。在下一篇文章中,笔者将会对剩下的疑问给出答案,敬请期待。

 

参考文献

1. All You Need To Know About Bean Validation With Spring Boot

2. 概述 Bean Validation 规范

3. 官方文档

4. Bean Validation 技术规范特性概述

可点击原文链接查看

valid 和 validated的使用小结

相关文章:

  • 2021-11-28
  • 2022-12-23
  • 2022-12-23
  • 2021-04-20
  • 2022-12-23
  • 2021-07-14
  • 2021-07-09
猜你喜欢
  • 2021-05-20
  • 2021-05-20
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2021-09-12
  • 2022-01-08
相关资源
相似解决方案