Spring Boot 参数校验 Validation 入门
1.概述
当我们想提供可靠的 API 接口,对参数的校验,以保证最终数据入库的正确性,是必不可少的活。例如说,用户注册时,会校验手机格式的正确性,密码非弱密码。
可惜的是,在翻开自己的项目的时候,会发现大量的 API 接口,我们并没有添加相应的参数校验,而是把这个活交给调用方(例如说前端)来完成。
世界比我们想象中的不安全,可能有“黑客”会绕过浏览器,直接使用 HTTP 工具,模拟请求向后端 API 接口传入违法的参数,以达到它们“不可告人”的目的。
又或者前端开发小哥,不小心漏做了一些 API 接口调用时的参数校验,结果导致用户提交了大量不正确的数据到后端 API 接口,并且这些数据成功入库了。这个时候,你是会甩锅给前端小哥,还是怒喷测试小姐姐验收不到位呢?
我相信,很多时候并不是我们不想添加,而是没有统一方便的方式,让我们快速的添加实现参数校验的功能。毕竟,比起枯燥的 CRUD 来说,它更枯燥。例如说,还是拿用户注册的接口,校验手机和密码这两个参数,可能就要消耗掉小 10 行的代码。更不要说,管理后台创建商品这种参数贼多的接口。
绝大多数情况下,也就 99.99% 吧,我们采用 Hibernate Validator 。
2. 注解
在开始入门之前,我们先了解下本文可能会涉及到的注解。
2.1 Bean Validation 定义的约束注解
javax.validation.constraints
包下,定义了一系列的约束( constraint )注解。如下:
- 空和非空检查
@NotBlank
:只能用于字符串不为null
,并且字符串#trim()
以后 length 要大于 0 。@NotEmpty
:集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为null
。@NotNull
:不能为null
。@Null
:必须为null
。
- 数值检查
@DecimalMax(value)
:被注释的元素必须是一个数字,其值必须小于等于指定的最大值。@DecimalMin(value)
:被注释的元素必须是一个数字,其值必须大于等于指定的最小值。@Digits(integer, fraction)
:被注释的元素必须是一个数字,其值必须在可接受的范围内。@Positive
:判断正数。@PositiveOrZero
:判断正数或 0 。@Max(value)
:该字段的值只能小于或等于该值。@Min(value)
:该字段的值只能大于或等于该值。@Negative
:判断负数。@NegativeOrZero
:判断负数或 0 。
- Boolean 值检查
@AssertFalse
:被注释的元素必须为true
。@AssertTrue
:被注释的元素必须为false
。
- 长度检查
@Size(max, min)
:检查该字段的size
是否在min
和max
之间,可以是字符串、数组、集合、Map 等。
- 日期检查
@Future
:被注释的元素必须是一个将来的日期。@FutureOrPresent
:判断日期是否是将来或现在日期。@Past
:检查该字段的日期是在过去。@PastOrPresent
:判断日期是否是过去或现在日期。
- 其它检查
@Email
:被注释的元素必须是电子邮箱地址。@Pattern(value)
:被注释的元素必须符合指定的正则表达式。
2.2 Hibernate Validator 附加的约束注解
org.hibernate.validator.constraints
包下,定义了一系列的约束( constraint )注解。如下:
@Range(min=, max=)
:被注释的元素必须在合适的范围内。@Length(min=, max=)
:被注释的字符串的大小必须在指定的范围内。@URL(protocol=,host=,port=,regexp=,flags=)
:被注释的字符串必须是一个有效的 URL 。@SafeHtml
:判断提交的 HTML 是否安全。例如说,不能包含 javascript 脚本等等。- … 等等,就不一一列举了。
2.3 @Valid 和 @Validated
@Valid
注解,是 Bean Validation 所定义,可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。
@Validated
注解,是 Spring Validation 锁定义,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验。同时,@Validated
有 value
属性,支持分组校验。属性如下:
// Validated.javaClass<?>[] value() default {};
对于初学者来说,很容易搞混 @Valid
和 @Validated
注解。
① 声明式校验
Spring Validation 仅对 @Validated
注解,实现声明式校验。
② 分组校验
Bean Validation 提供的 @Valid
注解,因为没有分组校验的属性,所以无法提供分组校验。此时,我们只能使用 ``@Validated` 注解。
③ 嵌套校验
相比来说,@Valid
注解的地方,多了【成员变量】。这就导致,如果有嵌套对象的时候,只能使用 @Valid
注解。例如说:
// User.java
public class User {private String id;@Validprivate UserProfile profile;}// UserProfile.java
public class UserProfile {@NotBlankprivate String nickname;}
- 如果不在
User.profile
属性上,添加@Valid
注解,就会导致UserProfile.nickname
属性,不会进行校验。
当然,@Valid
注解的地方,也多了【构造方法】和【方法返回】,所以在有这方面的诉求的时候,也只能使用 @Valid
注解。
🔥 总结
总的来说,绝大多数场景下,我们使用 @Validated
注解即可。
而在有嵌套校验的场景,我们使用 @Valid
注解添加到成员属性上。
3. 快速入门
本小节,我们会实现在 Spring Boot 中,对 SpringMVC 的 Controller 的 API 接口参数,实现参数校验。
同时,因为我们在 Service 也会有参数校验的诉求,所以我们也会提供示例。
3.1 引入依赖
在 pom.xml
文件中,引入相关依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.3.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><modelVersion>4.0.0</modelVersion><artifactId>lab-22-validation-01</artifactId><dependencies><!-- 实现对 Spring MVC 的自动化配置 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 保证 Spring AOP 相关的依赖包 --><dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId></dependency><!-- 方便等会写单元测试 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies></project>
spring-boot-starter-web
依赖里,已经默认引入hibernate-validator
依赖,所以本示例使用的是 Hibernate Validator 作为 Bean Validation 的实现框架。
3.2 Application
创建 Application.java
类,配置 @SpringBootApplication
注解即可。代码如下:
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true) // http://www.voidcn.com/article/p-zddcuyii-bpt.html
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}}
- 添加
@EnableAspectJAutoProxy
注解,重点是配置exposeProxy = true
,因为我们希望 Spring AOP 能将当前代理对象设置到 AopContext 中。
先暂时不启动项目。等我们添加好 Controller 。
3.3 UserAddDTO
在 com.sunrise.springboot.lab22.validation.dto
包路径下,创建 UserAddDTO 类,为用户添加 DTO 类。代码如下:
// UserAddDTO.javapublic class UserAddDTO {/*** 账号*/@NotEmpty(message = "登录账号不能为空")@Length(min = 5, max = 16, message = "账号长度为 5-16 位")@Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")private String username;/*** 密码*/@NotEmpty(message = "密码不能为空")@Length(min = 4, max = 16, message = "密码长度为 4-16 位")private String password;// ... 省略 setting/getting 方法
}
每个字段上的约束注解。
3.4 UserController
在 com.sunrise.springboot.lab22.validation.controller 包路径下,创建 UserController 类,提供用户 API 接口。代码如下:
// UserController.java@RestController
@RequestMapping("/users")
@Validated
public class UserController {private Logger logger = LoggerFactory.getLogger(getClass());@GetMapping("/get")public void get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id) {logger.info("[get][id: {}]", id);}@PostMapping("/add")public void add(@Valid UserAddDTO addDTO) {logger.info("[add][addDTO: {}]", addDTO);}}
- 在类上,添加
@Validated
注解,表示 UserController 是所有接口都需要进行参数校验。 - 对于
#get(id)
方法,我们在id
参数上,添加了@Min
注解,校验id
必须大于 0 。校验不通过示例如下图: - 对于
#add(addDTO)
方法,我们在addDTO
参数上,添加了@Valid
注解,实现对该参数的校验。校验不通过示例如下图:errors
字段,参数错误明细数组。每一个数组元素,对应一个参数错误明细。这里,username
违背了长度不满足[5, 16]
。
示例我们是已经成功跑通了,但是呢,这里有几点差异性,我们要来理解下。
第一点,#get(id)
方法上,我们并没有给 id
添加 @Valid
注解,而 #add(addDTO)
方法上,我们给 addDTO
添加 @Valid
注解。这个差异,是为什么呢?
因为 UserController 使用了 @Validated
注解,那么 Spring Validation 就会使用 AOP 进行切面,进行参数校验。而该切面的拦截器,使用的是 MethodValidationInterceptor 。
- 对于
#get(id)
方法,需要校验的参数id
,是平铺开的,所以无需添加@Valid
注解。 - 对于
#add(addDTO)
方法,需要校验的参数addDTO
,实际相当于嵌套校验,要校验的参数的都在addDTO
里面,所以需要添加@Valid
注解。
第二点,#get(id)
方法的返回的结果是 status = 500
,而 #add(addDTO)
方法的返回的结果是 status = 400
。
- 对于
#get(id)
方法,在 MethodValidationInterceptor 拦截器中,校验到参数不正确,会抛出 ConstraintViolationException 异常。 - 对于
#add(addDTO)
方法,因为addDTO
是个 POJO 对象,所以会走 SpringMVC 的 DataBinder 机制,它会调用DataBinder#validate(Object... validationHints)
方法,进行校验。在校验不通过时会抛出 BindException 。
在 SpringMVC 中,默认使用 DefaultHandlerExceptionResolver 处理异常。
- 对于 BindException 异常,处理成 400 的状态码。
- 对于 ConstraintViolationException 异常,没有特殊处理,所以处理成 500 的状态码。
这里,我们再抛个问题,如果 #add(addDTO
方法,如果参数正确,在走完 DataBinder 中的参数校验后,会不会在走一遍 MethodValidationInterceptor 的拦截器呢?
答案是会。这样,就会导致浪费。所以 Controller 类里,如果只有类似的 #add(addDTO)
方法的嵌套校验,那么我可以不在 Controller 类上添加 @Validated
注解。从而实现,仅使用 DataBinder 中来做参数校验。
第三点,无论是 #get(id)
方法,还是 #add(addDTO)
方法,它们的返回提示都非常不友好,那么该怎么办呢?
3.5 UserService
相比在 Controller 添加参数校验来说,在 Service 进行参数校验,会更加安全可靠。个人建议的话,Controller 的参数校验可以不做,Service 的参数校验一定要做。
在 com.sunrise.springboot.lab22.validation.service 包路径下,创建 UserService 类,提供用户 Service 逻辑。代码如下:
// UserService.java@Service
@Validated
public class UserService {private Logger logger = LoggerFactory.getLogger(getClass());public void get(@Min(value = 1L, message = "编号必须大于 0") Integer id) {logger.info("[get][id: {}]", id);}public void add(@Valid UserAddDTO addDTO) {logger.info("[add][addDTO: {}]", addDTO);}public void add01(UserAddDTO addDTO) {this.add(addDTO);}public void add02(UserAddDTO addDTO) {self().add(addDTO);}private UserService self() {return (UserService) AopContext.currentProxy();}}
- 和 UserController 的方法是一致的,包括注解。
- 额外添加了
#add01(addDTO)
和#add02(addDTO)
方法,用于演示方法内部调用。
创建 UserServiceTest 测试类,我们来测试一下简单的 UserService 的每个操作。代码如下:
// UserService.java@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserServiceTest {@Autowiredprivate UserService userService;@Testpublic void testGet() {userService.get(-1);}@Testpublic void testAdd() {UserAddDTO addDTO = new UserAddDTO();userService.add(addDTO);}@Testpublic void testAdd01() {UserAddDTO addDTO = new UserAddDTO();userService.add01(addDTO);}@Testpublic void testAdd02() {UserAddDTO addDTO = new UserAddDTO();userService.add02(addDTO);}}
① #testGet()
测试方法
执行,抛出 ConstraintViolationException 异常。日志如下:
javax.validation.ConstraintViolationException: get.id: 编号必须大于 0at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116)
- 符合期望。
② #testAdd()
测试方法
执行,抛出 ConstraintViolationException 异常。日志如下:
javax.validation.ConstraintViolationException: add.addDTO.username: 登录账号不能为空, add.addDTO.password: 密码不能为空at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116)
- 符合期望。不同于我们在调用
UserController#add(addDTO)
方法,这里被 MethodValidationInterceptor 拦截,进行参数校验,而不是 DataBinder 当中。
③ #testAdd01()
测试方法
执行,正常结束。因为进行 this.add(addDTO)
调用时,this
并不是 Spring AOP 代理对象,所以并不会被MethodValidationInterceptor 所拦截。
④ #testAdd02()
测试方法
执行,抛出 IllegalStateException 异常。日志如下:
java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available.at org.springframework.aop.framework.AopContext.currentProxy(AopContext.java:69)
- 理论来说,因为我们配置了
@EnableAspectJAutoProxy(exposeProxy = true)
注解,在 Spring AOP 拦截时,通过调用AopContext.currentProxy()
方法,是可以获取到当前的代理对象。结果,此处抛出 IllegalStateException 异常。 - 显然,这里并没有将当前的代理对象,设置到 AopContext 中,所以抛出 IllegalStateException 异常。目前猜测,可能是 BUG 。😈 暂时木有心情去调试,嘿嘿。
4. 处理校验异常
在 「3. 快速入门」 中,我们可以看到,如果直接将校验的结果返回给前端,提示内容的可阅读性是比较差的,所以我们需要对校验抛出的异常进行处理。
4.1 ServiceExceptionEnum
修改 ServiceExceptionEnum 枚举类,增加校验参数不通过的错误码枚举。代码如下:
// ServiceExceptionEnum.javaINVALID_REQUEST_PARAM_ERROR(2001001002, "请求参数不合法"),
4.2 GlobalExceptionHandler
修改 GlobalExceptionHandler 类,增加 #constraintViolationExceptionHandler(...)
方法,处理 ConstraintViolationException 异常。代码如下:
// GlobalExceptionHandler.java@ResponseBody
@ExceptionHandler(value = ConstraintViolationException.class)
public CommonResult constraintViolationExceptionHandler(HttpServletRequest req, ConstraintViolationException ex) {logger.debug("[constraintViolationExceptionHandler]", ex);// 拼接错误StringBuilder detailMessage = new StringBuilder();for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {// 使用 ; 分隔多个错误if (detailMessage.length() > 0) {detailMessage.append(";");}// 拼接内容到其中detailMessage.append(constraintViolation.getMessage());}// 包装 CommonResult 结果return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
- 将每个约束的错误内容提示,拼接起来,使用
;
分隔。 - 重新请求
UserController#get(id)
对应的接口,响应结果如下:
修改 GlobalExceptionHandler 类,增加 #bindExceptionHandler(...)
方法,处理 BindException 异常。代码如下:
// GlobalExceptionHandler.java@ResponseBody
@ExceptionHandler(value = BindException.class)
public CommonResult bindExceptionHandler(HttpServletRequest req, BindException ex) {logger.debug("[bindExceptionHandler]", ex);// 拼接错误StringBuilder detailMessage = new StringBuilder();for (ObjectError objectError : ex.getAllErrors()) {// 使用 ; 分隔多个错误if (detailMessage.length() > 0) {detailMessage.append(";");}// 拼接内容到其中detailMessage.append(objectError.getDefaultMessage());}// 包装 CommonResult 结果return CommonResult.error(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getCode(),ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR.getMessage() + ":" + detailMessage.toString());
}
- 将每个约束的错误内容提示,拼接起来,使用
;
分隔。 - 重新请求
UserController#add(addDTO)
对应的接口,响应结果如下:
5. 自定义约束
在大多数项目中,无论是 Bean Validation 定义的约束,还是 Hibernate Validator 附加的约束,都是无法满足我们复杂的业务场景。所以,我们需要自定义约束。
开发自定义约束一共只要两步:1)编写自定义约束的注解;2)编写自定义的校验器 ConstraintValidator 。
下面,就让我们一起来实现一个自定义约束,用于校验参数必须在枚举值的范围内。
5.1 IntArrayValuable
在com.risesun.springboot.lab22.validation.core.validator 包路径下,创建 IntArrayValuable 接口,用于返回值数组。代码如下:
// IntArrayValuable.javapublic interface IntArrayValuable {/*** @return int 数组*/int[] array();}
因为对于一个枚举类来说,我们无法获得它具体有那些值。所以,我们会要求这个枚举类实现该接口,返回它拥有的所有枚举值。
5.2 GenderEnum
在 com.risesun.springboot.lab22.validation.constants
包路径下,创建 GenderEnum
枚举类,枚举性别。代码如下:
// GenderEnum.javapublic enum GenderEnum implements IntArrayValuable {MALE(1, "男"),FEMALE(2, "女");/*** 值数组*/public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(GenderEnum::getValue).toArray();/*** 性别值*/private final Integer value;/*** 性别名*/private final String name;GenderEnum(Integer value, String name) {this.value = value;this.name = name;}public Integer getValue() {return value;}public String getName() {return name;}@Overridepublic int[] array() {return ARRAYS;}}
- 实现 IntArrayValuable 接口,返回值数组
ARRAYS
。
5.3 @InEnum
在 com.risesun.springboot.lab22.validation.core.validator 包路径下,创建 @InEnum 。 自定义约束的注解。代码如下:
// InEnum.java@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = InEnumValidator.class)
public @interface InEnum {/*** @return 实现 IntArrayValuable 接口的*/Class<? extends IntArrayValuable> value();/*** @return 提示内容*/String message() default "必须在指定范围 {value}";/*** @return 分组*/Class<?>[] groups() default {};/*** @return Payload 数组*/Class<? extends Payload>[] payload() default {};/*** Defines several {@code @InEnum} constraints on the same element.*/@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})@Retention(RetentionPolicy.RUNTIME)@Documented@interface List {InEnum[] value();}}
- 在类上,添加
@@Constraint(validatedBy = InEnumValidator.class)
注解,设置使用的自定义约束的校验器。 value()
属性,设置实现 IntArrayValuable 接口的类。这样,我们就能获得参数需要校验的值数组。message()
属性,设置提示内容。默认为"必须在指定范围 {value}"
。- 其它属性,复制粘贴即可,都可以忽略不用理解。
5.4 InEnumValidator
在 springboot.lab22.validation.core.validator 包路径下,创建 InEnumValidator 自定义约束的校验器。代码如下:
// InEnumValidator.javapublic class InEnumValidator implements ConstraintValidator<InEnum, Integer> {/*** 值数组*/private Set<Integer> values;@Overridepublic void initialize(InEnum annotation) {IntArrayValuable[] values = annotation.value().getEnumConstants();if (values.length == 0) {this.values = Collections.emptySet();} else {this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toSet());}}@Overridepublic boolean isValid(Integer value, ConstraintValidatorContext context) {// <2.1> 校验通过if (values.contains(value)) {return true;}// <2.2.1>校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值)context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate().replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句 return false; // <2.2.2.>}}
-
实现
ConstraintValidator
接口。
- 第一个泛型为
A extends Annotation
,设置对应的自定义约束的注解。例如说,这里我们设置了@InEnum
注解。 - 第二个泛型为
T
,设置对应的参数值的类型。例如说,这里我们设置了 Integer 类型。
- 第一个泛型为
-
实现
#initialize(annotation)
方法,获得@InEnum
注解的values()
属性,获得值数组,设置到values
属性种。 -
实现
#isValid(value, context)
方法,实现校验参数值,是否在
values
范围内。
<2.1>
处,校验参数值在范围内,直接返回true
,校验通过。<2.2.1>
处,校验不通过,自定义提示语句。<2.2.2>
处,校验不通过,所以返回false
。
至此,我们已经完成了自定义约束的实现。下面,我们来进行下测试。
5.5 UserUpdateGenderDTO
在 springboot.lab22.validation.dto 包路径下,创建 UserUpdateGenderDTO 类,为用户更新性别 DTO。代码如下:
// UserUpdateGenderDTO.javapublic class UserUpdateGenderDTO {/*** 用户编号*/@NotNull(message = "用户编号不能为空")private Integer id;/*** 性别*/@NotNull(message = "性别不能为空")@InEnum(value = GenderEnum.class, message = "性别必须是 {value}")private Integer gender;// ... 省略 set/get 方法
}
- 在
gender
字段上,添加@InEnum(value = GenderEnum.class, message = "性别必须是 {value}")
注解,限制传入的参数值,必须在 GenderEnum 枚举范围内。
5.6 UserController
修改 UserController 类,增加修改性别 API 接口。代码如下:
// UserController.java@PostMapping("/update_gender")
public void updateGender(@Valid UserUpdateGenderDTO updateGenderDTO) {logger.info("[updateGender][updateGenderDTO: {}]", updateGenderDTO);
}
模拟请求该 API 接口,响应结果如下:
因为我们传入的请求参数 gender
的值为 null
,显然不在 GenderEnum 范围内,所以校验不通过,输出 "性别必须是 [1, 2]"
。
6. 分组校验
在一些业务场景下,我们需要使用分组校验,即相同的 Bean 对象,根据校验分组,使用不同的校验规则。咳咳咳,貌似我们暂时没有这方面的诉求。即使有,也是拆分不同的 Bean 类。
6.1 UserUpdateStatusDTO
在 springboot.lab22.validation.dto 包路径下,创建 UserUpdateStatusDTO 类,为用户更新状态 DTO 。代码如下:
// UserUpdateStatusDTO.javapublic class UserUpdateStatusDTO {/*** 分组 01 ,要求状态必须为 true*/public interface Group01 {}/*** 状态 02 ,要求状态必须为 false*/public interface Group02 {}/*** 状态*/@AssertTrue(message = "状态必须为 true", groups = Group01.class)@AssertFalse(message = "状态必须为 false", groups = Group02.class)private Boolean status;// ... 省略 set/get 方法
}
- 创建了 Group01 和 Group02 接口,作为两个校验分组。不一定要定义在 UserUpdateStatusDTO 类中,这里仅仅是为了方便。
status
字段,在 Group01 校验分组时,必须为true
;在 Group02 校验分组时,必须为false
。
6.2 UserController
修改 UserController 类,增加两个修改状态的 API 接口。代码如下::
// UserController.java@PostMapping("/update_status_true")
public void updateStatusTrue(@Validated(UserUpdateStatusDTO.Group01.class) UserUpdateStatusDTO updateStatusDTO) {logger.info("[updateStatusTrue][updateStatusDTO: {}]", updateStatusDTO);
}@PostMapping("/update_status_false")
public void updateStatusFalse(@Validated(UserUpdateStatusDTO.Group02.class) UserUpdateStatusDTO updateStatusDTO) {logger.info("[updateStatusFalse][updateStatusDTO: {}]", updateStatusDTO);
}
- 对于
#updateStatusTrue(updateStatusDTO)
方法,我们在updateStatusDTO
参数上,添加了@Validated
注解,并且设置校验分组为 Group01 。校验不通过示例如下图: - 对于
#updateStatusFalse(updateStatusDTO)
方法,我们在updateStatusDTO
参数上,添加了@Validated
注解,并且设置校验分组为 Group02 。校验不通过示例如下图:
所以,使用分组校验,核心在于添加上 @Validated
注解,并设置对应的校验分组。
7. 手动校验
在上面的示例中,我们使用的主要是 Spring Validation 的声明式注解。然而在少数业务场景下,我们可能需要手动使用 Bean Validation API ,进行参数校验。
修改 UserServiceTest 测试类,增加手动参数校验的示例。代码如下:
// UserServiceTest.java@Autowired // <1.1>
private Validator validator;@Test
public void testValidator() {// 打印,查看 validator 的类型 // <1.2>System.out.println(validator);// 创建 UserAddDTO 对象 // <2>UserAddDTO addDTO = new UserAddDTO();// 校验 // <3>Set<ConstraintViolation<UserAddDTO>> result = validator.validate(addDTO);// 打印校验结果 // <4>for (ConstraintViolation<UserAddDTO> constraintViolation : result) {// 属性:消息System.out.println(constraintViolation.getPropertyPath() + ":" + constraintViolation.getMessage());}
}
-
<1.1>
处,注入 Validator Bean 对象。 -
<1.2>
处,打印validator
的类型。输出如下:org.springframework.validation.beanvalidation.LocalValidatorFactoryBean@48c3205a
- `validator 的类型为 LocalValidatorFactoryBean 。LocalValidatorFactoryBean 提供 JSR-303、JSR-349 的支持,同时兼容 Hibernate Validator 。
- 在 Spring Boot 体系中,使用 ValidationAutoConfiguration 自动化配置类,默认创建 LocalValidatorFactoryBean 作为 Validator Bean 。
-
<2>
处,创建 UserAddDTO 对象,即 「3.3 UserAddDTO」 ,已经添加相应的约束注解。 -
<3>
处,调用Validator#validate(T object, Class<?>... groups)
方法,进行参数校验。 -
<4>
处,打印校验结果。输出如下:username:登录账号不能为空 password:密码不能为空
- 如果校验通过,则返回的
Set<ConstraintViolation<?>>
集合为空。
- 如果校验通过,则返回的
8. 国际化 i18n
在一些项目中,我们会有国际化的需求,特别是我们在做 TOB 的 SASS 化服务的时候。那么,显然我们在使用 Bean Validator 做参数校验的时候,也需要提供国际化的错误提示。
给力的是,Hibernate Validator 已经内置了国际化的支持,所以我们只需要简单的配置,就可以实现国际化的错误提示。
8.1 应用配置文件
在 resources 目录下,创建 application.yaml 配置文件。配置如下:
spring:# i18 message 配置,对应 MessageSourceProperties 配置类messages:basename: i18n/messages # 文件路径基础名encoding: UTF-8 # 使用 UTF-8 编码
然后,我们在 resources/i18 目录下,创建不同语言的 messages 文件。如下:
-
messages.properties :默认的 i18 配置文件。
UserUpdateDTO.id.NotNull=用户编号不能为空
-
messages_en.properties :英文的 i18 配置文件。
UserUpdateDTO.id.NotNull=userId cannot be empty
-
messages_ja.properties :日文的 i18 配置文件。
UserUpdateDTO.id.NotNull=ユーザー番号は空にできません
8.2 ValidationConfiguration
在 cn.iocoder.springboot.lab22.validation.config 包路径下,创建 ValidationConfiguration 配置类,用于创建一个支持 i18 国际化的 Validator Bean 对象。代码如下:
// ValidationConfiguration.java@Configuration
public class ValidationConfiguration {/*** 参考 {@link ValidationAutoConfiguration#defaultValidator()} 方法,构建 Validator Bean** @return Validator 对象*/@Beanpublic Validator validator(MessageSource messageSource) {// 创建 LocalValidatorFactoryBean 对象LocalValidatorFactoryBean validator = ValidationAutoConfiguration.defaultValidator();// 设置 messageSource 属性,实现 i18 国际化validator.setValidationMessageSource(messageSource);// 返回return validator;}}
8.3 UserUpdateDTO
在 springboot.lab22.validation.dto 包路径下,创建 UserUpdateDTO 类,为用户更新 DTO 。代码如下:
// UserUpdateDTO.javapublic class UserUpdateDTO {/*** 用户编号*/@NotNull(message = "{UserUpdateDTO.id.NotNull}")private Integer id;// ... 省略 get/set 方法}
- 不同于我们上面看到的约束注解的
message
属性的设置,这里我们使用了{}
占位符。
8.4 UserController
修改 UserController 类,增加用户更新的 API 接口。代码如下:
// UserController.java@PostMapping("/update")
public void update(@Valid UserUpdateDTO updateDTO) {logger.info("[update][updateDTO: {}]", updateDTO);
}
下面,我们来进行下 API 接口测试。有一点要注意,SpringMVC 通过 Accept-Language
请求头,实现 i18n 国际化。
Accept-Language = zh
的情况,响应结果如下:Accept-Language = en
的情况,响应结果如下:Accept-Language = ja
的情况,响应结果如下:
至此,我们的 Validator 的 i18n 国际化已经完成了。
不过细心的胖友,会发现 "请求参数不合法"
并没有国际化处理。是的~实际上,国际化是个大工程,涉及到方方面面。例如说,业务信息表的国际化,商品同时支持中文、英文、韩文等多种语言