小言_互联网的博客

SpringBoot 使用Spring Validation实现接口参数校验

498人阅读  评论(0)

 

前言

实际开发中,参数校验必不可少,因为用户的心思你永远无法洞察,他们会提交你根本无法想象的内容或者格式,如果前端后端都没做数据校验,那么恭喜你,你应该会收到很多垃圾数据,有些人甚至会提交一些恶意脚本,这样的话服务器就存在被攻击的风险。最好的方法就是把这些坏心思扼杀在萌芽之中,除了前端校验,后端校验也是重中之重。因为前端还是有风险的,比如浏览器端的js校验,我们就可以通过设置跳过这些js校验,相当于前端校验作废了,如果你服务器端没加校验的话,脏数据、垃圾数据还是会进来,所以,虽有前端校验还是不行,后端校验必须有。

SpringBoot 实现参数校验

1、依赖pom

springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入validation和web,而springboot-2.3之前的版本只需要引入 web 依赖就可以了。


  
  1. <!--校验组件-->
  2. <dependency>
  3.     <groupId>org.springframework.boot </groupId>
  4.     <artifactId>spring-boot-starter-validation </artifactId>
  5.     <version>2.2.8.RELEASE </version>
  6. </dependency>
  7. <!--web组件-->
  8. <dependency>
  9.     <groupId>org.springframework.boot </groupId>
  10.     <artifactId>spring-boot-starter-web </artifactId>
  11. </dependency>

或者把spring-boot-starter-validation 替换为hibernate-validator


  
  1. <dependency>
  2.   <groupId>org.hibernate.validator </groupId>
  3.   <artifactId>hibernate-validator </artifactId>
  4.   <version>6.0.20.Final </version>
  5. </dependency>

2、单个类参数校验

定义要校验参数的实体类


  
  1. @Data
  2. public class UserInfo {
  3.    @ApiModelProperty(value = "id")
  4.    private Long id;
  5.    @NotBlank(message = "用户名不能为空")
  6.    @Size(max = 3,message = "用户名不能超过3")
  7.    @ApiModelProperty(value = "用户名")
  8.    private String userName;
  9.    @NotBlank(message = "昵称不能为空")
  10.    @ApiModelProperty(value = "昵称")
  11.    private String nickName;//
  12.    @Email(message = "邮箱格式不正确")
  13.    @ApiModelProperty(value = "邮箱")
  14.    private String email;
  15. }

内置校验注解:

注解 校验功能
@AssertFalse 必须是false
@AssertTrue 必须是true
@DecimalMax 小于等于给定的值
@DecimalMin 大于等于给定的值
@Digits 可设定最大整数位数和最大小数位数
@Email 校验是否符合Email格式
@Future 必须是将来的时间
@FutureOrPresent 当前或将来时间
@Max 最大值
@Min 最小值
@Negative 负数(不包括0)
@NegativeOrZero 负数或0
@NotBlank 不为null并且包含至少一个非空白字符
@NotEmpty 不为null并且不为空
@NotNull 不为null
@Null 为null
@Past 必须是过去的时间
@PastOrPresent 必须是过去的时间,包含现在
@PositiveOrZero 正数或0
@Size 校验容器的元素个数

定义UserInfoController进行测试


  
  1. @ApiOperation(value = "添加用户")
  2. @PostMapping("/addUserInfo")
  3.    public ResultInfo addUserInfo(@Validated UserInfo userInfo, BindingResult result) {
  4.        List <FieldError> fieldErrors = result.getFieldErrors();
  5.        if(!fieldErrors.isEmpty()){
  6.            //取出所有校验不通过的信息
  7.            List <String> collect = fieldErrors.stream().map(s->s.getDefaultMessage()).collect(Collectors.toList());
  8.            return ResultInfo.success(HttpStatus.BAD_REQUEST.value(),"字段校验不通过",collect);
  9.       }
  10.        return ResultInfo.success(200,"成功");
  11.   }

测试效果如下

3、全局异常处理

