参数校检
参考:https://cloud.tencent.com/developer/article/1630185
1、概述
Java 早在 2009 年就提出了 Bean Validation 规范,并且已经历经 JSR303、JSR349、JSR380 三次标准的置顶,发展到了 2.0 。
Bean Validation
和我们很久以前学习过的 JPA 一样,只提供规范,不提供具体的实现
。
实现 Bean Validation 规范的数据校验框架,主要有:Hibernate Validator
但是,我们在使用 Spring 的项目中,因为 Spring Validation 提供了对 Bean Validation 的内置封装支持,可以使用 @Validated
注解,实现声明式校验,而无需直接调用 Bean Validation 提供的 API 方法。而在实现原理上,也是基于 Spring AOP 拦截,实现校验相关的操作。
2、注解
2.1 Bean Validation 定义的约束注解
空和非空检查
@NotNull
:校验参数一定不能为null,但是可以为" "。@Null
:必须为null
。@NotEmpty
:校验集合类参数(如String类、Collection、Map、数据Array)不能为null或empty。其中String的length、Collection和Map的size不能为0@NotBlank
:验String字符串不能为null,且不能是空字符串(" "),即调用trim()之后字符串的长度不能为0。
数值检查
@DecimalMax(value)
:被注释的元素必须是一个数字,其值必须小于等于指定的最大值。@DecimalMin(value)
:被注释的元素必须是一个数字,其值必须大于等于指定的最小值。@Digits(integer, fraction)
:被注释的元素必须是一个数字,其值必须在可接受的范围内。@Positive
:带注释的元素必须为严格的正数(0无效)@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.java
Class<?>[] value() default {};
2.3.1 声明式校检
Spring Validation 仅对 @Validated
注解,实现声明式校验。
2.3.2 分组校验
Bean Validation 提供的 @Valid
注解,因为没有分组校验的属性,所以无法提供分组校验。此时,我们只能使用 @Validated
注解。
2.3.3 嵌套校验
相比来说,@Valid
注解的地方,多了【成员变量】。这就导致,如果有嵌套对象的时候,只能使用 @Valid
注解。例如说:
// User.java
public class User {
private String id;
@Valid
private UserProfile profile;
}
// UserProfile.java
public class UserProfile {
@NotBlank
private String nickname;
}
- 如果不在
User.profile
属性上,添加@Valid
注解,就会导致UserProfile.nickname
属性,不会进行校验。
当然,@Valid
注解的地方,也多了【构造方法】和【方法返回】,所以在有这方面的诉求的时候,也只能使用 @Valid
注解。
总的来说,绝大多数场景下,我们使用
@Validated
注解即可。而在有嵌套校验的场景,我们使用
@Valid
注解添加到成员属性上。
3、快速入门
3.1 引入依赖
在 pom.xml
文件中,引入相关依赖。
<!-- 实现对 Spring MVC 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--一般在spring-boot-starter-web中自带该依赖,若没有则加入该依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 保证 Spring AOP 相关的依赖包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
3.2 UserAddDTO
@Getter
@Setter
@ToString
public class UserAddDTO {
/**
* 用户名
*/
@NotEmpty(message = "账号不能为空")
@Length(min = 5,max = 16,message = "账号长度为5-16位之间")
@Pattern(regexp = "^[0-9a-zA-Z]+$",message = "账号只能由字母数组组成")
private String username;
/**
* 密码
*/
@NotEmpty(message = "密码不能为空")
@Length(min = 6,max = 18,message = "密码长度只能在6-18位之间")
private String password;
}
3.3 UserController
@RestController
@Validated
@RequestMapping("/user")
public class UserController {
@GetMapping("/get/{id}")
public ResponseModel<?> get(@PathVariable("id") @Positive(message = "编号必须大于0") Integer id) {
return ResponseModel.ok(id);
}
@PostMapping("/add")
public ResponseModel<?> add(@Valid UserAddDTO userAddDTO) {
return ResponseModel.ok(userAddDTO);
}
}
- 在类上,添加
@Validated
注解,表示 UserController 是所有接口都需要进行参数校验。
第一点,#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 的状态码。
5、全局异常处理
5.1 ResponseModel
public class ResponseModel<T> implements Serializable {
private static final long serialVersionUID = -1263032784821448286L;
/**
* 返回码
*/
private int code;
/**
* 返回信息
*/
private String msg;
/**
* 数据
*/
private T data;
public ResponseModel() {
this(ResponseStatusEnum.SUCCESS.getCode(), ResponseStatusEnum.SUCCESS.getMessage(), null);
}
public ResponseModel(T data) {
this(ResponseStatusEnum.SUCCESS.getCode(), ResponseStatusEnum.SUCCESS.getMessage(), data);
}
public ResponseModel(int code, String msg) {
this(code, msg, null);
}
public ResponseModel(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static <T> ResponseModel<T> ok() {
return new ResponseModel<>();
}
public static <T> ResponseModel<T> ok(T data) {
return new ResponseModel<>(data);
}
public static <T> ResponseModel<T> of(int code, String msg, T data) {
return new ResponseModel<>(code, msg, data);
}
public static <T> ResponseModel<T> fail(int code, String msg) {
return new ResponseModel<>(code, msg);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
5.2 ResponseStatusEnum
public enum ResponseStatusEnum {
/**
* 成功
*/
SUCCESS(0, "成功"),
/**
* 系统异常
*/
SYSTEM_EXCEPTION(10, "系统异常"),
/**
* 参数错误
*/
PARAMETER_EXCEPTION(20, "参数错误"),
/**
* 数据库异常
*/
DATABASE_EXCEPTION(30, "数据库异常");
/**
* 错误码
*/
private final int code;
/**
* 错误信息
*/
private final String message;
ResponseStatusEnum(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
5.3 GlobalExceptionHandler
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseModel<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
if (logger.isWarnEnabled()) {
logger.warn("参数校验异常", e);
}
String message = "未知错误";
if (e.getBindingResult().getFieldError() != null) {
message = e.getBindingResult().getFieldError().getDefaultMessage();
}
return ResponseModel.fail(ResponseStatusEnum.PARAMETER_EXCEPTION.getCode(), message);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseModel<?> constraintViolationExceptionHandler(ConstraintViolationException cve) {
StringBuilder message=new StringBuilder();
for (ConstraintViolation<?> constraintViolation : cve.getConstraintViolations()) {
if (message.length() > 0) {
message.append(";");
}
message.append(constraintViolation.getMessage());
}
return ResponseModel.fail(ResponseStatusEnum.PARAMETER_EXCEPTION.getCode(), message.toString());
}
@ExceptionHandler(BindException.class)
public ResponseModel<?> constraintViolationExceptionHandler(BindException be) {
StringBuilder message=new StringBuilder();
for (ObjectError objectError : be.getAllErrors()) {
if (message.length() > 0) {
message.append(";");
}
message.append(objectError.getDefaultMessage());
}
return ResponseModel.fail(ResponseStatusEnum.PARAMETER_EXCEPTION.getCode(), message.toString());
}
}