SpringBoot中BeanValidation数据校验与优雅处理详解

本篇要点

JDK1.8、SpringBoot2.3.4release

后端参数校验的必要性

在开发中,从表现层到持久化层,数据校验都是一项逻辑差不多,但容易出错的任务,

前端框架往往会采取一些检查参数的手段,比如校验并提示信息,那么,既然前端已经存在校验手段,后端的校验是否还有必要,是否多余了呢?

并不是,正常情况下,参数确实会经过前端校验传向后端,但如果后端不做校验,一旦通过特殊手段越过前端的检测,系统就会出现安全漏洞。

不使用Validator的参数处理逻辑

既然是参数校验,很简单呀,用几个if/else直接搞定:

    @PostMapping("/form")
    public String form(@RequestBody Person person) {
        if (person.getName() == null) {
            return "姓名不能为null";
        }
        if (person.getName().length() < 6 || person.getName().length() > 12) {
            return "姓名长度必须在6 - 12之间";
        }
        if (person.getAge() == null) {
            return "年龄不能为null";
        }
        if (person.getAge() < 20) {
            return "年龄最小需要20";
        }
        // service ..
        return "注册成功!";
    }

写法干脆,但if/else太多,过于臃肿,更何况这只是区区一个接口的两个参数而已,要是需要更多参数校验,甚至更多方法都需要这要的校验,这代码量可想而知。于是,这种做法显然是不可取的,我们可以利用下面这种更加优雅的参数处理方式。

Validator框架提供的便利

Validating data is a common task that occurs throughout all application layers,from the presentation to the persistence layer. Often the same validation logic is implemented in each layer which is time consuming and error-prone.

如果依照下图的架构,对每个层级都进行类似的校验,未免过于冗杂。

Jakarta Bean Validation 2.0 - defines a metadata model and API for entity and method validation. The default metadata source are annotations,with the ability to override and extend the meta-data through the use of XML.

The API is not tied to a specific application tier nor programming model. It is specifically not tied to either web or persistence tier,and is available for both server-side application programming,as well as rich client Swing application developers.

Jakarta Bean Validation2.0定义了一个元数据模型,为实体和方法提供了数据验证的API,默认将注解作为源,可以通过XML扩展源。

SpringBoot自动配置ValidationAutoConfiguration

Hibernate Validator Jakarta Bean Validation的参考实现。

在SpringBoot中,只要类路径上存在JSR-303的实现,如Hibernate Validator,就会自动开启Bean Validation验证功能,这里我们只要引入spring-boot-starter-validation的依赖,就能完成所需。

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

目的其实是为了引入如下依赖:

    <!-- Unified EL 获取动态表达式-->
	<dependency>
      <groupId>org.glassfish</groupId>
      <artifactId>jakarta.el</artifactId>
      <version>3.0.3</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.hibernate.validator</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>6.1.5.Final</version>
      <scope>compile</scope>
    </dependency>

SpringBoot对BeanValidation的支持的自动装配定义在org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration类中,提供了默认的LocalValidatorFactoryBean和支持方法级别的拦截器MethodValidationPostProcessor

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ExecutableValidator.class)
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {

	@Bean
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	@ConditionalOnMissingBean(Validator.class)
	public static LocalValidatorFactoryBean defaultValidator() {
        //ValidatorFactory
		LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
		MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
		factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
		return factoryBean;
	}

    // 支持Aop,MethodValidationInterceptor方法级别的拦截器
	@Bean
	@ConditionalOnMissingBean
	public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,@Lazy Validator validator) {
		MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
		boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class",Boolean.class,true);
		processor.setProxyTargetClass(proxyTargetClass);
        // factory.getValidator(); 通过factoryBean获取了Validator实例,并设置
		processor.setValidator(validator);
		return processor;
	}

}

Validator+BindingResult优雅处理

默认已经引入相关依赖。

为实体类定义约束注解

/**
 * 实体类字段加上javax.validation.constraints定义的注解
 * @author Summerday
 */

@Data
@ToString
public class Person {
    private Integer id;
    
    @NotNull
    @Size(min = 6,max = 12)
    private String name;

    @NotNull
    @Min(20)
    private Integer age;
}

使用@Valid或@Validated注解

@Valid和@Validated在Controller层做方法参数校验时功能相近,具体区别可以往后面看。

@RestController
public class ValidateController {

    @PostMapping("/person")
    public Map<String,Object> validatePerson(@Validated @RequestBody Person person,BindingResult result) {
        Map<String,Object> map = new HashMap<>();
        // 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里
        if (result.hasErrors()) {
            List<String> res = new ArrayList<>();
            result.getFieldErrors().forEach(error -> {
                String field = error.getField();
                Object value = error.getRejectedValue();
                String msg = error.getDefaultMessage();
                res.add(String.format("错误字段 -> %s 错误值 -> %s 原因 -> %s",field,value,msg));
            });
            map.put("msg",res);
            return map;
        }
        map.put("msg","success");
        System.out.println(person);
        return map;
    }
}