每个Controller方法中如果都写一遍BindingResult信息的处理还是很繁的。当我们写了@validated注解,不写BindingResult的时候,Spring 就会抛出异常。因此,我们可以通过全局异常处理的方式统一处理校验异常,从而免去重复编写异常信息的代码。全局异常处理类只需要在类上标注@RestControllerAdvice,并在处理相应异常的方法上使用@ExceptionHandler注解,写明处理哪个异常即可。

全局异常处理类 GlobalExceptionHandler


  
  1. @Slf4j
  2. @RestControllerAdvice
  3. public class GlobalExceptionHandler {
  4.    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
  5.    private static final String BAD_REQUEST_MSG = "参数检验不通过";
  6.    //处理 form data方式调用接口校验失败抛出的异常
  7.    @ExceptionHandler(BindException.class)
  8.    public ResultInfo bindExceptionHandler(BindException e) {
  9.        List <FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
  10.        List <String> collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
  11.        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
  12.   }
  13.    // 处理 json 请求体调用接口校验失败抛出的异常
  14.    @ExceptionHandler(MethodArgumentNotValidException.class)
  15.    public ResultInfo methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
  16.        List <FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
  17.        List <String> collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
  18.        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
  19.   }
  20.    // 处理单个参数校验失败抛出的异常
  21.    @ExceptionHandler(ConstraintViolationException.class)
  22.    public ResultInfo constraintViolationExceptionHandler(ConstraintViolationException e) {
  23.        Set <ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
  24.        List <String> collect = constraintViolations.stream().map(o -> o.getMessage()).collect(Collectors.toList());
  25.        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
  26.   }
  27.    // 处理以上处理不了的其他异常
  28.    @ExceptionHandler(Exception.class)
  29.    public ResultInfo exceptionHandler(Exception e) {
  30.        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, e.getMessage());
  31.   }
  32. }

4、测试

测试一:使用form data方式调用接口,校验异常抛出 BindException


  
  1. @ApiOperation(value = "添加用户2")
  2. @PostMapping("/addUserInfo2")
  3. public ResultInfo addUserInfo2(@Validated UserInfo userInfo) {
  4.    return ResultInfo.success(HttpStatus.OK.value(),"成功",userInfo);
  5. }

测试二:使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException


  
  1. @ApiOperation(value = "添加用户3")
  2. @PostMapping("/addUserInfo3")
  3. public ResultInfo addUserInfo3(@RequestBody @Validated UserInfo userInfo) {
  4.    return ResultInfo.success(HttpStatus.OK.value(),"成功",userInfo);
  5. }

测试三:单个参数校验异常抛出ConstraintViolationException

注意:单个参数校验需要在当前所在类的类名上加注解:@Validated


  
  1. @ApiOperation(value = "打招呼-Hello")
  2. @GetMapping("/hello")
  3. public ResponseEntity <String> hello(@RequestParam(value = "name",required = false) @NotBlank(message = "name不能为空") String name){
  4.    return ResponseEntity.ok("Hello:"+name);
  5. }

5、自定义注解

虽然Spring Validation 提供的注解基本上够用,但是面对复杂的定义,我们还是需要自己定义相关注解来实现自动校验。正好,Spring 这个万能的框架就提供了这种扩展。

自定义注解类 Phone11 校验11位手机号是否正确


  
  1. @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. @Constraint(validatedBy = {Phone11Validator.class})// 标明由哪个类执行校验逻辑
  5. @NotBlank(message = "电话不能为空")//有现成的轮子拿过来用啊
  6. public @interface Phone11 {
  7.    boolean required() default true;
  8.    String message() default "11位手机格式不正确";
  9.    Class <?>[] groups() default {};
  10.    Class <? extends Payload>[] payload() default {};
  11. }

