专栏首页码猿技术专栏SpringBoot整合JSR303实现参数校验

SpringBoot整合JSR303实现参数校验

前言

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

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

什么是 JSR-303?

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

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

添加依赖

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

<dependency>
    <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-validation</artifactId>
</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

被注释的元素必须在合适的范围内

如何使用?

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

简单校验

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

@Data
public class ArticleDTO {
    @NotNull(message = "文章id不能为空")
    @Min(value = 1,message = "文章ID不能为负数")
    private Integer id;
    @NotBlank(message = "文章内容不能为空")
    private String content;
    @NotBlank(message = "作者Id不能为空")
    private String authorId;
    @Future(message = "提交时间不能为过去时间")
    private Date submitTime;
}

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

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

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

/**
     * 添加文章
     */
    @PostMapping("/add")
    public String add(@Valid @RequestBody ArticleDTO articleDTO, BindingResult bindingResult) throws JsonProcessingException {
        //如果有错误提示信息
        if (bindingResult.hasErrors()) {
            Map<String , String> map = new HashMap<>();
            bindingResult.getFieldErrors().forEach( (item) -> {
                String message = item.getDefaultMessage();
                String field = item.getField();
                map.put( field , message );
            } );
            //返回提示信息
            return objectMapper.writeValueAsString(map);
        }
        return "success";
    }

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

分组校验

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

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

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

@Data
public class ArticleDTO {
    /**
     * 文章ID只在修改的时候需要检验,因此指定groups为修改的分组
     */
    @NotNull(message = "文章id不能为空",groups = UpdateArticleDTO.class )
    @Min(value = 1,message = "文章ID不能为负数",groups = UpdateArticleDTO.class)
    private Integer id;
    /**
     * 文章内容添加和修改都是必须校验的,groups需要指定两个分组
     */
    @NotBlank(message = "文章内容不能为空",groups = {AddArticleDTO.class,UpdateArticleDTO.class})
    private String content;
    @NotBlank(message = "作者Id不能为空",groups = AddArticleDTO.class)
    private String authorId;
    /**
     * 提交时间是添加和修改都需要校验的,因此指定groups两个
     */
    @Future(message = "提交时间不能为过去时间",groups = {AddArticleDTO.class,UpdateArticleDTO.class})
    private Date submitTime;
    
    //修改文章的分组
    public interface UpdateArticleDTO{}
    //添加文章的分组
    public interface AddArticleDTO{}

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

/**
     * 添加文章
     * @Validated:这个注解指定校验的分组信息
     */
    @PostMapping("/add")
    public String add(@Validated(value = ArticleDTO.AddArticleDTO.class) @RequestBody ArticleDTO articleDTO, BindingResult bindingResult) throws JsonProcessingException {
        //如果有错误提示信息
        if (bindingResult.hasErrors()) {
            Map<String , String> map = new HashMap<>();
            bindingResult.getFieldErrors().forEach( (item) -> {
                String message = item.getDefaultMessage();
                String field = item.getField();
                map.put( field , message );
            } );
            //返回提示信息
            return objectMapper.writeValueAsString(map);
        }
        return "success";
    }

嵌套校验

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

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

public class ArticleDTO{
  ...文章的一些属性.....
  
