一、为什么使用Validation来验证参数

通常我们在使用spring框架编写接口时,对于部分接口的参数我们要进行判空或者格式校验来避免程序出现异常。那是我们一般都是使用if-else逐个对参数进行校验。这种方法按逻辑来说也是没有问题的,同样也能实现预期效果。但是,这样的代码从可读性以及美观程序来看,是非常糟糕的。那么,我们就可以使用@valid注解来帮助我们优雅的校验参数。

二、如何使用Validation相关注解进行参数校验

  1. 为实体类中的参数或者对象添加相应的注解;
  2. 在控制器层进行注解声明,或者手动调用校验方法进行校验;
  3. 对异常进行处理;

三、Validation类的相关注解及描述

验证注解验证的数据类型说明
@AssertFalseBoolean,boolean验证注解的元素值是false
@AssertTrueBoolean,boolean验证注解的元素值是true
@NotNull任意类型验证注解的元素值不是null
@Null任意类型验证注解的元素值是null
@Min(value=值)BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型验证注解的元素值大于等于@Min指定的value值
@Max(value=值)和@Min要求一样验证注解的元素值小于等于@Max指定的value值
@DecimalMin(value=值)和@Min要求一样验证注解的元素值大于等于@ DecimalMin指定的value值
@DecimalMax(value=值)和@Min要求一样验证注解的元素值小于等于@ DecimalMax指定的value值
@Digits(integer=整数位数, fraction=小数位数)和@Min要求一样验证注解的元素值的整数位数和小数位数上限
@Size(min=下限, max=上限)字符串、Collection、Map、数组等验证注解的元素值的在min和max(包含)指定区间之内,如字符长度、集合大小
@Pastjava.util.Date,java.util.Calendar;Joda Time类库的日期类型验证注解的元素值(日期类型)比当前时间早
@Future与@Past要求一样验证注解的元素值(日期类型)比当前时间晚
@NotBlankCharSequence子类型验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的首位空格
@Length(min=下限, max=上限)CharSequence子类型验证注解的元素值长度在min和max区间内
@NotEmptyCharSequence子类型、Collection、Map、数组验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@Range(min=最小值, max=最大值)BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型验证注解的元素值在最小值和最大值之间
@Email(regexp=正则表达式,flag=标志的模式)CharSequence子类型(如String)验证注解的元素值是Email,也可以通过regexp和flag指定自定义的email格式
@Pattern(regexp=正则表达式,flag=标志的模式)String,任何CharSequence的子类型验证注解的元素值与指定的正则表达式匹配
@Valid任何非原子类型指定递归验证关联的对象如用户对象中有个地址对象属性,如果想在验证用户对象时一起验证地址对象的话,在地址对象上加@Valid注解即可级联验证

注意:

实体类中添加 @Valid 相关验证注解,并在注解中添加出错时的响应消息。

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;

@Data
public class User {
    @NotBlank(message = "姓名不能为空")
    private String username;
    @NotBlank(message = "密码不能为空")
    @Length(min = 6, max = 16, message = "密码长度为6-16位")
    private String password;
    @Pattern(regexp = "0?(13|14|15|17|18|19)[0-9]{9}", message = "手机号格式不正确")
    private String phone;
    // 嵌套必须加 @Valid,否则嵌套中的验证不生效
    @Valid
    @NotNull(message = "userinfo不能为空")
    private UserInfo userInfo;
}

如果是嵌套的实体对象,并且也要校验该对象,则需要在最外层属性上添加 @Valid 注解

此处只列出Validator提供的大部分验证约束注解,请参考hibernate validator官方文档了解其他验证约束注解和进行自定义的验证约束注解定义。

@Validated和@Valid在嵌套验证功能上的区别:

@Validated:用在方法入参上无法单独提供嵌套验证功能。不能用在成员属性(字段)上,也无法提示框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。

@Valid:用在方法入参上无法单独提供嵌套验证功能。能够用在成员属性(字段)上,提示验证框架进行嵌套验证。能配合嵌套验证注解@Valid进行嵌套验证。

@Validated和@Valid的区别和使用,包括嵌套检验可以参考:

https://blog.csdn.net/qq_27680317/article/details/79970590

https://blog.csdn.net/qq_45151158/article/details/112349233?spm=1001.2014.3001.5501

四、使用Validation API进行参数效验步骤

对于GET请求的参数可以使用@validated注解配合上面相应的注解进行校验或者按照原先if-else方式进行效验.

