工作几年了,原来我只用了数据校验的皮毛

开发 前端
不知不觉Spring Boot专栏文章已经写到第十四章了,无论写的好与不好,作者都在尽力写的详细,写的与其它的文章不同,每一章都不是浅尝辄止。

[[433466]]

前言

不知不觉Spring Boot专栏文章已经写到第十四章了,无论写的好与不好,作者都在尽力写的详细,写的与其它的文章不同,每一章都不是浅尝辄止。如果前面的文章没有看过的朋友,点击这里前往。

今天介绍一下 Spring Boot 如何优雅的整合JSR-303进行参数校验,说到参数校验可能都用过,但是你真的会用吗?网上的教程很多,大多是简单的介绍。

什么是 JSR-303?

JSR-303 是 JAVA EE 6 中的一项子规范,叫做 Bean Validation。

Bean Validation 为 JavaBean 验证定义了相应的元数据模型和API。缺省的元数据是Java Annotations,通过使用 XML 可以对原有的元数据信息进行覆盖和扩展。在应用程序中,通过使用Bean Validation 或是你自己定义的 constraint,例如 @NotNull, @Max, @ZipCode , 就可以确保数据模型(JavaBean)的正确性。constraint 可以附加到字段,getter 方法,类或者接口上面。对于一些特定的需求,用户可以很容易的开发定制化的 constraint。Bean Validation 是一个运行时的数据验证框架,在验证之后验证的错误信息会被马上返回。

添加依赖

Spring Boot整合JSR-303只需要添加一个starter即可,如下:

  1. <dependency> 
  2.     <groupId>org.springframework.boot</groupId> 
  3.    <artifactId>spring-boot-starter-validation</artifactId> 
  4. </dependency> 

内嵌的注解有哪些?

Bean Validation 内嵌的注解很多,基本实际开发中已经够用了,注解如下:

注解 详细信息
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式

以上是Bean Validation的内嵌的注解,但是Hibernate Validator在原有的基础上也内嵌了几个注解,如下。

注解 详细信息
@Email 被注释的元素必须是电子邮箱地址
@Length 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range 被注释的元素必须在合适的范围内

如何使用?

参数校验分为简单校验、嵌套校验、分组校验。

简单校验