逻辑校验类:Phone11Validator


  
  1. public class Phone11Validator implements ConstraintValidator <Phone11, String> {
  2.    //校验手机号正则
  3.    public static final String REGEX_MOBILE = "^((13[0-9])|(15[^4,\\D])|(17[0-9])|(18[0-9]))\\d{8}$";
  4.    
  5.    @Override
  6.    public boolean isValid(String mobile, ConstraintValidatorContext constraintValidatorContext) {
  7.        if (isMobile(mobile)){
  8.            return true;//校验通过
  9.       }else {
  10.            return false;//校验未通过
  11.       }
  12.   }
  13.    
  14.    /**
  15.     * 校验手机号
  16.     * @param mobile
  17.     * @return 校验通过返回true,否则返回false
  18.     */
  19.    public static boolean isMobile(String mobile) {
  20.        return Pattern.matches(REGEX_MOBILE, mobile);
  21.   }
  22. }

接着再实体里添加字段phone


  
  1.    //校验11位手机号格式是否正确
  2.    @Phone11
  3.    private String phone;

继续调用addUserInfo3进行测试,测试结果如下:

 

测试结果中发现电话有两个提示,一个不能为空,一个格式不对,不能为空那个就是因为在自定义注解的时候加了@NotNull注解,而另一个就是我们自己定义的提示信息。

6、递归校验

有时候我们的实体不是单纯的自己一个,而是TA里边有可能包含了另一个实体类,比如常见的一对一或者一对多关系,遇到这种情况,我们不但要校验本类自己的属性,而且包含的另一个实体类也需要校验,就会用到递归校验。很简单,我们只需要再包含的另一个类的上边加上注解 @Valid 即可实现,假设我们还有个部门实体Department,用户实体UserInfo包含部门实体,如下:


  
  1. @Data
  2. public class UserInfo {
  3.    @ApiModelProperty(value = "id")
  4.    private Long id;
  5.    
  6. ...省略其他代码...
  7.        
  8.    @Valid
  9.    private Depatement depatement;
  10. }

department 如下


  
  1. @Data
  2. @ApiModel(value = "department",description = "用户部门表")
  3. public class Department {
  4.    @ApiModelProperty(value = "id")
  5.    private Long id;
  6.    
  7.    @NotBlank(message = "部门名不能为空")
  8.    @ApiModelProperty(value = "部门名")
  9.    private String deptName;
  10. }

调用addUserInfo3进行测试,测试结果如下

7、快速失败返回

现在有个问题:就是有很多个字段需要校验,目前的情况是所有没通过校验的都会提示出来,对我们来说,只要有一个校验不通过,那么这次请求就是失败的,为啥还要花那时间全部检测出来呢。因此我们可以改善一下,快速失败,只要有一个字段不符合,就返回给用户提示,其他的也就不用再花时间去校验了。

新建配置类:ValidatorConfiguration,别忘了加注解@Configuration


  
  1. @Configuration
  2. public class ValidatorConfiguration {
  3.    /**
  4.     * JSR和Hibernate validator的校验只能对Object的属性进行校验
  5.     * 不能对单个的参数进行校验
  6.     * spring 在此基础上进行了扩展
  7.     * 添加了MethodValidationPostProcessor拦截器
  8.     * 可以实现对方法参数的校验
  9.     *
  10.     * @return
  11.     */
  12.    @Bean
  13.    public MethodValidationPostProcessor methodValidationPostProcessor() {
  14.        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
  15.        processor.setValidator(validator());
  16.        return processor;
  17.   }
  18.    @Bean
  19.    public static Validator validator() {
  20.        return Validation
  21.               .byProvider(HibernateValidator.class)
  22.               .configure()
  23.                //快速返回模式,有一个验证失败立即返回错误信息
  24.               .failFast(true)
  25.               .buildValidatorFactory()
  26.               .getValidator();
  27.   }
  28. }

然后我们再次测试,继续调用addUserInfo3,会发现每次都只返回一个错误信息。

 

代码地址:https://github.com/ComeFromChina/SpringBootDemo/tree/master/springboot-swagger-knife4j

有问题欢迎加群探讨:700637673


转载:https://blog.csdn.net/li_wen_jin/article/details/115633261
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场