而对于POST请求,大部分是以表单数据即以实体对象为参数,可以使用@Valid注解方式进行效验(可以简单概括一下,如果接口使用实体类接收参数,那么要用@Valid注解该对象,并且在对象的各属性上添加上方表格里的注解;如果接口直接使用某个字段来接收参数,那么在该字段前添加表格里的注解即可,可参考下文代码)。如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理。

img

五、Spring Validation的三种校验方式

第一种(适用于生产):在Controller方法参数前加@Valid注解——校验不通过时直接抛异常,get请求直接在平面参数前添加相应的校验规则注解,使用这种的话一般结合统一异常处理进行处理,后面会主要介绍这种方式,可以直接看六部分。

第二种:在Controller方法参数前加@Valid注解,参数后面定义一个BindingResult类型参数——执行时会将校验结果放进bindingResult里面,用户自行判断并处理。

/**
 * 将校验结果放进BindingResult里面,用户自行判断并处理
 *
 * @param userInfo
 * @param bindingResult
 * @return
 */
@PostMapping("/testBindingResult")
public String testBindingResult(@RequestBody @Valid UserInfo userInfo, BindingResult bindingResult) {
    // 参数校验
    if (bindingResult.hasErrors()) {
        String messages = bindingResult.getAllErrors()
                .stream()
                .map(ObjectError::getDefaultMessage)
                .reduce((m1, m2) -> m1 + ";" + m2)
                .orElse("参数输入有误!");
        //这里可以抛出自定义异常,或者进行其他操作
        throw new IllegalArgumentException(messages);
    }
    return "操作成功!";
}

这里我们是直接抛出了异常,如果没有进行全局异常处理的话,接口将会返回如下信息:

img

第三种:用户手动调用对应API执行校验——Validation.buildDefault ValidatorFactory().getValidator().validate(xxx)

这种方法适用于校验任意一个有valid注解的实体类,并不仅仅是只能校验接口中的参数;

这里我提取出一个工具类,如下:

import org.springframework.util.CollectionUtils;

import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.Validation;
import java.util.Set;

/**
 * 手动调用api方法校验对象
 */
public class MyValidationUtils {
    public static void validate(@Valid Object value) {
        Set<ConstraintViolation<@Valid Object>> validateSet = Validation.buildDefaultValidatorFactory()
                .getValidator()
                .validate(value);
        if (!CollectionUtils.isEmpty(validateSet)) {
            String messages = validateSet.stream()
                    .map(ConstraintViolation::getMessage)
                    // 归约实现字符串合并
                    .reduce((m1, m2) -> m1 + ";" + m2)
                    .orElse("参数输入有误!");
            throw new IllegalArgumentException(messages);
        }
    }
}

六、Spring Boot项目中实战演练

1、安装依赖

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

2、自定义异常

继承RuntimeException,要知道,spring 对于 RuntimeException 异常才会进行事务回滚,所以要继承RuntimeException。

@Data
@EqualsAndHashCode(callSuper = true)
public class MyException extends RuntimeException {

    private Integer code;

    public MyException(ResultEnum resultEnum) {
        super(resultEnum.getMsg());
        this.code = resultEnum.getCode();
    }

    public MyException(Integer code, String msg) {
        super(msg);
        this.code = code;
    }
}

3、定义三个异常拦截器

  1. ValidationExceptionHandle:校验异常统一拦截返回,定义在最前面

  2. OtherExceptionHandle:其他异常拦截,用于项目中其他异常的拦截返回

  3. FinalExceptionHandle:最终异常拦截,最后一道防线。

@RestControllerAdvice是帮助我们把信息转成json格式返回

@ResponseBody是将方法中的字符串转成json格式同一返回,一般该方法返回值为Object

三个异常拦截器都使用order控制顺序,小的排在前面。

注意:过滤器中的异常无法被拦截

ValidationExceptionHandle.java,验异常统一拦截返回,定义在最前面

@RestControllerAdvice
@Order(80)
@Slf4j
public class ValidationExceptionHandle extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        logger.error(ex.getMessage());
        return validExceptionCommon(ex.getBindingResult());
    }

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        logger.error(ex.getMessage());
        return validExceptionCommon(ex.getBindingResult());
    }

    /**
     * 校验异常统一返回格式
     * @param result
     * @return
     */
    private ResponseEntity<Object> validExceptionCommon(BindingResult result){
        ResultVo<Object> resultVo = new ResultVo<>();
        if (result.hasErrors()) {
            List<ObjectError> errors = result.getAllErrors();
            for (ObjectError error : errors) {
                FieldError fieldError = (FieldError) error;
                resultVo.setCode(500);
                resultVo.setMsg(fieldError.getDefaultMessage());
            }

        }
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(resultVo);
    }
}

