SpringBoot如何优雅的处理参数校验

SpringBoot如何优雅的处理参数校验

引言

对于一个web项目而言后端经常需要对前端参数进行校验,传统方式常常是在controller中使用大量if else进行参数合法性校验,这样做的缺点显而易见,便不在赘述。

解决方案

对于以上问题,SpringBoot项目我列举了三种处理方式:

对于前端传参的实体我们可以写这样一个类例如UserParam,然后使用javax.validation提供的注解进行参数条件限制

@Data
public class UserParam {
    private Integer id;

    @NotBlank(message="用户名不能为空")
    private String username;

    @Pattern(regexp = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\\\.[a-zA-Z0-9_-]+)+$", message = "邮箱地址格式不正确")
    private String email;

    @Pattern(regexp="^((13[0-9])|(15[^4,\\D])|(18[0,5-9]))\\d{8}$", message="手机号格式不正确")
    private String telephone;

    @Max(value=144, message="长度必须小于144个字符")
    private String description;
}

基于这样一个类,提出了两种关于aop的方式和一种异常处理的方式

AOP参数校验方式一

写一个AOP参数校验的类:

@Aspect
@Component
public class ValidParamAdvice {
    /**
     * 定义切点,切点为xyz.guqing.storycard.controller包和子包里任意方法的执行
     */
    @Pointcut("execution(* xyz.guqing.storycard.controller..*(..))")
    public void controllerPointCut() {
    }

    @Around("controllerPointCut() && args(.., bindingResult)")
    public Object doAround(ProceedingJoinPoint joinPoint, BindingResult bindingResult) throws Throwable {
        // 收集并返回参数校验错误信息,这里根据实际返回结果类自己封装
        if(bindingResult.hasErrors()) {
            Map<String, String> errors = new HashMap<>(16);
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();
            fieldErrors.forEach(fieldError -> {
                errors.put(fieldError.getField(), fieldError.getDefaultMessage());
            });
            return errors.toString();
        }

        return joinPoint.proceed(joinPoint.getArgs());
    }
}

然后在controller中使用如下,只需要在参数UserParam前添加@Valid即可完成参数校验

@RestController
@CrossOrigin
@RequestMapping("/user")
public class UserController {

    @PostMapping("/advice-valid")
    public String exceptionAdviceValidUser(@RequestBody @Valid UserParam userParam) {
        return userParam.toString();
    }
}

AOP参数校验方式二

同样写一个aop参数处理类

@Aspect
@Component
public class ValidParamAdvice {
    /**
     * 定义切点,切点为xyz.guqing.storycard.controller包和子包里任意方法的执行
     */
    @Pointcut("execution(* xyz.guqing.storycard.controller..*(..))")
    public void controllerPointCut() {
    }

    @Around("controllerPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        BindingResult bindingResult = null;
        // 使用遍历参数的方式找到BindingResult
        for(Object arg:joinPoint.getArgs()){
            if(arg instanceof BindingResult){
                bindingResult = (BindingResult) arg;
            }
        }

        if(bindingResult != null && bindingResult.hasErrors()){
            Map<String, String> errors = new HashMap<>(16);
            List<FieldError> fieldErrors = bindingResult.getFieldErrors();
            fieldErrors.forEach(fieldError -> {
                errors.put(fieldError.getField(), fieldError.getDefaultMessage());
            });
            return errors.toString();
        }
        return joinPoint.proceed();
    }
}

controller中使用

@RestController
@CrossOrigin
@RequestMapping("/user")
public class UserController {
    @PostMapping("/aop-valid")
    public String aopValidUser(@RequestBody @Valid UserParam userParam, BindingResult result) {
        return userParam.toString();
    }
}

如上,不同点在于不仅需要在UserParam参数前写@Valid,还需要多添加一个参数BindingResult result才可以

全局异常处理参数校验

写这样一个异常处理类

@RestControllerAdvice
@Slf4j
public class ValidExceptionAdvice {

    /**
     * 当使用@Valid不带@RequestBody request参数时:
     * 对象验证失败,验证将引发BindException而不是MethodArgumentNotValidException
     * @param e 参数绑定异常
     * @return 返回参数校验失败的错误信息
     */
    @ExceptionHandler(BindException.class)
    public Object validExceptionHandler(BindException e){
        // 将错误的参数的详细信息封装到统一的返回实体
        return validParam(e.getBindingResult());
    }

    /**
     * 使用@Valid并且带有@RequestBody request参数时
     * 参数教研失败将抛出MethodArgumentNotValidException异常,由此方法捕获处理
     * @param e 方法参数校验失败的异常
     * @return 返回校验失败错误信息
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Object validExceptionHandler(MethodArgumentNotValidException e){
        return validParam(e.getBindingResult());
    }

    private Object validParam(BindingResult bindResult) {
        List<FieldError> fieldErrors = bindResult.getFieldErrors();
        Map<String, String> map = new HashMap<>(16);
        fieldErrors.forEach(fieldError -> {
            map.put(fieldError.getField(), fieldError.getDefaultMessage());
        });

        return map;
    }
}

controller中使用

@RestController
@CrossOrigin
@RequestMapping("/user")
public class UserController {

    @PostMapping("/advice-valid")
    public String exceptionAdviceValidUser(@RequestBody @Valid UserParam userParam) {
        return userParam.toString();
    }
}

只需要在需要进行参数校验的参数UserParam前添加@Valid即可,当校验不通过时会被ValidExceptionAdvice类拦截处理

@Valid注解说明

javaJSR303声明了@Valid这类接口,而Hibernate-validator对其进行了实现,因此具体注解使用参考Hibernate Validator,下面列举一些常见注解的使用说明:

注解备注
@Null只能为null
@NotNull必须不为null
@Max(value)必须为一个不大于 value 的数字
@Min(value)必须为一个不小于 value 的数字
@AssertFalse必须为false
@AssertTrue必须为true
@DecimalMax(value)必须为一个小于等于 value 的数字
@DecimalMin(value)必须为一个大于等于 value 的数字
@Digits(integer,fraction)必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Past必须是 日期 ,且小于当前日期
@Future必须是 日期 ,且为将来的日期
@Size(max,min)字符长度必须在min到max之间
@Pattern(regex=,flag=)必须符合指定的正则表达式
@NotEmpty必须不为null且不为空(字符串长度不为0、集合大小不为0)
@NotBlank必须不为空(不为null、去除首位空格后长度不为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格
@Email必须为Email,也可以通过正则表达式和flag指定自定义的email格式