发送Post请求,伪造不合法数据

这里使用IDEA提供的HTTP Client工具发送请求。

POST http://localhost:8081/person
Content-Type: application/json

{
  "name": "天乔巴夏","age": 10
}

响应信息如下:

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat,14 Nov 2020 15:58:17 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "msg": [
    "错误字段 -> name 错误值 -> 天乔巴夏 原因 -> 个数必须在6和12之间","错误字段 -> age 错误值 -> 10 原因 -> 最小不能小于20"
  ]
}

Response code: 200; Time: 393ms; Content length: 92 bytes

Validator + 全局异常处理

在接口方法中利用BindingResult处理校验数据过程中的信息是一个可行方案,但在接口众多的情况下,就显得有些冗余,我们可以利用全局异常处理,捕捉抛出的MethodArgumentNotValidException异常,并进行相应的处理。

定义全局异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * If the bean validation is failed,it will trigger a MethodArgumentNotValidException.
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Object> handleMethodArgumentNotValid(
        MethodArgumentNotValidException ex,HttpStatus status) {
        BindingResult result = ex.getBindingResult();
        Map<String,Object> map = new HashMap<>();
        List<String> list = new LinkedList<>();
        result.getFieldErrors().forEach(error -> {
            String field = error.getField();
            Object value = error.getRejectedValue();
            String msg = error.getDefaultMessage();
            list.add(String.format("错误字段 -> %s 错误值 -> %s 原因 -> %s",msg));
        });
        map.put("msg",list);
        return new ResponseEntity<>(map,status);
    }
}

定义接口

@RestController
public class ValidateController {

    @PostMapping("/person")
    public Map<String,Object> validatePerson(@Valid @RequestBody Person person) {
        Map<String,Object> map = new HashMap<>();
        map.put("msg","success");
        System.out.println(person);
        return map;
    }
}

@Validated精确校验到参数字段

有时候,我们只想校验某个参数字段,并不想校验整个pojo对象,我们可以利用@Validated精确校验到某个字段。

定义接口

@RestController
@Validated
public class OnlyParamsController {

    @GetMapping("/{id}/{name}")
    public String test(@PathVariable("id") @Min(1) Long id,@PathVariable("name") @Size(min = 5,max = 10) String name) {
        return "success";
    }
}

发送GET请求,伪造不合法信息

GET http://localhost:8081/0/hyh
Content-Type: application/json

未作任何处理,响应结果如下:

{
  "timestamp": "2020-11-15T15:23:29.734+00:00","status": 500,"error": "Internal Server Error","trace": "javax.validation.ConstraintViolationException: test.id: 最小不能小于1,test.name: 个数必须在5和10之间...省略","message": "test.id: 最小不能小于1,test.name: 个数必须在5和10之间","path": "/0/hyh"
}

可以看到,校验已经生效,但状态和响应错误信息不太正确,我们可以通过捕获ConstraintViolationException修改状态。

捕获异常,处理结果

@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(CustomGlobalExceptionHandler.class);


    /**
     * If the @Validated is failed,it will trigger a ConstraintViolationException
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public void constraintViolationException(ConstraintViolationException ex,HttpServletResponse response) throws IOException {
        ex.getConstraintViolations().forEach(x -> {
            String message = x.getMessage();
            Path propertyPath = x.getPropertyPath();
            Object invalidValue = x.getInvalidValue();
            log.error("错误字段 -> {} 错误值 -> {} 原因 -> {}",propertyPath,invalidValue,message);
        });
        response.sendError(HttpStatus.BAD_REQUEST.value());
    }
}

@Validated和@Valid的不同

参考:@Validated和@Valid的区别?教你使用它完成Controller参数校验(含级联属性校验)以及原理分析【享学Spring】

  • @Valid是标准JSR-303规范的标记型注解,用来标记验证属性和方法返回值,进行级联和递归校验。
  • @Validated:是Spring提供的注解,是标准JSR-303的一个变种(补充),提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。
  • Controller中校验方法参数时,使用@Valid和@Validated并无特殊差异(若不需要分组校验的话)。
  • @Validated注解可以用于类级别,用于支持Spring进行方法级别的参数校验。@Valid可以用在属性级别约束,用来表示级联校验
  • @Validated只能用在类、方法和参数上,而@Valid可用于方法、字段、构造器和参数上。

如何自定义注解

Jakarta Bean Validation API定义了一套标准约束注解,如@NotNull,@Size等,但是这些内置的约束注解难免会不能满足我们的需求,这时我们就可以自定义注解,创建自定义注解需要三步:

  1. 创建一个constraint annotation。
  2. 实现一个validator。
  3. 定义一个default error message。

创建一个constraint annotation

/**
 * 自定义注解
 * @author Summerday
 */