OtherExceptionHandle.java,其他异常拦截,用于项目中其他异常的拦截返回。

@ControllerAdvice
@Order(90)
public class OtherExceptionHandle {

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 无
     * @param e
     * @return
     */
    @ExceptionHandler(ValidationException.class)
    @ResponseBody
    public Object handleValidationException(ValidationException e) {
        logger.error(e.getMessage(), e);
        return e;
    }

    /**
     * 违反约束异常处理
     * @param e
     * @return
     */
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseBody
    public Object handConstraintViolationException(ConstraintViolationException e) {
        logger.error(e.getMessage(), e);
        return e;
    }
}

FinalExceptionHandle.java,最终异常拦截,最后一道防线。

@RestControllerAdvice
@Order(100)
public class FinalExceptionHandle{

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Object  handle(Exception e) {
        logger.error(e.getMessage(), e);
        Map<String,Object> map = new HashMap<>();
        if (e instanceof MyException) {
            MyException myException = (MyException) e;
            map.put("code",500);
            map.put("msg",myException.getMessage());
            return map;
        } else {
            e.printStackTrace();
            map.put("code",500);
            map.put("msg","出错啦");
            return map;
        }
    }
}

七、ExceptionResolver与@ControllerAdvice的选择

在基于Spring框架的项目中,可以通过在ApplicationContext-MVC.xml(即SpringMVC配置)文件中配置 ExceptionResolver 的bean ,来配置 全局捕获异常处理 类,然后自定义异常处理类处理。注意如果是spring配置文件中定义过的ExceptionResolver 类,不需要添加@Component。如果是SpringBoot 则需要。这是因为springboot没有自定义配置全局异常捕获类,所以需要添加@Component,来标识该类为Bean。

异常处理可以分为三种:

第一种是进入@Controller标识的方法前 产生的异常,例如URL地址错误。这种异常处理需要 异常处理类通过实现 ErrorController 来处理。

第二种是进入Controller时,但还未进行逻辑处理时 产生的异常,例如传入参数数据类型错误。这种异常处理需要用@ControllerAdvice标识并处理,建议继承 ResponseEntityExceptionHandler 来处理,该父类包括了很多已经被@ExceptionHandler 注解标识的方法,包括一些参数转换,请求方法不支持等类型等等。

第三种时进入Controller,进行逻辑处理时产生的异常,例如NullPointerException异常等。这种异常处理也可以用@ControllerAdvice来标识并进行处理,也建议继承ResponseEntityExceptionHandler 处理, 这里我们可以用@ExceptionHandler 自定义捕获的异常并处理。

以上三种情况都是restful的情况,结果会返回一个Json。
  • 如果希望返回跳转页面,则需要实现HandlerExceptionResolver类来进行异常处理并跳转。
  • 注意@ControllerAdvice需要搭配@ExceptionHandler来使用,自定义捕获并处理异常。
  • @ControllerAdvice一样可以做页面跳转,返回String不要加@ResponseBody

八、validation校验注解作用域

  • @Validated @Valid —— entity(实体)
  • @NotBlank —— String
  • @NotNull —— Integer
  • @NotEmpty —— java.util.Collection(集合)

九、validation参数校验三种异常情况

1、BindException

BindException:作用于 @Validated @Valid 注解,仅对于表单提交有效,对于以json格式提交将会失效。

/**
 * BindException异常处理
 * <p>BindException: 作用于@Validated @Valid 注解,仅对于表单提交有效,对于以json格式提交将会失效</p>
 *
 * @param e BindException异常信息
 * @return 响应数据
 */
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(BindException.class)
public Result<?> bindExceptionHandler(BindException e) {
    String msg = e.getBindingResult().getFieldErrors()
            .stream()
            .map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage()))
            .reduce((x, y) -> String.format("%s; %s", x, y))
            .orElse("参数输入有误");
    log.error("BindException异常,参数校验异常:{}", msg);
    return Result.verifyError(msg);
}

2、MethodArgumentNotValidException

MethodArgumentNotValidException:作用于 @Validated @Valid 注解,前端提交的方式为json格式有效,出现异常时会被该异常类处理。

