SpringBoot 使用 Validation API 和 全局异常 优雅的校验方法参数

程序员考拉

共 24491字,需浏览 49分钟

 ·

2021-07-09 14:55

公众号关注 “GitHub今日热榜
设为 “星标”,带你挖掘更多开发神器!







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


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


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

  

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


三、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注解即可级联验证


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


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


整个过程如下图所示,用户访问接口,然后进行参数效验。对于GET请求的参数可以使用@validated注解配合上面相应的注解进行校验或者按照原先if-else方式进行效验。而对于POST请求,大部分是以表单数据即以实体对象为参数,可以使用@Valid注解方式进行效验。如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理。



五、 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 "操作成功!";
 
 }


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



第三种:用户手动调用对应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 user) {
 
        Set<ConstraintViolation<@Valid Object>> validateSet = Validation.buildDefaultValidatorFactory()
                .getValidator()
                .validate(user, new Class[0]);
        if (!CollectionUtils.isEmpty(validateSet)) {
            String messages = validateSet.stream()
                    .map(ConstraintViolation::getMessage)
                    .reduce((m1, m2) -> m1 + ";" + m2)
                    .orElse("参数输入有误!");
            throw new IllegalArgumentException(messages);
        }
    }
}


六、springboot项目中实战演练


spring-boot-starter-web依赖已经集成相关jar,无需额外引入。


1.对实体类的变量进行注解标注


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


User.class


import org.hibernate.validator.constraints.Length;
 
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 注解。


UserInfo.class


import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
 
@Data
 
public class UserInfo {
 
    @NotBlank(message = "年龄不为空")
    @Max(value = 18, message = "不能超过18岁")
    private String age;
 
    @NotBlank(message = "性别不能为空")
    private String gender;
 
}


2.创建自定义异常


自定义异常类,方便我们处理手动抛出的异常。


public class ParamaErrorException extends RuntimeException {
    public ParamaErrorException() {
    }
    public ParamaErrorException(String message) {
        super(message);
    }
}


3.自定义响应枚举类


定义一个返回信息的枚举类,方便我们快速响应信息,不必每次都写返回消息和响应码。


public enum ResultEnum {
 
    SUCCESS(1000, "请求成功"),
    PARAMETER_ERROR(1001, "请求参数有误!"),
    UNKNOWN_ERROR(9999, "未知的错误!");
 
    private Integer code;
    private String message;
    ResultEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
    public Integer getCode() {
        return code;
    }
    public String getMessage() {
        return message;
    }
}


4.自定义响应对象类


创建用于返回调用方的响应信息的实体类。


import com.sue.demo.enums.ResultEnum;
import lombok.Data;
 
@Data
 
public class ResponseResult {
    private Integer code;
    private String msg;
    public ResponseResult() {
    }
    public ResponseResult(ResultEnum resultEnum) {
        this.code = resultEnum.getCode();
        this.msg = resultEnum.getMessage();
    }
 
    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
 
}


5.添加全局异常处理


全局异常用于处理校验不通过时抛出的异常,并通过接口返回,同时对其他未知异常进行处理。


import com.sue.demo.controller.ResponseResult;
import com.sue.demo.enums.ResultEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
 
import java.util.List;
 
@RestControllerAdvice("com.sue.demo.controller")
public class GlobalExceptionHandler {
    private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    /**
     * 忽略参数异常处理器
     * @param e 忽略参数异常
     * @return ResponseResult
 
     */

 
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ResponseResult parameterMissingExceptionHandler(MissingServletRequestParameterException e) {
        logger.error("", e);
        return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), "请求参数 " + e.getParameterName() + " 不能为空");
    }
 
    /**
     * 缺少请求体异常处理器
     * @param e 缺少请求体异常
     * @return ResponseResult
     */

 
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseResult parameterBodyMissingExceptionHandler(HttpMessageNotReadableException e) {
        logger.error("", e);
        return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), "参数体不能为空");
    }
 
    /**
     * 参数效验异常处理器
     * @param e 参数验证异常
     * @return ResponseInfo
     */

 
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseResult parameterExceptionHandler(MethodArgumentNotValidException e) {
        logger.error("", e);
        // 获取异常信息
        BindingResult exceptions = e.getBindingResult();
        // 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
        if (exceptions.hasErrors()) {
            List<ObjectError> errors = exceptions.getAllErrors();
            if (!errors.isEmpty()) {
                // 这里列出了全部错误参数,按正常逻辑,只需要第一条错误即可
                FieldError fieldError = (FieldError) errors.get(0);
                return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), fieldError.getDefaultMessage());
            }
        }
        return new ResponseResult(ResultEnum.PARAMETER_ERROR);
    }
 
    /**
     * 自定义参数错误异常处理器
     * @param e 自定义参数
     * @return ResponseInfo
     */

 
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler({ParamaErrorException.class})
    public ResponseResult paramExceptionHandler(ParamaErrorException e) {
        logger.error("", e);
        // 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
        if (!StringUtils.isEmpty(e.getMessage())) {
            return new ResponseResult(ResultEnum.PARAMETER_ERROR.getCode(), e.getMessage());
        }
        return new ResponseResult(ResultEnum.PARAMETER_ERROR);
    }
 
 
    /**
     * 其他异常
     * @param e
     * @return
     */

 
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler({Exception.class})
    public ResponseResult otherExceptionHandler(Exception e) {
        logger.error("其他异常", e);
        // 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
        if (!StringUtils.isEmpty(e.getMessage())) {
            return new ResponseResult(ResultEnum.UNKNOWN_ERROR.getCode(), e.getMessage());
        }
        return new ResponseResult(ResultEnum.UNKNOWN_ERROR);
    }
}