简单的校验即是没有嵌套属性,直接在需要的元素上标注约束注解即可。如下:

  1. @Data 
  2. public class ArticleDTO { 
  3.  
  4.     @NotNull(message = "文章id不能为空"
  5.     @Min(value = 1,message = "文章ID不能为负数"
  6.     private Integer id; 
  7.  
  8.     @NotBlank(message = "文章内容不能为空"
  9.     private String content; 
  10.  
  11.     @NotBlank(message = "作者Id不能为空"
  12.     private String authorId; 
  13.  
  14.     @Future(message = "提交时间不能为过去时间"
  15.     private Date submitTime; 

同一个属性可以指定多个约束,比如@NotNull和@MAX,其中的message属性指定了约束条件不满足时的提示信息。

以上约束标记完成之后,要想完成校验,需要在controller层的接口标注@Valid注解以及声明一个BindingResult类型的参数来接收校验的结果。

下面简单的演示下添加文章的接口,如下:

  1. /** 
  2.      * 添加文章 
  3.      */ 
  4.     @PostMapping("/add"
  5.     public String add(@Valid @RequestBody ArticleDTO articleDTO, BindingResult bindingResult) throws JsonProcessingException { 
  6.         //如果有错误提示信息 
  7.         if (bindingResult.hasErrors()) { 
  8.             Map<String , String> map = new HashMap<>(); 
  9.             bindingResult.getFieldErrors().forEach( (item) -> { 
  10.                 String message = item.getDefaultMessage(); 
  11.                 String field = item.getField(); 
  12.                 map.put( field , message ); 
  13.             } ); 
  14.             //返回提示信息 
  15.             return objectMapper.writeValueAsString(map); 
  16.         } 
  17.         return "success"
  18.     } 

仅仅在属性上添加了约束注解还不行,还需在接口参数上标注@Valid注解并且声明一个BindingResult类型的参数来接收校验结果。

分组校验

举个栗子:上传文章不需要传文章ID,但是修改文章需要上传文章ID,并且用的都是同一个DTO接收参数,此时的约束条件该如何写呢?

此时就需要对这个文章ID进行分组校验,上传文章接口是一个分组,不需要执行@NotNull校验,修改文章的接口是一个分组,需要执行@NotNull的校验。

所有的校验注解都有一个groups属性用来指定分组,Class[]类型,没有实际意义,因此只需要定义一个或者多个接口用来区分即可。

  1. @Data 
  2. public class ArticleDTO { 
  3.  
  4.     /** 
  5.      * 文章ID只在修改的时候需要检验,因此指定groups为修改的分组 
  6.      */ 
  7.     @NotNull(message = "文章id不能为空",groups = UpdateArticleDTO.class ) 
  8.     @Min(value = 1,message = "文章ID不能为负数",groups = UpdateArticleDTO.class) 
  9.     private Integer id; 
  10.  
  11.     /** 
  12.      * 文章内容添加和修改都是必须校验的,groups需要指定两个分组 
  13.      */ 
  14.     @NotBlank(message = "文章内容不能为空",groups = {AddArticleDTO.class,UpdateArticleDTO.class}) 
  15.     private String content; 
  16.  
  17.     @NotBlank(message = "作者Id不能为空",groups = AddArticleDTO.class) 
  18.     private String authorId; 
  19.  
  20.     /** 
  21.      * 提交时间是添加和修改都需要校验的,因此指定groups两个 
  22.      */ 
  23.     @Future(message = "提交时间不能为过去时间",groups = {AddArticleDTO.class,UpdateArticleDTO.class}) 
  24.     private Date submitTime; 
  25.      
  26.     //修改文章的分组 
  27.     public interface UpdateArticleDTO{} 
  28.  
  29.     //添加文章的分组 
  30.     public interface AddArticleDTO{} 
  31.  

JSR303本身的@Valid并不支持分组校验,但是Spring在其基础提供了一个注解@Validated支持分组校验。@Validated这个注解value属性指定需要校验的分组。

  1. /** 
  2.      * 添加文章 
  3.      * @Validated:这个注解指定校验的分组信息 
  4.      */ 
  5.     @PostMapping("/add"
  6.     public String add(@Validated(value = ArticleDTO.AddArticleDTO.class) @RequestBody ArticleDTO articleDTO, BindingResult bindingResult) throws JsonProcessingException { 
  7.         //如果有错误提示信息 
  8.         if (bindingResult.hasErrors()) { 
  9.             Map<String , String> map = new HashMap<>(); 
  10.             bindingResult.getFieldErrors().forEach( (item) -> { 
  11.                 String message = item.getDefaultMessage(); 
  12.                 String field = item.getField(); 
  13.                 map.put( field , message ); 
  14.             } ); 
  15.             //返回提示信息 
  16.             return objectMapper.writeValueAsString(map); 
  17.         } 
  18.         return "success"
  19.     } 

嵌套校验

嵌套校验简单的解释就是一个实体中包含另外一个实体,并且这两个或者多个实体都需要校验。

举个栗子:文章可以有一个或者多个分类,作者在提交文章的时候必须指定文章分类,而分类是单独一个实体,有分类ID、名称等等。大致的结构如下:

  1. public class ArticleDTO{ 
  2.   ...文章的一些属性..... 
  3.    
  4.   //分类的信息 
  5.   private CategoryDTO categoryDTO; 

此时文章和分类的属性都需要校验,这种就叫做嵌套校验。

嵌套校验很简单,只需要在嵌套的实体属性标注@Valid注解,则其中的属性也将会得到校验,否则不会校验。

如下文章分类实体类校验:

  1. /** 
  2.  * 文章分类 
  3.  */ 
  4. @Data 
  5. public class CategoryDTO { 
  6.     @NotNull(message = "分类ID不能为空"
  7.     @Min(value = 1,message = "分类ID不能为负数"
  8.     private Integer id; 
  9.  
  10.     @NotBlank(message = "分类名称不能为空"
  11.     private String name

文章的实体类中有个嵌套的文章分类CategoryDTO属性,需要使用@Valid标注才能嵌套校验,如下:

  1. @Data 
  2. public class ArticleDTO { 
  3.  
  4.     @NotBlank(message = "文章内容不能为空"
  5.     private String content; 
  6.  
  7.     @NotBlank(message = "作者Id不能为空"
  8.     private String authorId; 
  9.  
  10.     @Future(message = "提交时间不能为过去时间"
  11.     private Date submitTime; 
  12.  
  13.     /** 
  14.      * @Valid这个注解指定CategoryDTO中的属性也需要校验 
  15.      */ 
  16.     @Valid 
  17.     @NotNull(message = "分类不能为空"
  18.     private CategoryDTO categoryDTO; 
  19.   } 

Controller层的添加文章的接口同上,需要使用@Valid或者@Validated标注入参,同时需要定义一个BindingResult的参数接收校验结果。

嵌套校验针对分组查询仍然生效,如果嵌套的实体类(比如CategoryDTO)中的校验的属性和接口中@Validated注解指定的分组不同,则不会校验。

JSR-303针对集合的嵌套校验也是可行的,比如List的嵌套校验,同样需要在属性上标注一个@Valid注解才会生效,如下:

  1. @Data 
  2. public class ArticleDTO { 
  3.     /** 
  4.      * @Valid这个注解标注在集合上,将会针对集合中每个元素进行校验 
  5.      */ 
  6.     @Valid 
  7.     @Size(min = 1,message = "至少一个分类"
  8.     @NotNull(message = "分类不能为空"
  9.     private List<CategoryDTO> categoryDTOS; 
  10.   } 

总结:嵌套校验只需要在需要校验的元素(单个或者集合)上添加@Valid注解,接口层需要使用@Valid或者@Validated注解标注入参。

如何接收校验结果?

接收校验的结果的方式很多,不过实际开发中最好选择一个优雅的方式,下面介绍常见的两种方式。

BindingResult 接收

这种方式需要在Controller层的每个接口方法参数中指定,Validator会将校验的信息自动封装到其中。这也是上面例子中一直用的方式。如下:

  1. @PostMapping("/add"
  2.     public String add(@Valid @RequestBody ArticleDTO articleDTO, BindingResult bindingResult){} 

这种方式的弊端很明显,每个接口方法参数都要声明,同时每个方法都要处理校验信息,显然不现实,舍弃。

此种方式还有一个优化的方案:使用AOP,在Controller接口方法执行之前处理BindingResult的消息提示,不过这种方案仍然不推荐使用。

全局异常捕捉

参数在校验失败的时候会抛出的MethodArgumentNotValidException或者BindException两种异常,可以在全局的异常处理器中捕捉到这两种异常,将提示信息或者自定义信息返回给客户端。

全局异常捕捉之前有单独写过一篇文章,不理解的可以看满屏的try-catch,你不瘆得慌?。

作者这里就不再详细的贴出其他的异常捕获了,仅仅贴一下参数校验的异常捕获(仅仅举个例子,具体的返回信息需要自己封装),如下:

  1. @RestControllerAdvice 
  2. public class ExceptionRsHandler { 
  3.  
  4.     @Autowired 
  5.     private ObjectMapper objectMapper; 
  6.  
  7.     /** 
  8.      * 参数校验异常步骤 
  9.      */ 
  10.     @ExceptionHandler(value= {MethodArgumentNotValidException.class , BindException.class}) 
  11.     public String onException(Exception e) throws JsonProcessingException { 
  12.         BindingResult bindingResult = null
  13.         if (e instanceof MethodArgumentNotValidException) { 
  14.             bindingResult = ((MethodArgumentNotValidException)e).getBindingResult(); 
  15.         } else if (e instanceof BindException) { 
  16.             bindingResult = ((BindException)e).getBindingResult(); 
  17.         } 
  18.         Map<String,String> errorMap = new HashMap<>(16); 
  19.         bindingResult.getFieldErrors().forEach((fieldError)-> 
  20.                 errorMap.put(fieldError.getField(),fieldError.getDefaultMessage()) 
  21.         ); 
  22.         return objectMapper.writeValueAsString(errorMap); 
  23.     } 
  24.  

spring-boot-starter-validation做了什么?

这个启动器的自动配置类是ValidationAutoConfiguration,最重要的代码就是注入了一个Validator(校验器)的实现类,代码如下:

  1. @Bean 
  2.  @Role(BeanDefinition.ROLE_INFRASTRUCTURE) 
  3.  @ConditionalOnMissingBean(Validator.class) 
  4.  public static LocalValidatorFactoryBean defaultValidator() { 
  5.   LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean(); 
  6.   MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory(); 
  7.   factoryBean.setMessageInterpolator(interpolatorFactory.getObject()); 
  8.   return factoryBean; 
  9.  } 

这个有什么用呢?Validator这个接口定义了校验的方法,如下:

  1. <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups); 
  2.  
  3.  
  4. <T> Set<ConstraintViolation<T>> validateProperty(T object, 
  5.               String propertyName, 
  6.               Class<?>... groups); 
  7.                             
  8. <T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType, 
  9.               String propertyName, 
  10.               Object value, 
  11.               Class<?>... groups); 
  12. ...... 

这个Validator可以用来自定义实现自己的校验逻辑,有些大公司完全不用JSR-303提供的@Valid注解,而是有一套自己的实现,其实本质就是利用Validator这个接口的实现。

如何自定义校验?

虽说在日常的开发中内置的约束注解已经够用了,但是仍然有些时候不能满足需求,需要自定义一些校验约束。

举个栗子:有这样一个例子,传入的数字要在列举的值范围中,否则校验失败。

自定义校验注解

首先需要自定义一个校验注解,如下:

  1. @Documented 
  2. @Constraint(validatedBy = { EnumValuesConstraintValidator.class}) 
  3. @Target({ METHOD, FIELD, ANNOTATION_TYPE }) 
  4. @Retention(RUNTIME) 
  5. @NotNull(message = "不能为空"
  6. public @interface EnumValues { 
  7.     /** 
  8.      * 提示消息 
  9.      */ 
  10.     String message() default "传入的值不在范围内"
  11.  
  12.     /** 
  13.      * 分组 
  14.      * @return 
  15.      */ 
  16.     Class<?>[] groups() default { }; 
  17.  
  18.     Class<? extends Payload>[] payload() default { }; 
  19.  
  20.     /** 
  21.      * 可以传入的值 
  22.      * @return 
  23.      */ 
  24.     int[] values() default { }; 

根据Bean Validation API 规范的要求有如下三个属性是必须的:

  • message:定义消息模板,校验失败时输出
  • groups:用于校验分组
  • payload:Bean Validation API 的使用者可以通过此属性来给约束条件指定严重级别. 这个属性并不被API自身所使用。

除了以上三个必须要的属性,添加了一个values属性用来接收限制的范围。

该校验注解头上标注的如下一行代码:

  1. @Constraint(validatedBy = { EnumValuesConstraintValidator.class}) 

这个@Constraint注解指定了通过哪个校验器去校验。

自定义校验注解可以复用内嵌的注解,比如@EnumValues注解头上标注了一个@NotNull注解,这样@EnumValues就兼具了@NotNull的功能。

自定义校验器

@Constraint注解指定了校验器为EnumValuesConstraintValidator,因此需要自定义一个。

自定义校验器需要实现ConstraintValidator这个接口,第一个泛型是校验注解,第二个是参数类型。代码如下:

  1. /** 
  2.  * 校验器 
  3.  */ 
  4. public class EnumValuesConstraintValidator implements ConstraintValidator<EnumValues,Integer> { 
  5.     /** 
  6.      * 存储枚举的值 
  7.      */ 
  8.     private  Set<Integer> ints=new HashSet<>(); 
  9.  
  10.     /** 
  11.      * 初始化方法 
  12.      * @param enumValues 校验的注解 
  13.      */ 
  14.     @Override 
  15.     public void initialize(EnumValues enumValues) { 
  16.         for (int value : enumValues.values()) { 
  17.             ints.add(value); 
  18.         } 
  19.     } 
  20.  
  21.     /** 
  22.      * 
  23.      * @param value  入参传的值 
  24.      * @param context 
  25.      * @return 
  26.      */ 
  27.     @Override 
  28.     public boolean isValid(Integer value, ConstraintValidatorContext context) { 
  29.         //判断是否包含这个值 
  30.         return ints.contains(value); 
  31.     } 

如果约束注解需要对其他数据类型进行校验,则可以的自定义对应数据类型的校验器,然后在约束注解头上的@Constraint注解中指定其他的校验器。

演示

校验注解和校验器自定义成功之后即可使用,如下:

  1. @Data 
  2. public class AuthorDTO { 
  3.     @EnumValues(values = {1,2},message = "性别只能传入1或者2"
  4.     private Integer gender; 

总结 

数据校验作为客户端和服务端的一道屏障,有着重要的作用,通过这篇文章希望能够对JSR-303数据校验有着全面的认识。

 

责任编辑:武晓燕 来源: 码猿技术专栏
相关推荐

2021-12-23 11:10:38

税收大数据大数据税收

2021-03-26 05:59:10

内存检测工具

2020-03-06 08:15:54

新人技术主管陈琦

2022-06-30 12:44:48

黑客数据泄露

2015-05-18 09:23:04

JavaScript数组

2017-05-24 17:25:44

2022-12-31 13:53:53

工程

2018-11-07 12:19:18

辞退北大能力

2021-06-02 10:06:52

神经网络数据图形

2021-02-07 18:19:44

RabbitMQ客户端

2023-07-17 09:19:20

CSSCSS 渐变

2021-03-08 08:02:40

IDEA插件JSON

2021-03-26 15:18:11

代码工具Mockoon

2020-09-08 08:45:39

jupyter插件代码

2015-11-11 14:38:18

2022-07-26 00:00:00

MQ消息中间件

2020-06-11 09:09:33

Ansible模块Linux

2020-10-20 18:59:40

数据湖数据仓库采集

2024-10-17 09:45:03

2012-12-26 09:46:03

点赞
收藏

51CTO技术栈公众号