springboot系列学习笔记全部文章请移步值博主专栏**: spring boot 2.X/spring cloud Greenwich。
由于是一系列文章,所以后面的文章可能会使用到前面文章的项目。springboot系列代码全部上传至GitHub:https://github.com/liubenlong/springboot2_demo
本系列环境:Java11;springboot 2.1.1.RELEASE;springcloud Greenwich.RELEASE;MySQL 8.0.5;
本文介绍在Spring Boot中实现对controller请求的数据进行全局校验。
本文核心为普通GET/POST请求参数校验;BEAN的嵌套校验;集合校验;全局同意异常处理。
@Valid 被注释的元素是一个对象,需要检查此对象的所有字段值
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式
@Email 被注释的元素必须是电子邮箱地址
@Length(min=, max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=, max=) 被注释的元素必须在合适的范围内
@NotBlank 被注释的字符串的必须非空
@URL(protocol=,host=, port=, regexp=, flags=) 被注释的字符串必须是一个有效的url
主要区分下@NotNull @NotEmpty @NotBlank 3个注解的区别:
首先是两controller,两个方法,用于校验get和post请求。其实是一样的,只需要添加@Validated
即可。
@RestController("validateController")
@Slf4j
public class ValidateController {
@RequestMapping(value = "posttest", method = RequestMethod.POST)
public String test1(@Validated @RequestBody StudentVO studentVO) {
return studentVO.getName();
}
@RequestMapping(value = "gettest", method = RequestMethod.GET)
public String test2(@Validated FatherVO father) {
return father.getName();
}
}
入参VO , 其中包含了普通校验;嵌套校验;list集合校验
@Data
@NoArgsConstructor
public class StudentVO {
@NotBlank(message = "name不可为空")
private String name;
@Range(min = 0, max = 100, message = "年龄必须再[0-100]之间")
private int age;
@Email(message="非法邮件地址")
private String email;
@Pattern(regexp="^(\\d{6})(\\d{4})(\\d{2})(\\d{2})(\\d{3})([0-9]|X)$",message="身份证号不合规")
private String cardNo;
//自定义注解验证参数
@Money
private Double balance;
@NotNull(message = "father不可为空")
@Valid // 嵌套校验
private FatherVO fatherVO;
@NotEmpty
@Size(min = 1, max = 3, message = "fatherVOS长度必须在[1-3]之间")
@Valid//list也可以嵌套校验
private List<FatherVO> fatherVOS;
}
@Data
@NoArgsConstructor
public class FatherVO {
@NotBlank(message = "父亲名字不可为空")
private String name;
@NotBlank(message = "父亲工作不可为空")
private String work;
}
这一步是关键,因为仅仅校验,提示信息可能不够友好,且异常信息也有很多种,所以我这里提供了统一处理。
更重要的是统一了错误返回码,对于接口测试开发很有用。
/**
* 参数校验统一处理类
*/
@ControllerAdvice
@Slf4j
public class RestApiExceptionAdvice {
@ExceptionHandler({BindException.class})
@ResponseBody
public R handleValidationException(BindException e) {
StringBuilder bindErrorBuilder = new StringBuilder();
StringBuilder noBindErrorBuilder = new StringBuilder();
List<FieldError> fieldErrors = e.getFieldErrors();
fieldErrors.forEach(fieldError -> {
if (fieldError.isBindingFailure()) {
bindErrorBuilder.append(fieldError.getField()).append(",");
} else {
noBindErrorBuilder.append(fieldError.getDefaultMessage()).append(",");
}
});
String bindError = bindErrorBuilder.toString();
String bindErrorMsg = StringUtils.isBlank(bindError) ? null : "参数有误:" + bindError.substring(0, bindError.length() - 1);
String noBindError = noBindErrorBuilder.toString();
String noBindErrorMsg = StringUtils.isBlank(noBindError) ? null : noBindError.substring(0, noBindError.length() - 1);
String msg = StringUtils.isBlank(bindErrorMsg) ?
noBindErrorMsg :
bindErrorMsg + (StringUtils.isBlank(noBindErrorMsg) ? "" : "," + noBindErrorMsg);
log.warn("参数校验不通过,msg: {}", msg, e);
return new R<>(ParamErrorCode.PARAM_ERROR, msg, null);
}
@ExceptionHandler({ConstraintViolationException.class,
MethodArgumentNotValidException.class,
ServletRequestBindingException.class,
MethodArgumentTypeMismatchException.class,
IllegalArgumentException.class,
HttpMessageNotReadableException.class})
@ResponseBody
public R handleValidationException(Exception e) {
String msg = "";
if (e instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException t = (MethodArgumentNotValidException) e;
msg = t.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(","));
} else if (e instanceof ConstraintViolationException) {
ConstraintViolationException t = (ConstraintViolationException) e;
msg = t.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(","));
} else if (e instanceof MissingServletRequestParameterException) {
MissingServletRequestParameterException t = (MissingServletRequestParameterException) e;
msg = t.getParameterName() + " 不能为空";
} else if (e instanceof MissingPathVariableException) {
MissingPathVariableException t = (MissingPathVariableException) e;
msg = t.getVariableName() + " 不能为空";
} else if (e instanceof IllegalArgumentException) {
IllegalArgumentException t = (IllegalArgumentException) e;
msg = t.getMessage();
} else {
msg = "参数有误";
}
log.warn("参数校验不通过,msg: {}", msg, e);
return new R<>(ParamErrorCode.PARAM_ERROR, msg, null);
}
/**
* 统一拦截所有服务端抛出的异常
*
* @param e
* @return
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public R handleException(Exception e) {
log.error("服务器发生异常!", e);
return new R<>(ParamErrorCode.ERROR, "服务器发生异常", e.getMessage());
}
}
public class R<T> {
private Integer code;
private String msg;
private T data;
public R() {
}
public R(ParamErrorCode returnCode, String msg, T data) {
this.code = returnCode.getCode();
this.msg = msg;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer 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;
}
}
public enum ParamErrorCode {
OK(200),
PARAM_ERROR(100),
BIZ_ERROR(300),
ERROR(500),
;
private Integer code;
ParamErrorCode(Integer code) {
this.code = code;
}
public Integer getCode() {
return code;
}
}
有时候默认的一些校验规则,可能无法满足我们复杂的业务需求。此时可以通过自定义注解的方式来实现参数的校验。
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MoneyValidator.class)
public @interface Money {
String message() default "不是金额形式";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class MoneyValidator implements ConstraintValidator<Money, Double> {
private String moneyReg = "^\\d+(\\.\\d{1,2})?$";//表示金额的正则表达式
private Pattern moneyPattern = Pattern.compile(moneyReg);
public void initialize(Money money) {
}
public boolean isValid(Double value, ConstraintValidatorContext arg1) {
if (value == null)
return true;
return moneyPattern.matcher(value.toString()).matches();
}
}
在VO中直接使用即可
//自定义注解验证参数
@Money
private Double balance;
上面都是将参数封装为Bean的形式传参,然后进行校验。有时候参数比较少,直接在方法参数传递:
@RequestMapping(value = "nomal", method = RequestMethod.GET)
public String test3(@NotBlank(message = "name 不可为空") String name,
@NotNull(message = "age不可为空") @Min(value = 2, message = "最小2") Integer age) {
return name;
}
这种默认情况下,是无法参与检验的。不过好在hibernate validation提供了对方法参数的校验:https://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/#section-validating-executable-constraints。
根据官方文档,只需要写个代理类进行处理即可
@Component
@Aspect
public class RequestParamValidAspect {
@Pointcut("execution(* com.example.controller.*.*(..))")
public void controllerBefore() {
}
@Before("controllerBefore()")
public void before(JoinPoint point) {
Object target = point.getThis();
// 获得切入方法参数
Object[] args = point.getArgs();
// 获得切入的方法
Method method = ((MethodSignature) point.getSignature()).getMethod();
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
ExecutableValidator executableValidator = factory.getValidator().forExecutables();
// 执行校验,获得校验结果
Set<ConstraintViolation<Object>> validResult = executableValidator.validateParameters(target, method, args);
//如果有校验不通过的
if (!validResult.isEmpty()) {
StringBuilder builder = new StringBuilder();
validResult.forEach(objectConstraintViolation -> {
builder.append(objectConstraintViolation.getMessage()).append(";");
});
String result = builder.toString();
throw new IllegalArgumentException(result.substring(0, result.length() - 1));
}
}
}
springboot系列学习笔记全部文章请移步值博主专栏**: spring boot 2.X/spring cloud Greenwich。
由于是一系列文章,所以后面的文章可能会使用到前面文章的项目。springboot系列代码全部上传至GitHub:https://github.com/liubenlong/springboot2_demo
本系列环境:Java11;springboot 2.1.1.RELEASE;springcloud Greenwich.RELEASE;MySQL 8.0.5;