前言

参数校验在日常开发时是很常用的操作。为了避免null指针、非法数据存入数据库,我们通常要进行参数校验。

导包只需要spring-boot-starter-validation和web即可

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

测试DTO

public class TestDTO {

    @NotNull(message = "姓名不能为空")
    private String name;
     
    @NotNull(message = "年龄不能为空")
    @Max(value = 120, message = "最大值不能大于120")
    @Min(value = 0, message = "最小值不能低于0")
    private Integer age;

测试

@RestController
@RequestMapping("/test")
public class TestController {
    /**
     * 测试
     * `@Valid` 表示对这个对象校验
     * `BindingResult` 获取的是校验的结果,这个对象有许多方法获取校验信息,可以自定义返回信息
     * 
     *
     * @param dto
     * @param bindingResult
     * @return
     */
    @PostMapping("/post")
    public Map<Object, Object> test(@Validated @RequestBody TestDTO dto, BindingResult bindingResult) {
        Map<Object, Object> res = new HashMap<>();
        if (bindingResult.hasErrors()) {
            res.put("status", 400);
            res.put("msg", bindingResult.getFieldError().getDefaultMessage());
            return res;
        } else {
            res.put("status", 200);
            res.put("msg", "ok");
            res.put("data", dto);
            return res;
        }

    }
}

自定义

1.创建自定义注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
@Constraint(validatedBy = FlagValidatorClass.class) // 绑定对应校验器
public @interface FlagValidator {
    String[] value() default {};

    String message() default "flag is not found";
     
    Class<?>[] groups() default {};
     
    Class<? extends Payload>[] payload() default {};
}

2.编写对应校验器

/**
 * 标志位校验器
 */
   public class FlagValidatorClass implements ConstraintValidator<FlagValidator, Integer> {
    private String[] values;

    /**
     * 初始化
     *
     * @param flagValidator 注解上设置的值
     */
      @Override
      public void initialize(FlagValidator flagValidator) {
        this.values = flagValidator.value();
      }

    /**
     * 校验
     *
     * @param value  被校验的值,即输入
     * @param constraintValidatorContext 校验上下文
     * @return 返回true证明校验通过,false校验失败
     */
       @Override
       public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
        boolean isValid = false;
        // 当value为null,校验失败
        if (value == null) {
            return false;
        }
        //遍历校验
        for (int i = 0; i < values.length; i++) {
            if (values[i].equals(String.valueOf(value))) {
                isValid = true;
                break;
            }
        }
        return isValid;
       }
      }

这样子就完成了自定义参数校验。当然了,我们可以在isValid方法中添加更加复杂的校验逻辑,如正则匹配等方法,这里就不展开了。

测试一下,先在DTO中添加flag属性,标上注解。

@NotNull(message = "标识位不能为空")
    @FlagValidator(value = {"0", "1"}, message = "标志位有误")
    private Integer flag;

效果:

System.out.println(bindingResult);
// 成功时,这没什么好说的
org.springframework.validation.BeanPropertyBindingResult: 0 errors

// 失败时
org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'testDTO' on field 'flag': rejected value [12]; 
codes [FlagValidator.testDTO.flag,FlagValidator.flag,FlagValidator.java.lang.Integer,FlagValidator]; 
arguments 
[org.springframework.context.support.DefaultMessageSourceResolvable: codes [testDTO.flag,flag]; 
arguments []; 
default message [flag],[Ljava.lang.String;@1017351]; 
default message [标志位有误]

我们可以看到一些属性,Field、Default message、Rejected value。我们可以通过他的方法获取对应的值。获取Error,getFieldError默认返回第一个错误。getAllErrors返回错误列表。

获取对应的属性,构造我们自己的校验返回内容。通过Java8的stream方法我们可以简单的获取到报错信息并返回前端;

Object[] objects =  bindingResult.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).toArray();

优化
每次都要写bindingResult以及判断方法其实也挺烦人的,所以再写一个工具类,constraintViolations是一个Set集合,里面包含了错误的信息,我们可以自定义一个异常抛出,并定义全局异常处理器处理请求返回。

public class ValidatorUtils {
    private static Validator validator;

    static {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }
     
    /**
     * 校验对象
     *
     * @param object 待校验对象
     * @param groups 待校验的组
     * @throws ApiException 校验不通过,则报ApiException异常
     */
    public static void validateEntity(Object object, Class<?>... groups) {
        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
        if (!constraintViolations.isEmpty()) {
            constraintViolations.forEach(o -> {
                throw new ApiException(400, o.getMessage());
            });
        }
    }
}

修改TestController,可以看到简洁了许多。效果是一样的,就不演示了。

