前言:
Java API规范(JSR303)定义了Bean校验的标准validation-api,但没有提供实现。hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。
引入依赖pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
常用注解
注意不用错了类型!
注解 | 限制 |
@Null | 限制只能为null |
@NotNull | 限制必须不为null |
@AssertFalse | 限制必须为false |
@AssertTrue | 限制必须为true |
@DecimalMax(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) | 限制必须为一个不小于指定值的数字 |
@Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction |
@Future | 限制必须是一个将来的日期 |
@Max(value) | 限制必须为一个不大于指定值的数字 |
@Min(value) | 限制必须为一个不小于指定值的数字 |
@Past | 限制必须是一个过去的日期 |
@Pattern(value) | 限制必须符合指定的正则表达式 字符串 |
@Size(max,min) | 限制字符长度必须在min到max之间 【不是数字】 |
@Past | 验证注解的元素值(日期类型)比当前时间早 |
@NotEmpty | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@NotBlank | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
@Email | 验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 |
@Length(min = 6, max = 16) | 指定传入的字符串的长度 |
使用
对于web
服务来说,为防止非法参数对业务造成影响,在Controller
层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:
POST
、PUT
请求,使用requestBody
传递参数;
这种情况下,后端使用DTO
对象进行接收。只要给DTO
对象加上@Validated
注解就能实现自动参数校验。比如,有一个保存User的接口,要求userName长度是2-10,account和password字段长度是6-20。如果校验失败,会抛出MethodArgumentNotValidException
异常,Spring默认会将其转为400(Bad Request)
请求。
DTO
表示数据传输对象(Data Transfer Object
),用于服务器和客户端之间交互传输使用的。在spring-web项目中可以表示用于接收请求参数的Bean对象。
GET
请求,使用requestParam/PathVariable
传递参数。
如果参数比较多(比如超过6个),还是推荐使用DTO对象接收。否则,推荐将一个个参数平铺到方法入参中。在这种情况下,必须在Controller
类上标注@Validated
注解,并在入参上声明约束注解(如@Min
等)。如果校验失败,会抛出ConstraintViolationException
异常。
首先在实体类里面加入校验的注解
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
public class User implements Serializable {
private static final long serialVersionUID = -5577261004651495987L;
@TableId(type = IdType.AUTO)
private Integer userId;
@NotEmpty
@Length(min = 6, max = 16, message = "密码不能为空") //@Length本身就不允许为空
private String password;
private Integer role;
private String salt;
/**
* 通过邮箱来登录
*/
@Email(regexp = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$")
@NotNull
private String email;
private Date registerTime;
@Override
public String toString() {
String ret = null;
ObjectMapper mapper = new ObjectMapper();
try {
ret = mapper.writeValueAsString(this);
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
}
然后在接收User对象的时候加入@Valid
或者@Validated
注解:
注意:
spring提供的验证:org.springframework.validation.annotation.Validated;
javax提供的验证:javax.validation.Valid;
@Valid 和 @Validated 都用来触发一次校验, @Valid 是 JSR 303规范的注解, @Validated 是Spring 加强版的注解, 加强的地方有:
@Validated 支持组序列, 该特性用的较少 推荐使用 @Validated 注解.
有几种方法都可以捕获到出现的不匹配情况:
方法一:全局异常捕获
@PostMapping("/login")
public RetJson<Object> login(@Valid @RequestBody User user){
...
}
使用全局异常返回错误信息(就是写在实体类上面的message信息)
@RestControllerAdvice
@Slf4j
public class ExceptionResolver {
/**
* 参数不匹配异常
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = { MethodArgumentNotValidException.class})
public RetJson methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
log.error("------->MethodArgumentNotValidException参数异常-------- ", e);
return RetJson.fail(ResultCode.VALIDATE_FAILED.getCode(), "参数异常"+e.getBindingResult().getFieldError().getDefaultMessage());
//getDefaultMessage()会返回message信息
}
}
第二种:在方法上直接处理
在实体类传参时加上BindingResult
注解:
同时注意
BindingResult
必须紧跟@ModelAttribute的后面
例如:必须写成这种方式:
public String add(@Valid RoleBO role, BindingResult binding,
Map<String, Object> request)写成如下方式,
public String add(@Valid RoleBO role, Map<String, Object> request),BindingResult result)
会抛出异常:java.lang.IllegalStateException: An Errors/BindingResult argument is expected to be declared immediately after the model attribute, the @RequestBody or the @RequestPart arguments to which they apply:
其实过程就是:
- 在 Pojo 类的字段上, 加上 Hibernate validator 注解
- 在Controller 函数的形参前加上 @Valid 或 @Validated 注解, 触发一次validation.
- 在每个 @Valid 或 @Validated 注解参数后, 紧跟一个 BindingResult 类型的参数. 如果没有提供对应的 BindingResult 参数, Spring MVC 将抛出异常.
- 在Controller 函数中, 通过 BindingResult 类型的参数获取检验的结果.
@PostMapping("/login")
public RetJson<Object> login(@Valid @RequestBody User user, BindingResult result){
if (result.hasErrors()) {
log.error("[login]参数错误:User={}", user);
String s = result.getFieldError().getDefaultMessage();
throw new BaseException(ResultCode.VALIDATE_FAILED.getCode(), s);
}
...
}
这样也可以直接返回message错误的信息。
方法三:工具类
写一个工具类判断有没有不匹配的情况:
public class ValidatedUtil {
private static Validator validator=
Validation.byProvider(HibernateValidator.class).configure().failFast(true).buildValidatorFactory().getValidator();
public static<T> Boolean validate(T v){
Set<ConstraintViolation<T>> set= validator.validate(v);
return set.size() == 0;
}
public static void validData(BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
StringBuilder sb = new StringBuilder();
for (ObjectError error : bindingResult.getAllErrors()) {
sb.append(error.getDefaultMessage());
}
throw new BaseException(ResultCode.VALIDATE_FAILED.getCode(), sb.toString());
}
}
}
当然单个参数也可以直接使用
@GetMapping("/getEmailCode")
public RetJson<Object> getEmailCode(
@RequestParam("email") @Valid
@Email(regexp = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$", message = "邮箱格式不对") String email) {
//使用全局异常拦截MethodArgumentNotValidException 可以拦截到这个信息
}