Bean Validation 2.0 关注点
- 使用Bean Validation的最低Java版本为Java 8
- 支持容器的校验,通过
TYPE_USE
类型的注解实现对容器内容的约束:List<@Email String>
- 支持日期/时间的校验,@Past和@Future
- 拓展元素数据:@Email,@NotEmpty,@NotBlank,@Positive, @PositiveOrZero,@Negative,@NegativeOrZero,@PastOrPresent和@FutureOrPresent
常见的元数据
meta-data | comment | version |
---|---|---|
@Null | 对象,为空 | Bean Validation 1.0 |
@NotNull | 对象,不为空 | Bean Validation 1.0 |
@AssertTrue | 布尔,为True | Bean Validation 1.0 |
@AssertFalse | 布尔,为False | Bean Validation 1.0 |
@Min(value) | 数字,最小为value | Bean Validation 1.0 |
@Max(value) | 数字,最大为value | Bean Validation 1.0 |
@DecimalMin(value) | 数字,最小为value | Bean Validation 1.0 |
@DecimalMax(value) | 数字,最大为value | Bean Validation 1.0 |
@Size(max, min) | min<=value<=max | Bean Validation 1.0 |
@Digits (integer, fraction) | 数字,某个范围内 | Bean Validation 1.0 |
@Past | 日期,过去的日期 | Bean Validation 1.0 |
@Future | 日期,将来的日期 | Bean Validation 1.0 |
@Pattern(value) | 字符串,正则校验 | Bean Validation 1.0 |
字符串,邮箱类型 | Bean Validation 2.0 | |
@NotEmpty | CharSequence、Collection、Map、Array,不为空 | Bean Validation 2.0 |
@NotBlank | 字符串,不为空字符串 | Bean Validation 2.0 |
@Positive | 数字,正数 | Bean Validation 2.0 |
@PositiveOrZero | 数字,正数或0 | Bean Validation 2.0 |
@Negative | 数字,负数 | Bean Validation 2.0 |
@NegativeOrZero | 数字,负数或0 | Bean Validation 2.0 |
@PastOrPresent | 过去或者现在 | Bean Validation 2.0 |
@FutureOrPresent | 将来或者现在 | Bean Validation 2.0 |
理清关系
JSR303 是一套JavaBean参数校验的标准,它定义了很多常用的校验注解
Bean Validation
是Java定义的一套基于注解/xml的数据校验规范,目前已经从JSR 303
的1.0版本升级到JSR 349
的1.1版本,再到JSR 380
的2.0版本(2.0完成于2017.08),已经经历了三个版本
Bean Validation 2.0的唯一实现为
Hibernate Validator
Hibernate validator 在JSR303的基础上对校验注解进行了扩展
Hibernate Validation 是常用的针对Bean Validation API的事实上标准,并在Bean Validation 的API基础上,进行了扩展,以覆盖更多的场景。
JSR和Hibernate validator的校验只能对Object的属性进行校验,不能对单个的参数进行校验,spring 在此基础上进行了扩展,添加了MethodValidationPostProcessor拦截器,可以实现对方法参数和返回值校验
Spring Validation 则在整合了Hibernate Validation 的基础上,以Spring的方式,支持Spring应用的输入输出校验,比如MVC入参校验,方法级校验等等。
pom 配置
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.14.Final</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
<version>3.0.3</version>
</dependency>
定义一个校验器
HibernateValidatorConfiguration configuration = Validation.byProvider(HibernateValidator.class)
// .providerResolver( ... ) // 因为制定了Provider,这个参数就可选了
.configure()
.failFast(false);
ValidatorFactory validatorFactory = configuration.buildValidatorFactory();
分组校验
public interface Update {
}
public interface Create {
}
public interface Get {
}
注:未提供组的校验注解默认为Default.class
组
Person 对象
@Data
public class Person {
private long id;
/**
* 添加groups 属性,说明只在特定的验证规则里面起作用,不加则表示在使用Deafault规则时起作用
*/
@NotNull(groups = {Create.class, Update.class}, message = "添加、修改用户时名字不能为空", payload = ValidateErrorLevel.Info.class)
private String name;
@NotNull(groups = {Create.class}, message = "添加用户时地址不能为空")
private String address;
@Range(min = 0, max = 150, groups = {Create.class, Update.class}, message = "姓名不能低于0岁不能高于150 岁")
private int age;
private String birth;
}
Employee 对象
@Data
public static class Employee {
@NotNull(groups = {Update.class})
private String uuid;
@NotBlank(message = "员工姓名不能为空", groups = {Create.class})
private String name;
@Pattern(regexp = "1[0-9]{10}")
private String number;
@NotEmpty
private List<@Email String> emails;
@Valid // family中每一个Person对象都进行完整校验
@NotEmpty
private List<Person> family;
@Valid // employee对象也会被作为一个DTO完整校验
private Employee superior;
}
自定义注解
Spring 校验
Spring Validation 则在整合了Hibernate Validation 的基础上,以Spring的方式,支持Spring应用的输入输出校验,比如MVC入参校验,方法级校验等等。
Spring中的校验有两种场景,一种是MVC中的controller
层校验,一种是添加@Validated
的bean的校验,上面提到的例子其实是两种场景的共用的情况。
MVC中的校验比较简单,在Controller的方法入参或者出参添加@Valid
或者@Validated
注解,即可对标记的对象进行校验。
国际化配置
方法校验
JSR和Hibernate validator的校验只能对Object的属性进行校验,不能对单个的参数进行校验,spring 在此基础上进行了扩展,添加了MethodValidationPostProcessor拦截器,可以实现对方法参数的校验,实现如下:
实例化MethodValidationPostProcessor
@Configuration
public class ValidationConfig {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
控制器校验
@RestController
@Validated({Create.class, Update.class, Default.class})
public class EmployeeController {
@RequestMapping(value = "/employee", method = RequestMethod.GET)
public @NotNull(groups = Get.class) Employee getEmployee(@Length(min = 10) @RequestParam String name) {
}
/**
* Employee
* 此处启用PersonAddView 这个验证规则
* 备注:此处@Validated(Create.class) 表示使用 Create 这套校验规则,若使用 @Valid、@Validated、@Validated(Default.class) 都则表示使用默认校验规则,
* 若两个规则同时加上去,则只有第一套起作用
*/
@RequestMapping(value = "/employee", method = RequestMethod.POST)
public void addEmployee(@RequestBody @Validated({Create.class}) List<Employee> employee) {
}
//方法上的分组覆盖类的分组
@Validated({Default.class})
public Object doSomething(@NotNull(groups = {Insert.class}) Object arg) {
// do something
return null;
}
/**
* 修改 Employee 对象
* 此处启用 Update 这个验证规则
*/
@RequestMapping(value = "/employee", method = RequestMethod.PUT)
public void modifyEmployee(@RequestBody @Validated(value = {Update.class}) Employee employee) {
}
@RequestMapping(value = "/employee", method = RequestMethod.PUT)
public ResponseEntity<?> test3(@Validated List<Employee> employee, BindingResult result, @Validated List<Employee> employee2, BindingResult result2) {
if (result.hasErrors()) {
for (FieldError fieldError : result.getFieldErrors()) {
//...
}
return ResponseEntity.badRequest().body("fail");
}
}
}
MVC 的校验中@Valid
和@Validated
是可以互换的,行为基本一致。test1
中没有将校验的结果放到BindingResult
中,则controller
校验未通过时,会直接扔出异常,如没有自动捕获,则请求会返回BadRequest:400
。
一般建议用 @Valid
,统一异常处理。
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理所有校验失败的异常(MethodArgumentNotValidException异常)
*
* @param ex
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
// 设置响应状态码为400
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result handleBindGetException(MethodArgumentNotValidException ex) {
Map<String, Object> body = new LinkedHashMap<String, Object>();
body.put("timestamp", new Date());
// 获取所有异常
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(x -> x.getDefaultMessage())
.collect(Collectors.toList());
body.put("errors", errors);
return new Result(false, 20001, "提交的数据校验失败", body);
}
/**
* 处理所有参数校验时抛出的异常
*
* @param ex
* @return
*/
@ExceptionHandler(value = ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result handleBindException(ValidationException ex) {
Map<String, Object> body = new LinkedHashMap<String, Object>();
body.put("timestamp", new Date());
// 获取所有异常
List<String> errors = new LinkedList<String>();
if(ex instanceof ConstraintViolationException){
ConstraintViolationException exs = (ConstraintViolationException) ex;
Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
for (ConstraintViolation<?> item : violations) {
errors.add(item.getMessage());
}
}
body.put("errors", errors);
return new Result(true, 20001, "提交的参数校验失败", body);
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(BindException.class)
public Result bindExceptionHandler(final BindException e) {
String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
return new Result(500, message);
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handler(final MethodArgumentNotValidException e) {
String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
return new Result(500, message);
}
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(ConstraintViolationException.class)
public Result handler(final ConstraintViolationException e) {
String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining());
return new Result(500, message);
}
}
备注:Service 层也可以校验,一般在接口校验。方法与 Controller 层一样。
参考
https://www.jianshu.com/p/21c1c463e5bf
https://www.jianshu.com/p/7726c47c6644
https://www.jianshu.com/p/26f4b0e837a0
国际化 https://blog.csdn.net/choimroc/article/details/101880225、https://www.jianshu.com/p/46eda1f96abe
附录
注:
@Valid (javax.validation): 是Bean Validation 中的标准注解,表示对需要校验的 【字段/方法/入参】 进行校验标记
@Validated(org.springframework.validation.annotation):是Spring对@Valid扩展后的变体,支持分组校验。
Validated
关于@Validated
注解的功能,官方注释里面已经写的很清楚了,我这里简单翻译下:
- JSR-303的变种
@Valid
,支持验证组规范。支持基于Spring的JSR-303,但不支持JSR-303的特殊扩展。- 可以用于例如Spring MVC处理程序方法参数。通过{
@linkorg.springframework.validation.SmartValidator
}支持组验证。- 支持方法级的验证。在方法级别上添加此注解,会覆盖类上的组信息。但是方法上的注释不会作为切入点,要想方法上的注解生效,类上也必须添加注解。
- 支持元注解,可以添加在自定义注解上,组装为新的注解
通过官方的注释,已经能够明白这个注解的大部分功能了。
将@Validated
加在类上,Spring会将标注的类包装为切面,从而让类中的方法调用时,支持Java的校验,所以当使用@Validated
时,不仅可以用于Controller上,其他所有的Spring的bean也都可以使用。
为何自己在使用的时候从来都没有导入过EL相关Jar包,也能正常数据校验呢?
答:那是因为绝大多数情况下你使用@Valid是使用在Spring MVC上,它是不依赖于EL方式的