6.接口类中添加相关注解


处理get请求直接在参数前添加验证注解,处理post请求时在对象前添加@Valid注解


TestController.class


import com.sue.demo.entity.User;
import com.sue.demo.entity.UserInfo;
import com.sue.demo.enums.ResultEnum;
import com.sue.demo.exception.ParamaErrorException;
import com.sue.demo.util.MyValidationUtils;
import com.sue.demo.util.ResponseResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.hibernate.validator.constraints.Length;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
 
 
import javax.validation.Valid;
import java.util.List;
 
 
@Validated
@RestController
@Api(value = "测试使用validation验证参数")
public class TestController {
 
    /**
     * 测试get方法,手动if进行判空,校验失败时手动抛出自定义异常
     * @param username 姓名
     * @return ResponseResult
     */

 
    @ApiOperation(value = "测试get方法", notes = "输入用户名")
    @GetMapping("/testGet")
    public ResponseResult testGet(String username) {
        if (username == null || "".equals(username)) {
            throw new ParamaErrorException("username 不能为空");
        }
        return new ResponseResult(ResultEnum.SUCCESS);
    }
 
    /**
     * 使用注解校验get请求平面参数,需要在Controller类头部添加@Validated注解,否则不能成功校验,这种方法不用手动抛出异常
     * @param username
     * @return
     */

 
    @ApiOperation(value = "测试get方法", notes = "输入用户名")
    @GetMapping("/testGetByValidated")
    public ResponseResult testGetByValidated(@Length(max = 4) @RequestParam("username") String username) {
        return new ResponseResult(ResultEnum.SUCCESS);
    }
 
    /**
     * post方法传入单个对象进行校验,在参数前添加@Valid注解,校验失败时会抛出异常并使用全局异常进行处理
     * @param userInfo 用户信息
     * @return ResponseResult
     */

 
    @ApiOperation(value = "post方法传入单个对象", notes = "传入json对象")
    @PostMapping("/testUserInfo")
    public ResponseResult testUserInfo(@Valid @RequestBody UserInfo userInfo) {
        return new ResponseResult(ResultEnum.SUCCESS);
    }
 
 
 
    /**
     * post方法传入对象,手动校验,此时参数前没有添加@Valid注解,所以不会自动进行校验,手动调用validate方法进行校验,失败时会抛出异常
     * @param userInfo
     * @return ResponseResult
     */

 
    @ApiOperation(value = "post方法传入对象,手动测试", notes = "单个对象")
    @PostMapping("/checkByMethod")
    public ResponseResult checkByMethod(@RequestBody UserInfo userInfo) {
        //调用api校验
        MyValidationUtils.validate(userInfo);
        return new ResponseResult(ResultEnum.SUCCESS);
    }
 
 
    /**
     * post方法传入多个对象,当使用@Valid校验对象集合时,要在控制层添加@Validated注解,否则不会对集合中的每个对象进行校验
     * @param userInfo
     * @return ResponseResult
     */

 
    @ApiOperation(value = "post方法传入多个对象", notes = "多个对象")
    @PostMapping("/testUserList")
    public ResponseResult testUserList(@Valid @RequestBody List<UserInfo> userInfo) {
        return new ResponseResult(ResultEnum.SUCCESS);
    }
 
 
 
    /**
     * 测试对象中嵌套对象的情况,此时也要在对象属性上添加@Valid注解
     * @param user
     * @return
     */

 
    @ApiOperation(value = "测试对象中嵌套对象的情况")
    @PostMapping("/checkUser")
    public ResponseResult checkUser(@Valid @RequestBody User user) {
        return new ResponseResult(ResultEnum.SUCCESS);
    }
 
 
 
    /**
     * 将校验结果放进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 "操作成功!";
    }
}


7.进行测试


补充:使用自定义参数注解


1.我们这里创建一个身份证校验注解


@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdentityCardNumberValidator.class)
public @interface IdentityCardNumber {
    String message() default "身份证号码不合法";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
 
}


这个注解是作用在Field字段上,运行时生效,触发的是IdentityCardNumber这个验证类。


  • message 定制化的提示信息,主要是从ValidationMessages.properties里提取,也可以依据实际情况进行定制
  • groups 这里主要进行将validator进行分类,不同的类group中会执行不同的validator操作
  • payload 主要是针对bean的,使用不多。


2.自定义Validator


import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
 
public class IdentityCardNumberValidator implements ConstraintValidator<IdentityCardNumber, Object> {
    @Override
    public void initialize(IdentityCardNumber identityCardNumber) {
    }
    @Override
    public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
        return IdCardValidatorUtils.isValidate18Idcard(o.toString());
    }
}


校验工具类IdCardValidatorUtils.class


3. 使用自定义的注解


@NotBlank(message = "身份证号不能为空")
@IdentityCardNumber(message = "身份证信息有误,请核对后提交")
private String clientCardNo;




出处:csdn.net/chenyao1994/article/details/107858409










关注GitHub今日热榜,专注挖掘好用的开发工具,致力于分享优质高效的工具、资源、插件等,助力开发者成长!








点个在看,你最好看


浏览 47
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报