SpringBoot 使用Spring Validation实现接口参数校验-Spring专区论坛-技术-SpringForAll社区

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

前言

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

SpringBoot 实现参数校验

1、依赖pom

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

<!--校验组件-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.2.8.RELEASE</version>
</dependency>
<!--web组件-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.20.Final</version>
</dependency>

2、单个类参数校验

定义要校验参数的实体类

@Data
public class UserInfo {
    @ApiModelProperty(value = "id")
    private Long id;
    
    @NotBlank(message = "用户名不能为空")
    @Size(max = 3,message = "用户名不能超过3")
    @ApiModelProperty(value = "用户名")
    private String userName;
    
    @NotBlank(message = "昵称不能为空")
    @ApiModelProperty(value = "昵称")
    private String nickName;//
​
    @Email(message = "邮箱格式不正确")
    @ApiModelProperty(value = "邮箱")
    private String email;
​
}

内置校验注解:

注解 校验功能
@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进行测试

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

图片[1]-SpringBoot 使用Spring Validation实现接口参数校验-Spring专区论坛-技术-SpringForAll社区

测试效果如下

image

 

3、全局异常处理

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

全局异常处理类 GlobalExceptionHandler

​
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    private static final String BAD_REQUEST_MSG = "参数检验不通过";
​
    //处理 form data方式调用接口校验失败抛出的异常
    @ExceptionHandler(BindException.class)
    public ResultInfo bindExceptionHandler(BindException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
    }
​
    //  处理 json 请求体调用接口校验失败抛出的异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultInfo methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<String> collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
    }
​
    //  处理单个参数校验失败抛出的异常
    @ExceptionHandler(ConstraintViolationException.class)
    public ResultInfo constraintViolationExceptionHandler(ConstraintViolationException e) {
        Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
        List<String> collect = constraintViolations.stream().map(o -> o.getMessage()).collect(Collectors.toList());
        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
    }
​
    // 处理以上处理不了的其他异常
    @ExceptionHandler(Exception.class)
    public ResultInfo exceptionHandler(Exception e) {
​
        return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, e.getMessage());
    }
​
}

图片[1]-SpringBoot 使用Spring Validation实现接口参数校验-Spring专区论坛-技术-SpringForAll社区

4、测试

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

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

image

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

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

image

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

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

image

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

image

5、自定义注解

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

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

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {Phone11Validator.class})// 标明由哪个类执行校验逻辑
@NotBlank(message = "电话不能为空")
public @interface Phone11 {
    boolean required() default true;
​
    String message() default "11位手机格式不正确";
​
    Class<?>[] groups() default {};
​
    Class<? extends Payload>[] payload() default {};
}

逻辑校验类:Phone11Validator

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

接着再实体里添加字段phone

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

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

image

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

6、递归校验

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

@Data
public class UserInfo {
    @ApiModelProperty(value = "id")
    private Long id;
    
    ...省略其他代码...
        
    @Valid
    private Depatement depatement;
}

department 如下

@Data
@ApiModel(value = "department",description = "用户部门表")
public class Department {
    @ApiModelProperty(value = "id")
    private Long id;
    
    @NotBlank(message = "部门名不能为空")
    @ApiModelProperty(value = "部门名")
    private String deptName;
​
}

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

image

7、快速失败返回

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

image

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

@Configuration
public class ValidatorConfiguration {
    /**
     * JSR和Hibernate validator的校验只能对Object的属性进行校验
     * 不能对单个的参数进行校验
     * spring 在此基础上进行了扩展
     * 添加了MethodValidationPostProcessor拦截器
     * 可以实现对方法参数的校验
     *
     * @return
     */
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
        processor.setValidator(validator());
        return processor;
    }
​
    @Bean
    public static Validator validator() {
        return Validation
                .byProvider(HibernateValidator.class)
                .configure()
                //快速返回模式,有一个验证失败立即返回错误信息
                .failFast(true)
                .buildValidatorFactory()
                .getValidator();
    }
}

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

image

demo地址

请登录后发表评论

    没有回复内容