@RestController
@RequestMapping("/test")
public class TestController {
    /**
     * 测试
     * @param dto
     * @param bindingResult
     * @return
     */
    @PostMapping("/post")
    public Map<Object, Object> test(@RequestBody TestDTO dto) {
            ValidatorUtils.validateEntity(dto);

            Map<Object, Object> res = new HashMap<>();
        
            res.put("status", 200);
            res.put("msg", "ok");
            res.put("data", dto);
            return res;
    }
}


数据传递到spring中的执行过程大致为:前端通过http协议将数据传递到spring,spring通过HttpMessageConverter类将流数据转换成Map类型,然后通过ModelAttributeMethodProcessor类对参数进行绑定到方法对象中,并对带有@Valid或@Validated注解的参数进行参数校验,对参数进行处理和校验的方法为ModelAttributeMethodProcessor.resolveArgument(…),通过查看源码,当BindingResult中存在错误信息时,会抛出BindException异常,BindException实现了BindingResult接口(BindResult是绑定结果的通用接口, BindResult继承于Errors接口),所以该异常类拥有BindingResult所有的相关信息,因此我们可以通过捕获该异常类,对其错误结果进行分析和处理。这样,我们对是content-type类型为form(表单)类型的请求的参数校验的异常处理就解决了。

对于不同的传输数据的格式spring采用不同的HttpMessageConverter(http参数转换器)来进行处理.以下是对HttpMessageConverter进行简介:

HTTP 请求和响应的传输是字节流,意味着浏览器和服务器通过字节流进行通信。但是,使用 Spring,controller 类中的方法返回纯 String 类型或其他 Java 内建对象。如何将对象转换成字节流进行传输?

在报文到达SpringMVC和从SpringMVC出去,都存在一个字节流到java对象的转换问题。在SpringMVC中,它是由HttpMessageConverter来处理的。

当请求报文来到java中,它会被封装成为一个ServletInputStream的输入流,供我们读取报文。响应报文则是通过一个ServletOutputStream的输出流,来输出响应报文。

针对不同的数据格式,springmvc会采用不同的消息转换器进行处理,当使用json作为传输格式时,springmvc会采用MappingJacksonHttpMessageConverter消息转换器, 而且底层在对参数进行校验错误时,抛出的是MethodArgumentNotValidException异常,因此我们需要对BindException和MethodArgumentNotValidException进行统一异常管理。

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
 
    /**
     * 参数验证异常(前端json格式提交)
     *
     * @param e 异常
     * @return 响应实体
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String handleParameterException(MethodArgumentNotValidException e) {
        log.error(e.getMessage(), e);
        String errorMessage = CommonUtil.getErrorMessage(e.getBindingResult());
        return errorMessage;
    }
 
    /**
     * 参数验证异常(前端form表单格式提交)
     *
     * @param exception 异常
     * @return http响应对象
     */
    @ExceptionHandler(BindException.class)
    public String handlerBindException(BindException exception) {
        String errorMessage = CommonUtil.getErrorMessage(exception.getBindingResult());
        return errorMessage;
    }
 
}


常用注解
@Null 只能为null Object
@NotNull 不能为null Object
@NotEmpty [字符串、数组、集合、Map]不能为null&长度不为0 字符串、Object[]、基础类型数组、[Collection、Map]接口实现类
@NotBlank 不能为null&去除首尾空格后,不能为空字符串 字符串
@Max [字符串长度、数字]不能大于value 数值、数值格式的字符串
@Min [字符串长度、数字]不能小于value 数值、数值格式的字符串
@Size [字符串、数组、集合、Map]长度为min至max之间 字符串、Object[]、基础类型数组、[Collection、Map]接口实现类
@AssertFalse 只能为false boolean
@AssertTrue 只能为true boolean
@DecimalMax 不能大于等于value; inclusive为false时, 不能大于value 数值、数值格式的字符串
@DecimalMin 不能小于等于value; inclusive为false时, 不能小于value 数值、数值格式的字符串
@Digits 整数位数不能大于integer, 小数位数不能大于fraction 数值、数值格式的字符串
@Pattern 根据正则表达式(regexp)进行效验 字符串
@Negative 只能为负数 数值
@NegativeOrZero 只能为负数或0 数值
@Positive 只能为正数 数值
@PositiveOrZero 只能为正数或0 数值
@Future 只能是未来时间 时间
@FutureOrPresent 只能是未来时间或当前时间 时间
@Past 只能是过去时间 时间
@PastOrPresent 只能是过去时间或当前时间 时间
@Email 验证邮箱格式, 可以使用regex指定格式 字符串
@Length 字符串长度为min至max之间 字符串
@Range [字符串、数字]大小在min和max之间,字符串必须是数字格式 数值、数值格式的字符串