  //分类的信息
  private CategoryDTO categoryDTO;
}

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

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

如下文章分类实体类校验

/**
 * 文章分类
 */
@Data
public class CategoryDTO {
    @NotNull(message = "分类ID不能为空")
    @Min(value = 1,message = "分类ID不能为负数")
    private Integer id;
    @NotBlank(message = "分类名称不能为空")
    private String name;
}

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

@Data
public class ArticleDTO {
    @NotBlank(message = "文章内容不能为空")
    private String content;
    @NotBlank(message = "作者Id不能为空")
    private String authorId;
    @Future(message = "提交时间不能为过去时间")
    private Date submitTime;
    /**
     * @Valid这个注解指定CategoryDTO中的属性也需要校验
     */
    @Valid
    @NotNull(message = "分类不能为空")
    private CategoryDTO categoryDTO;
  }

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

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

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

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

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

如何接收校验结果?

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

BindingResult 接收

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

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

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

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

全局异常捕捉

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

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

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

@RestControllerAdvice
public class ExceptionRsHandler {
    @Autowired
    private ObjectMapper objectMapper;
    /**
     * 参数校验异常步骤
     */
    @ExceptionHandler(value= {MethodArgumentNotValidException.class , BindException.class})
    public String onException(Exception e) throws JsonProcessingException {
        BindingResult bindingResult = null;
        if (e instanceof MethodArgumentNotValidException) {
            bindingResult = ((MethodArgumentNotValidException)e).getBindingResult();
        } else if (e instanceof BindException) {
            bindingResult = ((BindException)e).getBindingResult();
        }
        Map<String,String> errorMap = new HashMap<>(16);
        bindingResult.getFieldErrors().forEach((fieldError)->
                errorMap.put(fieldError.getField(),fieldError.getDefaultMessage())
        );
        return objectMapper.writeValueAsString(errorMap);
    }

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

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

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

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

<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);
<T> Set<ConstraintViolation<T>> validateProperty(T object,
													 String propertyName,
													 Class<?>... groups);
                           
<T> Set<ConstraintViolation<T>> validateValue(Class<T> beanType,
												  String propertyName,
												  Object value,
												  Class<?>... groups);
......

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

如何自定义校验?

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

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

自定义校验注解

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

@Documented
@Constraint(validatedBy = { EnumValuesConstraintValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@NotNull(message = "不能为空")
public @interface EnumValues {
    /**
     * 提示消息
     */
    String message() default "传入的值不在范围内";
    /**
     * 分组
     * @return
     */
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
    /**
     * 可以传入的值
     * @return
     */
    int[] values() default { };
}

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

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

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

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

@Constraint(validatedBy = { EnumValuesConstraintValidator.class})

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

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

自定义校验器

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

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

/**
 * 校验器
 */
public class EnumValuesConstraintValidator implements ConstraintValidator<EnumValues,Integer> {
    /**
     * 存储枚举的值
     */
    private  Set<Integer> ints=new HashSet<>();
    /**
     * 初始化方法
     * @param enumValues 校验的注解
     */
    @Override
    public void initialize(EnumValues enumValues) {
        for (int value : enumValues.values()) {
            ints.add(value);
        }
    }
    /**
     *
     * @param value  入参传的值
     * @param context
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        //判断是否包含这个值
        return ints.contains(value);
    }
}

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

演示

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

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

总结

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

另外作者的第一本PDF书籍已经整理好了,由浅入深的详细介绍了Mybatis基础以及底层源码,有需要的朋友公众号回复关键词Mybatis进阶即可获取,目录如下:

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • SpringBoot系列之@Value和@ConfigurationProperties

    继上一篇博客SpringBoot系列之YAML配置用法之后,再写一篇@Value、@ConfigurationProperties的对比博客

    SmileNicky
  • SpringBoot集成JSR303校验

    爱撒谎的男孩
  • 封装了一个Excel导入加校验的工具,同事们用了都说好

    最近在做Excel导入功能,产品要求对导入数据先进行校验然后再入库。于是简单封装了一个工具,结果兄弟们用了都说好,今天就把思路分享出来。

    码农小胖哥
  • Spring Boot入门(一)

    第二个父项目用来加载所有的依赖版本,所以每次导入依赖的时候不需要书写版本号,spring boot默认会导入这个父项目中的版本。如果当前依赖在spring bo...

    石的三次方
  • Spring Boot 配置文件

    springboot 项目创建完成后,在 resources目录下会生成一个 application.properties 文件,用于编写springboot ...

    LCyee
  • Spring Boot 使用 JSR303 实现参数验证

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

    程序员果果
  • Spring Boot 使用 JSR303 实现参数验证

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

    程序员果果
  • JavaBean基于注解实现校验

    前言 上一文我通过传递不合法参数触发异常,进行了统一拦截,那么这篇文章主要介绍JSR303,Hibernate Validator详细讲解及如何优雅的对参数进行...

    胖虎
  • Spring Boot之JSR303数据校验及多环境切换

    但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置文件;

    兮动人
  • springBoot读取配置文件的注解@ConfigurationProperties及与@Value区别

    在sprigboot中,处理配置文件最好的方法是采用@ConfigurationProperties注解。该注解能方便的将配置文件中的属性配置到具体的对象中。 ...

    冬天里的懒猫
  • springmvc之如何对表单数据进行校验

    2、spring在进行数据绑定时,可同时调用校验框架完成数据校验工作。在springmvc中,可直接通过注解驱动的方式进行数据校验。

    西西嘛呦
  • 使用spring validation完成数据后端校验

    前言 数据的校验是交互式网站一个不可或缺的功能,前端的js校验可以涵盖大部分的校验职责,如用户名唯一性,生日格式,邮箱格式校验等等常用的校验。但是为了避免用户...

    kirito-moe
  • springboot系列学习(七)JSR303数据校验 , 注解不能使用爆红的解决方法,后端自动的校验实体类的值是不是合法的,

    我们之前在前端,会校验我们输入的值是不是合法的,比如email,如果不是email格式那么就报错。这个是前段 的验证规则,其实后端也是可以的。这个就是JSR30...

    一天不写程序难受
  • Spring MVC-07循序渐进之验证器 下 (JSR 303验证)

    JSR303”Bean Validation” 和 JSR349 “Bean Validation 1.1”指定了一整套的API,通过标注对象属性添加约束。

    小小工匠
  • 补习系列-springboot 参数校验详解

    在定义 Restful 风格的接口时,通常会采用 PathVariable 指定关键业务参数,如下:

    美码师
  • 【依葫芦画瓢】SSM-CRUD-3

    继续上一篇的讲解【依葫芦画瓢】SSM-CRUD --- 2 概要: 服务端返回json数据,构建员工列表 完成员工新增功能 增加表单前后端校验(jQuery+J...

    企鹅号小编
  • Dubbo使用jsr303框架hibernate-validator遇到 ConstraintDescriptorImpl could not be instantiated

    Dubbo可以集成jsr303标准规范的验证框架,作为验证框架不二人选的hibernate-validator是大家都会经常在项目中使用的,但是在Dubbo使用...

    王念博客
  • Spring Boot之yaml配置注入基本使用

    1、在springboot项目中的resources目录下新建一个文件 application.yml 2、编写一个实体类 Dog;

    兮动人
  • 一坨一坨的 if/else 参数校验,终于被 SpringBoot 参数校验组件整干净了!

    数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后...

    Guide哥

扫码关注云+社区

领取腾讯云代金券