@Validated、@Valid和BindingResult
Bean Validation是Java定义的一套基于注解的数据校验规范,比如@Null、@NotNull、@Pattern等,它们位于 javax.validation.constraints这个包下。
hibernate validator是对这个规范的实现,并增加了一些其他校验注解,如 @NotBlank、@NotEmpty、@Length等,它们位于org.hibernate.validator.constraints这个包下。
依赖
hibernate validator框架已经集成在 spring-boot-starter-web中,所以无需再添加其他依赖。如果不是Spring Boot项目,需要添加如下依赖。
@Valid和@Validated 区别
Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种)。
javax提供了@Valid,配合BindingResult可以直接提供参数验证结果(标准JSR-303规范)。
@Validation对@Valid进行了二次封装,在使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同
分组
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。
@Valid:没有分组校验的功能。
注解地方
@Validated:用在类型、方法和方法参数上(类, 方法, 参数)。但不能用于成员属性。
@Valid:可以用在方法、构造函数、方法参数和成员属性上(方法, 构造器, 参数,字段, 泛型),可以用@Valid实现嵌套验证
两者是否能用于成员属性(字段)上直接影响能否提供嵌套验证的功能
如A类中引用B类,且A、B二类都有内部校验,为了使B类也生效,在A类中引用B类时,在B类变量上加@Valid注解,如果B类为集合等类型且不能为空还需要再加@NotEmpty。
BindingResult
BindingResult用在实体类校验信息返回结果绑定。
该类作为方法入参,要写在实体对象后面。
@PostMapping("/menus")
public Result addMenu(@RequestBody @Valid Menu menu, BindingResult result) {}
规则注解
validator内置注解
hibernate validator扩展注解
分类
空与非空
注解 | 支持Java类型 | 说明 |
@Null | Object | 为null |
@NotNull | Object | 不为null |
@NotBlank | CharSequence | 不为null,且必须有一个非空格字符 |
@NotEmpty | CharSequence、Collection、Map、Array | 不为null,且不为空(length/size>0) |
Boolean
注解 | 支持Java类型 | 说明 | 备注 |
@AssertTrue | boolean、Boolean | 为true | 为null有效 |
@AssertFalse | boolean、Boolean | 为false | 为null有效 |
日期
注解 | 支持Java类型 | 说明 | 备注 |
@Future | Date、 Calendar、 Instant、 LocalDate、 LocalDateTime、 LocalTime、 MonthDay、 OffsetDateTime、 OffsetTime、 Year、 YearMonth、 ZonedDateTime、 HijrahDate、 JapaneseDate、 MinguoDate、 ThaiBuddhistDate | 验证日期为当前时间之后 | 为null有效 |
@FutureOrPresent | Date、 Calendar、 Instant、 LocalDate、 LocalDateTime、 LocalTime、 MonthDay、 OffsetDateTime、 OffsetTime、 Year、 YearMonth、 ZonedDateTime、 HijrahDate、 JapaneseDate、 MinguoDate、 ThaiBuddhistDate | 验证日期为当前时间或之后 | 为null有效 |
@Past | Date、 Calendar、 Instant、 LocalDate、 LocalDateTime、 LocalTime、 MonthDay、 OffsetDateTime、 OffsetTime、 Year、 YearMonth、 ZonedDateTime、 HijrahDate、 JapaneseDate、 MinguoDate、 ThaiBuddhistDate | 验证日期为当前时间之前 | 为null有效 |
@PastOrPresent | Date、 Calendar、 Instant、 LocalDate、 LocalDateTime、 LocalTime、 MonthDay、 OffsetDateTime、 OffsetTime、 Year、 YearMonth、 ZonedDateTime、 HijrahDate、 JapaneseDate、 MinguoDate、 ThaiBuddhistDate | 验证日期为当前时间或之前 | 为null有效 |
数值
注解 | 支持Java类型 | 说明 | 备注 |
@Max | BigDecimal、BigInteger, byte、short、int、long以及包装类 | 小于或等于 | 为null有效 |
@Min | BigDecimal、BigInteger, byte、short、int、long以及包装类 | 大于或等于 | 为null有效 |
@DecimalMax | BigDecimalBigInteger、CharSequence, byte、short、int、long以及包装类 | 小于或等于 | 为null有效 |
@DecimalMin | BigDecimal、BigIntegerCharSequence, byte、short、int、long以及包装类 | 大于或等于 | 为null有效 |
@Negative | BigDecimal、BigInteger, byte、short、int、long、float、double以及包装类 | 负数 | 为null有效,0无效 |
@NegativeOrZero | BigDecimal、BigInteger, byte、short、int、long、float、double以及包装类 | 负数或零 | 为null有效 |
@Positive | BigDecimal、BigInteger, byte、short、int、long、float、double以及包装类 | 正数 | 为null有效,0无效 |
@PositiveOrZero | BigDecimal、BigInteger, byte、short、int、long、float、double以及包装类 | 正数或零 | 为null有效 |
@Digits(integer = 3, fraction = 2) | BigDecimal、BigInteger、CharSequence, byte、short、int、long以及包装类 | 整数位数和小数位数上限 | 为null有效 |
@Length | String | 字符串长度范围 | @Length |
@Range | 数值类型和String | 指定范围 | @Range |
其他
注解 | 支持Java类型 | 说明 | 备注 |
@Pattern | CharSequence | 匹配指定的正则表达式 | 为null有效 |
CharSequence | 邮箱地址 | 为null有效,默认正则 '.*' | |
@Size | CharSequence、Collection、Map、Array | 大小范围(length/size>0) | 为null有效 |
@URL | URL地址验证 | @URL |
使用
单参数校验
需要在参数前添加注解,而且controller类上必须添加@Validated注解。
@RestController
@RequestMapping("/menu")
@Validated // 单参数校验需要加的注解
public class SysMenuController {
@DeleteMapping("/menus")
public Result deleteMenu(@NotNull(message = "id不能为空") Long id) {
}
}
对象参数校验
先在对象的校验属性上添加注解,然后在Controller方法的对象参数前添加@Valid、@Validated
// 对象
public class Menu {
private Long menuId;
@NotNull(message =parentId不能为空")
private Long parentId;
}
@PostMapping("/menus")
public Result addMenu(@RequestBody @Valid Menu menu, BindingResult result) {
}
对象嵌套
// 对象
public class PagedQueryReqBody<T> {
private Integer page_no;
private Integer page_row_no;
@NotNull
private String page_flg;
@Valid
private T data_request;
}
public class DataReqPQ {
@NotNull
private String car_no;
}
// 接口
@PostMapping(value = "/queryParameter")
public Result queryParameter(@RequestBody @Validated PagedQueryReqBody<DataReqPQ> requestMsg, BindingResult result){
}
分组校验
新建组
Validated有自己默认的组 Default.class
public interface Update {
}
public interface Add extends Default {
}
// 对象
public class User {
@NotBlank(message = "id不能为空",groups = {Update.class})
private String id;
private String name;
@NotBlank(message = "密码不能为空",groups = {Add.class})
private String password;
}
id属性的校验属于Update分组的校验
password属性的校验属于Add、Default分组的校验
使用分组
使用默认分组:Add分组继承Default,所以校验password,不校验id
@PostMapping("/addUser")
public Resp addUser(@Validated @RequestBody User uer) {
}
使用Update分组:只校验id,不校验password
@PostMapping("/updateUser")
public Resp updateUser(@Validated(Update.class) @RequestBody User user) {
}
异常处理
全局异常处理类
缺少参数抛出的异常是MissingServletRequestParameterException
单参数校验失败后抛出的异常是ConstraintViolationException
get请求的对象参数校验失败后抛出的异常是BindException
post请求的对象参数校验失败后抛出的异常是MethodArgumentNotValidException
不同异常对象的结构不同,对异常消息的提取方式也就不同。
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)// 设置状态码为500
@ExceptionHandler(MethodArgumentNotValidException.class)
public String postExceptionHandler(MethodArgumentNotValidException e){
log.error("执行异常",e);
BindingResult exceptions = e.getBindingResult();
if (exceptions.hasErrors()) {}
}
@ResponseBody
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)// 设置状态码为500
@ExceptionHandler(ConstraintViolationException.class)
public String paramExceptionHandler(ConstraintViolationException e){
log.error("执行异常",e);
}
}
BindingResult异常
Controller方法的中处理
@PostMapping("addUser")
public Result addUser(@RequestBody @Valid User user,BindingResult result){
//校验到错误
if (result.hasErrors()) {
//获得错误信息列表
List<String> errMsgs = result.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(toList());
String lists = StringUtils.join(lists, ";");
return new Result(“” "", lists);
}
return new Result(“”, "", null);
}
AOP校验
/**
*将此注解加在需要进行参数校验的方法上,
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamValid {
}
@Aspect
@Component
public class ParamValidAspect {
private static final Logger log = LoggerFactory.getLogger(ParamValidAspect.class);
@Before("@annotation(paramValid)")
public void paramValid(JoinPoint point, ParamValid paramValid){
Object[] paramObj = point.getArgs();
if (paramObj.length > 0){
Arrays.stream(paramObj).forEach(
e ->{
if (e instanceof BindingResult) {
BindingResult result = (BindingResult) e;
Result errorMap = this.validRequestParams(result);
if (errorMap != null){
ServletRequestAttributes res = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = res.getResponse();
response.setCharacterEncoding("UTF-8");
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(HttpStatus.BAD_REQUEST.value());
OutputStream output = null;
try {
output = response.getOutputStream();
String error = objectMapper.writeValueAsString(errorMap);
//响应错误信息
output.write(error.getBytes("UTF-8"));
}catch (IOException e){
log.error(e.getMessage());
}finally{
try{
if (output != null){
output.close();
}
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
}
});
}
}
/**
* 校验
*/
private Result validRequestParams(BindingResult result) {
if (result.hasErrors()) {
List<String> errMsgs = result.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(toList());
String lists = StringUtils.join(lists, ";");
return new Result("", "", lists);
}
return null;
}
}