@Target({FIELD,METHOD,PARAMETER,ANNOTATION_TYPE,TYPE_USE})
@Retention(RUNTIME)
@Constraint(validatedBy = CheckCaseValidator.class) //需要定义CheckCaseValidator
@Documented
@Repeatable(CheckCase.List.class)
public @interface CheckCase {
    String message() default "{CheckCase.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    CaseMode value();

    @Target({FIELD,ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        CheckCase[] value();
    }
}

实现一个validator

/**
 * 实现ConstraintValidator
 *
 * @author Summerday
 */
public class CheckCaseValidator implements ConstraintValidator<CheckCase,String> {

    private CaseMode caseMode;

    /**
     * 初始化获取注解中的值
     */
    @Override
    public void initialize(CheckCase constraintAnnotation) {
        this.caseMode = constraintAnnotation.value();
    }

    /**
     * 校验
     */
    @Override
    public boolean isValid(String object,ConstraintValidatorContext constraintContext) {
        if (object == null) {
            return true;
        }

        boolean isValid;
        if (caseMode == CaseMode.UPPER) {
            isValid = object.equals(object.toUpperCase());
        } else {
            isValid = object.equals(object.toLowerCase());
        }

        if (!isValid) {
            // 如果定义了message值,就用定义的,没有则去
            // ValidationMessages.properties中找CheckCase.message的值
            if(constraintContext.getDefaultConstraintMessageTemplate().isEmpty()){
                constraintContext.disableDefaultConstraintViolation();
                constraintContext.buildConstraintViolationWithTemplate(
                        "{CheckCase.message}"
                ).addConstraintViolation();
            }
        }
        return isValid;
    }
}

定义一个default error message

ValidationMessages.properties文件中定义:

CheckCase.message=Case mode must be {value}.

这样,自定义的注解就完成了,如果感兴趣可以自行测试一下,在某个字段上加上注解:@CheckCase(value = CaseMode.UPPER)

源码下载

本文内容均为对优秀博客及官方文档总结而得,原文地址均已在文中参考阅读处标注。最后,文中的代码样例已经全部上传至Gitee:https://gitee.com/tqbx/springboot-samples-learn,另有其他SpringBoot的整合哦。

参考阅读

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


今天小编给大家分享的是Springboot下使用Redis管道(pipeline)进行批量操作的介绍,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起...
本篇文章和大家了解一下springBoot项目常用目录有哪些。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。springBoot项目常用目录springBoot项...
本篇文章和大家了解一下Springboot自带线程池怎么实现。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。一: ThreadPoolTaskExecuto1 ThreadP...
这篇文章主要介绍了SpringBoot读取yml文件有哪几种方式,具有一定借鉴价值,需要的朋友可以参考下。下面就和我一起来看看吧。Spring Boot读取yml文件的主要方式...
今天小编给大家分享的是SpringBoot配置Controller实现Web请求处理的方法,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧...
本篇文章和大家了解一下SpringBoot实现PDF添加水印的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。简介PDF(Portable Document Form...
本篇文章和大家了解一下解决Springboot全局异常处理与AOP日志处理中@AfterThrowing失效问题的方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有...
本篇文章和大家了解一下IDEA创建SpringBoot父子Module项目的实现方法。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。目录前言1. 软硬件环...
今天小编给大家分享的是springboot获取项目目录路径的方法,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会有所收...
本篇内容主要讲解“SpringBoot+Spring Security无法实现跨域如何解决”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面...
这篇文章主要介绍“vue怎么发送请求到springboot程序”,在日常操作中,相信很多人在vue怎么发送请求到springboot程序问题上存在疑惑,小编查阅了各式资料,整理...
本篇内容主要讲解“Springboot内置的工具类CollectionUtils如何使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家...
本文小编为大家详细介绍“SpringBoot上传文件大小受限如何解决”,内容详细,步骤清晰,细节处理妥当,希望这篇“SpringBoot上传文件大小受限如何解决”文章能帮...
本文小编为大家详细介绍“springboot拦截器如何创建”,内容详细,步骤清晰,细节处理妥当,希望这篇“springboot拦截器如何创建”文章能帮助大家解决疑惑,下面...
本文小编为大家详细介绍“Hikari连接池使用SpringBoot配置JMX监控的方法是什么”,内容详细,步骤清晰,细节处理妥当,希望这篇“Hikari连接池使用SpringBoot配...
今天小编给大家分享一下SpringBoot如何使用Sa-Token实现权限认证的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大...
这篇文章主要介绍“SpringBoot如何集成SFTP客户端实现文件上传下载”,在日常操作中,相信很多人在SpringBoot如何集成SFTP客户端实现文件上传下...
本篇内容主要讲解“Springboot插件怎么开发”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Springboot插件怎
这篇文章主要介绍“Springboot怎么解决跨域请求问题”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇...
今天小编给大家分享一下如何在SpringBoot2中整合Filter的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文...