/**
 * MethodArgumentNotValidException-Spring封装的参数验证异常处理
 * <p>MethodArgumentNotValidException:作用于 @Validated @Valid 注解,接收参数加上@RequestBody注解(json格式)才会有这种异常。</p>
 *
 * @param e MethodArgumentNotValidException异常信息
 * @return 响应数据
 */
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
    String msg = e.getBindingResult().getFieldErrors()
        .stream()
        .map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage()))
        .reduce((x, y) -> String.format("%s; %s", x, y))
        .orElse("参数输入有误");
    log.error("MethodArgumentNotValidException异常,参数校验异常:{}", msg);
    return Result.verifyError(msg);
}

3、ConstraintViolationException

ConstraintViolationException:作用于 @NotBlank @NotNull @NotEmpty 注解,校验单个String、Integer、Collection等参数异常处理。

/**
 * ConstraintViolationException-jsr规范中的验证异常,嵌套检验问题
 * <p>ConstraintViolationException:作用于 @NotBlank @NotNull @NotEmpty 注解,校验单个String、Integer、Collection等参数异常处理。</p>
 * <p>注:Controller类上必须添加@Validated注解,否则接口单个参数校验无效</p>
 *
 * @param e ConstraintViolationException异常信息
 * @return 响应数据
 */
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = ConstraintViolationException.class)
public Result<?> constraintViolationExceptionHandler(ConstraintViolationException e) {
    String msg = e.getConstraintViolations()
        .stream()
        .map(ConstraintViolation::getMessage)
        .collect(Collectors.joining("; "));

    log.error("ConstraintViolationException,参数校验异常:{}", msg);
    return Result.verifyError(msg);
}

注:Controller类上必须添加@Validated注解,否则接口参数校验无效

4、统一异常处理完整代码

Spring validation入参验证框架,一般在Controller类加上@Validated注解(可检验集合参数),接口方法对应的dto加上@Valid注解,然后直接对以上三个异常进行全局捕获处理即可。

ValidationExceptionHandle.java完整代码:

package com.tangsm.spring.boot.validation.handler;


import com.tangsm.spring.boot.validation.domain.vo.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;

/**
 * 参数校验通用异常处理
 *
 * @author tangsm
 */
@Order(80)
@RestControllerAdvice
public class ValidationExceptionHandle {
    private static final Logger log = LoggerFactory.getLogger(ValidationExceptionHandle.class);

    /**
     * BindException异常处理
     * <p>BindException: 作用于@Validated @Valid 注解,仅对于表单提交有效,对于以json格式提交将会失效</p>
     *
     * @param e BindException异常信息
     * @return 响应数据
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(BindException.class)
    public Result<?> bindExceptionHandler(BindException e) {
        String msg = e.getBindingResult().getFieldErrors()
                .stream()
                .map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage()))
                .reduce((x, y) -> String.format("%s; %s", x, y))
                .orElse("参数输入有误");

        log.error("BindException异常,参数校验异常:{}", msg);
        return Result.verifyError(msg);
    }

    /**
     * MethodArgumentNotValidException-Spring封装的参数验证异常处理
     * <p>MethodArgumentNotValidException:作用于 @Validated @Valid 注解,接收参数加上@RequestBody注解(json格式)才会有这种异常。</p>
     *
     * @param e MethodArgumentNotValidException异常信息
     * @return 响应数据
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result<?> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors()
                .stream()
                .map(n -> String.format("%s: %s", n.getField(), n.getDefaultMessage()))
                .reduce((x, y) -> String.format("%s; %s", x, y))
                .orElse("参数输入有误");
        log.error("MethodArgumentNotValidException异常,参数校验异常:{}", msg);
        return Result.verifyError(msg);
    }

    /**
     * ConstraintViolationException-jsr规范中的验证异常,嵌套检验问题
     * <p>ConstraintViolationException:作用于 @NotBlank @NotNull @NotEmpty 注解,校验单个String、Integer、Collection等参数异常处理。</p>
     * <p>注:Controller类上必须添加@Validated注解,否则接口单个参数校验无效</p>
     *
     * @param e ConstraintViolationException异常信息
     * @return 响应数据
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = ConstraintViolationException.class)
    public Result<?> constraintViolationExceptionHandler(ConstraintViolationException e) {
        String msg = e.getConstraintViolations()
                .stream()
                .map(ConstraintViolation::getMessage)
                .collect(Collectors.joining("; "));

        log.error("ConstraintViolationException,参数校验异常:{}", msg);
        return Result.verifyError(msg);
    }
}

参考:

更多推荐