前言:
本篇文章不是API参考文档,所以不会将用到的所有内容详细列出来。本文的目的主要是告诉读者关于Java的 Bean Validation在Spring的应用,并针对常见的场景进行说明,力求让读者对Java的Bean Validation有一个完整的认识和理解。
文章关键字:
- JSR-303
- Bean Validation 1.0/1.1/2.0
- MVC Validation
- Hibernate Validation
- Spring Validation
代码中为了保证代码的正常运行,经常会对入参参数做大量的校验,以防止非法输入导致程序运行异常,Java 从2009年开始提出了 Bean Validation 1.0(也就是JSR-303)API,力求将输入输入的校验标准化和简单化,更重要的是将校验通用化。Hibernate Validation 是常用的针对Bean Validation API的实现之一(还有Apache BVal),并在Bean Validation 的API基础上,进行了扩展,以覆盖更多的场景。Spring Validation 则在整合了Hibernate Validation 的基础上,以Spring的方式,支持Spring应用的输入输出校验,比如MVC入参校验,方法级校验等等。至此,针对文章关键字已经进行了大概的说明,下面是他们之间的详细关系:

到目前为止Java Bean validation一共有三个版本。

概览
下面的代码片段是Controller中常见的代码,这里出现了@Valid
,@Validated
,@NotEmpty
等等和校验相关的注解,但是其目的却很简单:对uuid
和dtoList
两个参数进行校验,并且对list
中的元素也进行遍历校验。
后续我们在针对此代码片段进行详细说明。
@Validated
@RestController
public class DemoController {
@PutMapping("bean/validation/tips/{uuid}")
@Validated({Default.class, Update.class})
public ResponseEntity<List<ValidationDTO>> doSomething(
@PathVariable("uuid") @Size(min=32, max=32) String uuid,
@RequestBody @Valid @NotEmpty List<ValidationDTO> dtoList) {
// do something
}
}
@Valid和@Validated
-
@
Valid
(javax.validation
): 是Bean Validation 中的标准注解,表示对需要校验的 【字段/方法/入参】 进行校验标记 -
@
Validated
(org.springframework.validation.annotation
):是Spring对@Valid
扩展后的变体,支持分组校验。
MVC中的校验
Spring中的校验有两种场景,一种是MVC中的controller
层校验,一种是添加@Validated
的bean的校验,上面提到的例子其实是两种场景的共用的情况。
MVC中的校验比较简单,在Controller的方法入参或者出参添加@Valid
或者@Validated
注解,即可对标记的对象进行校验。
假设需要校验的目标对象为Person
,Person
的每个字段都有一定的业务要求:
public class Person {
@NotBlank //名称不能为空
private String name;
@Pattern(regexp = "1[0-9]{10}") // 电话号码满足1开头,11位长的数字
private String number;
@NotEmpty //至少有一个地址
private List<String> address;
//getter/setter
}
则以下几种使用方法都是ok的
// test1: 使用Valied对Person进行校验
@PostMapping("test1")
public ResponseEntity<?> test1(@RequestBody @Valid Person person) {
return ResponseEntity.ok("ok");
}
// test2: 使用@Validated对person进行校验,并将错误信息绑定到BindingResult中
@PostMapping("test2")
public ResponseEntity<?> test2(@RequestBody @Validated Person person, BindingResult result) {
if (result.hasErrors()) {
for (FieldError fieldError : result.getFieldErrors()) {
//...
}
return ResponseEntity.badRequest().body("fail");
}
return ResponseEntity.ok("success");
}
// test3: 如果有多个需要校验的参数需要给到BindingResult中,则每个result需要紧跟着被校验对象
@PostMapping("test3")
public ResponseEntity<?> test3(@Validated Person person, BindingResult result,
@Validated Person person2, BindingResult result2) {
if (result.hasErrors()) {
for (FieldError fieldError : result.getFieldErrors()) {
//...
}
return ResponseEntity.badRequest().body("fail");
}
return ResponseEntity.ok("success");
}
综上代码所述:mvc的校验中@Valid
和@Validated
是可以互换的,行为基本一致。test1
中没有将校验的结果放到BindingResult
中,则controller
校验未通过时,会直接扔出异常,如没有自动捕获,则请求会返回BadRequest:400
。
校验对象树
上述例子中Person
是一个较为简单的DTO,如果是一个比较复杂的嵌套的DTO话,则校验的目标就不应该是一个对象,而是一个对象树(可以把每一复杂的对象属性看作一个节点)。这种情况只需要调整DTO中的校验注解,在需要进入到内部校验的对象或者数据集合添加@Valid
注解即可。Hibernate Validator官方文档中有较为详细的描述【占坑】。
public static class Employee {
@NotNull(groups = {Update.class})
private String uuid;
@NotBlank(message = "员工姓名不能为空")
private String name;
@Pattern(regexp = "1[0-9]{10}")
private String number;
@NotEmpty
private List<String> address;
@Valid // family中每一个Person对象都进行完整校验
@NotEmpty
private List<Person> family;
@Valid // employee对象也会被作为一个DTO完整校验
private Employee superior;
}
自定义错误信息&分组校验
上述Employee
中name
字段上的@NotEmpty
注解提供了message
,其作用是当校验未通过,将会使用message
的值作为错误消息返回。如果缺省的话,校验框架会自动生成消息如:"Employee.name can not be empty",大多数情况,校验注解中的message
都会配置为Spring的国际化消息的code
进行使用。
上述Employee
的uuid
主键字段上添加了NotNull
注解,但是提供了groups
,其值为Update.class
。其作用是当校验组包含Update.class
标记时,此校验注解才会生效,其他未提供组的校验注解默认为Default.class
组,也就是默认组。这个就是按组校验,如果要上Employee中所有的校验注解都生效,则需要使用@Validated({Update.class, Default.class})
,当然如果只需要默认组生效,直接用@Validated
或者@Validated(Default.class)
都可以。
下面是用法举例:
// 分组校验
@PostMapping("test1")
public ResponseEntity<?> test4(@RequestBody @Validated({Update.class, Default.class}) Employee employee) {
return ResponseEntity.ok("ok");
}
MVC的入参校验未生效
ok,到目前都是看起来一切都OK,但是注意下面例子中 test5/test6的
情况。
@PostMapping("test5")
public ResponseEntity<?> test5(@Valid @RequestBody List<Person> personList) {
return ResponseEntity.ok("ok");
}
@PostMapping("test6")
public ResponseEntity<?> test6(@Validated @RequestBody List<Person> personList) {
return ResponseEntity.ok("ok");
}
接口的批量操作是很常见的需求,比如批量新建数据,这个时候Controller的入参基本上都是集合的形式。但是奇怪的是这种写法并不会生效,无论是@Valid
或者@Validated
注解。为什么呢?
原因分析:
直接对List集合进行校验的行为和对自定的DTO校验的行为其实是有区别的,区别在于自定义的DTO是被作为一个整体对象校验(可以理解为一个入口),对象里的每一个字段都会被按照标记的注解进行校验。但是将List作为一个整体对象的时候,其内部是没有任何校验注解的,因为java源码中本身就没有。上述的
test5
和test6
其本质是方法级别的校验,与下面这个例子test7
类似。这个时候@Valid
和@NotEmpty
都想把personList
作为一个字段来校验,但是MVC不支持这种模式,所以未生效。
@PostMapping("test7")
public ResponseEntity<?> test7(@Valid @NotEmpty @RequestBody List<Person> personList) {
return ResponseEntity.ok("ok");
}
解决方案:
解决办法有两种,一种是封装,将接口需要校验的参数封装为一个DTO,然后再校验。第二个种是使用Spring的方法级别的校验,在Controller的类上添加
@Validated
注解。注意任何Spring的bean都可以添加@Validated
注解来进行方法级别的校验,并不是只能用在Controller上。
详解@Validated
注解
关于@Validated注解的使用,官方注释里面已经写的很清楚了,我这里简单翻译下:
- JSR-303的变种{@link javax.validation.Valid},支持验证组规范。支持基于Spring的JSR-303,但不支持JSR-303的特殊扩展。
- 可以用于例如Spring MVC处理程序方法参数。通过{
@linkorg.springframework.validation.SmartValidator
}支持组验证。- 支持方法级的验证。在方法级别上添加此注解,会覆盖类上的组信息。但是方法上的注释不会作为切入点,要想方法上的注解生效,类上也必须添加注解。
- 支持元注解,可以添加在自定义注解上,组装为新的注解
通过官方的注释,已经能够明白这个注解的大部分功能了。
。。。未完待续
网友评论