SpringBoot使用javax.validation进行参数校验(3)
内置的校验注解
注解 | 校验功能 |
---|---|
@AssertFalse | 必须是false |
@AssertTrue | 必须是true |
@DecimalMax | 小于等于给定的值 |
@DecimalMin | 大于等于给定的值 |
@Digits | 可设定最大整数位数和最大小数位数 |
校验是否符合Email格式 | |
@Future | 必须是将来的时间 |
@FutureOrPresent | 当前或将来时间 |
@Max | 最大值 |
@Min | 最小值 |
@Negative | 负数(不包括0) |
@NegativeOrZero | 负数或0 |
@NotBlank | 不为null并且包含至少一个非空白字符 |
@NotEmpty | 不为null并且不为空 |
@NotNull | 不为null |
@Null | 为null |
@Past | 必须是过去的时间 |
@PastOrPresent | 必须是过去的时间,包含现在 |
@PositiveOrZero | 正数或0 |
@Size | 校验容器的元素个数 |
校验
规范返回值
package com.hanzhenya.learnspringboot.util; import java.util.List; /** * @Description: 结果处理 * @author: 韩振亚 * @date: 2021年03月29日 13:43 */ public class ResultInfo<T> { private Integer status; private String message; private T response; public ResultInfo() { } public ResultInfo(Integer status, String message, T response) { this.status = status; this.message = message; this.response = response; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getResponse() { return response; } public void setResponse(T response) { this.response = response; } public ResultInfo success(Integer status, String message, T response) { return new ResultInfo(status, message,response); } }
实体类
public class UserBean { @NotBlank(message="用户名不能为空") private String userName; @NotBlank(message="年龄不能为空") @Pattern(regexp="^[0-9]{1,2}$",message="年龄不正确") private String age; @AssertFalse(message = "必须为false") private Boolean isFalse; /** * 如果是空,则不校验,如果不为空,则校验 */ @Pattern(regexp="^[0-9]{4}-[0-9]{2}-[0-9]{2}$",message="出生日期格式不正确") private String birthday; }
测试controller
package com.hanzhenya.learnspringboot.valid; import com.hanzhenya.learnspringboot.util.ResultInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @Description: TODO * @author: 韩振亚 * @date: 2021年03月29日 10:22 */ @RestController public class ValidatorController { private static final Logger LOGGER = LoggerFactory.getLogger(PhoneController.class); @GetMapping(value="/user/resign") public ResultInfo resign(@Validated UserBean user) { return new ResultInfo().success(200,"成功",user); } }
全局异常处理
通过全局异常处理的方式统一处理校验异常。
当我们写了@validated
注解,不写BindingResult
的时候,Spring 就会抛出异常。由此,可以写一个全局异常处理类来统一处理这种校验异常,从而免去重复组织异常信息的代码。
全局异常处理类只需要在类上标注@RestControllerAdvice
,并在处理相应异常的方法上使用@ExceptionHandler
注解,写明处理哪个异常即可。
package com.hanzhenya.learnspringboot.advice; import com.hanzhenya.learnspringboot.util.ResultInfo; import org.springframework.http.HttpStatus; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import java.util.List; import java.util.Set; import java.util.stream.Collectors; /** * @Description: 切面异常处理 * @author: 韩振亚 * @date: 2021年03月29日 13:38 */ @RestControllerAdvice public class GlobalControllerAdvice{ private static final String BAD_REQUEST_MSG = "客户端请求参数错误"; // <1> 处理 form data方式调用接口校验失败抛出的异常 @ExceptionHandler(BindException.class) public ResultInfo bindExceptionHandler(BindException e) { List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors(); List collect = fieldErrors.stream().map(o ->o.getDefaultMessage()).collect(Collectors.toList()); return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect); } // <2> 处理 json 请求体调用接口校验失败抛出的异常 @ExceptionHandler(MethodArgumentNotValidException.class) public ResultInfo methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) { List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors(); List collect = fieldErrors.stream().map(o ->o.getDefaultMessage()).collect(Collectors.toList()); return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect); } // <3> 处理单个参数校验失败抛出的异常 @ExceptionHandler(ConstraintViolationException.class) public ResultInfo constraintViolationExceptionHandler(ConstraintViolationException e) { Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations(); List collect = constraintViolations.stream().map(o -> o.getMessage()).collect(Collectors.toList()); return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect); } }
在全局异常处理类中,我们可以写多个异常处理方法,课代表总结了三种参数校验时可能引发的异常:
- 使用form data方式调用接口,校验异常抛出 BindException
- 使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException
- 单个参数校验异常抛出ConstraintViolationException
全局异常处理类可以添加各种需要处理的异常,比如添加一个对Exception.class
的异常处理,当所有ExceptionHandler
都无法处理时,由其记录异常信息,并返回友好提示。
分组校验
如果同一个参数,需要在不同场景下应用不同的校验规则,就需要用到分组校验了。比如:新注册用户还没起名字,我们允许name
字段为空,但是不允许将名字更新为空字符。
分组校验有三个步骤:
- 定义一个分组类(或接口)
- 在校验注解上添加
groups
属性指定分组 Controller
方法的@Validated
注解添加分组类
public interface Update extends Default{ }
public class UserVO { @NotBlank(message = "name 不能为空",groups = Update.class) private String name; // 省略其他代码... }
@PostMapping("update") public ResultInfo update(@Validated({Update.class}) UserVO userVO) { return new ResultInfo().success(userVO); }
递归校验
如果 UserVO 类中增加一个 OrderVO 类的属性,而 OrderVO 中的属性也需要校验,就用到递归校验了,只要在相应属性上增加@Valid
注解即可实现(对于集合同样适用)
public class OrderVO { @NotNull private Long id; @NotBlank(message = "itemName 不能为空") private String itemName; // 省略其他代码... }
public class UserVO {
@NotBlank(message = "name 不能为空",groups = Update.class)
private String name;
//需要递归校验的OrderVO
@Valid
private OrderVO orderVO;
// 省略其他代码...
}
自定义校验
Spring Validation允许用户自定义校验,实现很简单,分两步:
- 自定义校验注解
- 编写校验者类
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {HaveNoBlankValidator.class})// 标明由哪个类执行校验逻辑 public @interface HaveNoBlank { // 校验出错时默认返回的消息 String message() default "字符串中不能含有空格"; Class>[] groups() default { }; Class extends Payload>[] payload() default { }; /** * 同一个元素上指定多个该注解时使用 */ @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented public @interface List { NotBlank[] value(); } }
public class HaveNoBlankValidator implements ConstraintValidator<HaveNoBlank, String> { @Override public boolean isValid(String value, ConstraintValidatorContext context) { // null 不做检验 if (value == null) { return true; } if (value.contains(" ")) { // 校验失败 return false; } // 校验成功 return true; } }