美文网首页
Web开发轮子(一)——使用spring validation注

Web开发轮子(一)——使用spring validation注

作者: moutory | 来源:发表于2022-12-10 22:25 被阅读0次

    前言

    web开发过程中,难免会遇到参数校验的场景,这种需求往往校验的行为类似,只是具体的规则不同。比如A功能校验文本的长度是否大于10,B功能要校验文本的长度是否大于5。虽然这些校验都可以通过代码来实现,但如果有工具类来帮助我们应对这类场景的话,就可以让我们把更多精力放在核心业务逻辑上面。实际上,针对这个场景spring已经提供了相关的注解来帮助我们优雅地完成参数校验。
    本篇文章也将这对这部分的知识点进行总结,方便自己后面回顾。同时也希望对各位读者有所帮助

    文章最后有注解总结,对背景和使用过程没有兴趣的话可以直接看最后的部分即可。

    一、聊聊背景

    spring validate并不是凭空冒出来的工具类,实际上这个工具类和JSR303/JSR-349标准的出现有着密切的关系。

    (一)JSR303/JSR-349标准

    JSR-303规范(Bean Validation规范)提供了对 Java EE 和 Java SE 中的 Java Bean 进行验证的方式。该规范主要使用注解的方式来实现对 Java Bean 的验证功能 ,JSR-349是其的升级版本,添加了一些新特性。他们规定一些校验规范即校验注解,如@Null,@NotNull,@Pattern,他们位于javax.validation.constraints包下

    (二)Hibernate Validation实现

    规范虽然定下来了,但是具体的实现还是得有人来做,hibernate validation是对这个规范的实践(这里的hibernate不是指orm框架的hibernate),他提供了相应的实现,并增加了一些其他校验注解,如@Email,@Length,@Range等等,他们位于org.hibernate.validator.constraints包下。

    (三)Spring Validation实现

    为了让开发者可以更加便捷地使用validation注解,Spring对hibernate validation进行了二次封装,显示校验validated bean时,你可以使用spring validation或者hibernate validation,而spring validation另一个特性,便是其在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中。
    springboot在2.3之前默认集成了spring validation工具,但在2.3版本之后就没有再默认集成这个工具类了,想要使用的话需要我们单独引入,具体操作可以看文章后面介绍。

    也就是说,spring validationJSR303/JSR-349标准的具体实现,同时也是基于hibernate validation所封装和拓展的工具类。

    二、开始使用

    (一)项目中引入validation依赖

    我们在第一章中提到,考虑到开发的便捷性,springboot已经帮我们做了validation依赖的继承,所以理论上来说,当我们项目中引入spring-boot-starter-web时,就可以自动引入validation依赖了。

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

    但是,在springboot2.3之后,springboot移除了对validation的默认依赖引入,所以对于springboot2.3之后的项目,我们需要单独引入对应的依赖。

    <dependency>
        <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
    (二)在项目中进行validation注解的使用
    步骤一:定义Request实体类
    public class ValidateRequest {x
        @NotNull(message="产品名称不能为空")
        private String productName;
    
        public String getProductName() {
            return productName;
        }
        public void setProductName(String productName) {
            this.productName = productName;
        }
       ...
    }
    

    @NotNull一般是加在字段上面,校验失败后有默认的错误信息,但我们一般会传入message自定义的错误信息。

    步骤二:定义Controller代码
    @RestController
    public class ValidationController {
    
        @RequestMapping(value = "/validate",method = RequestMethod.POST)
        public ResponseResult validate(@RequestBody @Validated  ValidateRequest request, BindingResult bindingResult){
            String errorMsg = this.handleBindResult(bindingResult);
            if(StringUtils.hasText(errorMsg)){
                return ResponseResult.FAIL(errorMsg);
            }
            return ResponseResult.OK(null);
        }
    
        private String handleBindResult(BindingResult bindingResult) {
            if(bindingResult.hasErrors()){
                for (ObjectError error : bindingResult.getAllErrors()) {
                    System.out.println("errorMessage = " + error.getDefaultMessage());
                    return error.getDefaultMessage();
                }
            }
            return null;
        }
    }
    

    除了常规的MVC配置外,我们需要在我们的入参前面加上@Validated注解,表示当前这个接口需要启用实体类中的参数校验。同时,需要有BindingResult作为入参去承接校验的结果(如果不加的话,当字段校验失败后接口就会直接返回400状态码出去,后台只能看到失败日志,但是不能对校验的结果进行个性化的处理,例如统一错误格式返回给前端)。

    步骤三:启动项目进行测试

    当我们的接口入参没有传productName的时候,就会把错误信息返回给前端,不需要再手写校验代码。

    测试1-入参为空

    当我们的接口入参有传productName的时候,就会正常返回结果

    测试2-入参不为空

    注意:实际项目中,我们一般更多的做法是定义全局的异常处理类,在处理BindingResult的过程中根据情况抛出异常,再来进行统一的异常结果返回。

    (三)各种注解使用用例
    @NotNull
        @NotNull(message="产品名称不能为空")
        private String productName;
    
    @Null
        @Null(message="别名必须为空")
        private String aliasName;
    
    @AssertTrue
        @AssertTrue(message="inDisCount值必须为true")
        private boolean inDisCount;
    
    @AssertFalse
        @AssertFalse(message="stopSell值必须为true")
        private boolean stopSell;
    
    @Min和@Max
        @Min(value = 100,message = "价格不得低于100")
        @Max(value = 500,message = "价格不得高于500")
        private Integer price;
    
    @Size

    需要注意,@Size注解并不能用在数值上面,而是用在String类型的数据上面,用于限制长度

        @Size(min = 3,max = 10,message = "用户名长度需要在3-10之间")
        private String userName;
    
    @DecimalMin和@DecimalMax

    有相似功能的注解还有@Range,但是@Range注解适用于整数,而这两个注解可以用于小数级别的限制

        @DecimalMin(value = "100.5",message = "价格不得低于100.5")
        @DecimalMax(value = "200.5",message = "价格不得高于100.5")
        private BigDecimal price;
    
    @Digits

    这个注解主要用来限制数字的格式

        @Digits(integer = 3,fraction = 3,message = "价格格式错误,整数位和小数位不得超过3位")
        private BigDecimal price;
    
    @Past

    如果是LocalDateTime这类时间格式的话,一般需要搭配JsonFormat注解使用

        @Past(message = "销售时间必须为过去的时间")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        private LocalDateTime sellTime;
    
    @Future

    如果是LocalDateTime这类时间格式的话,一般需要搭配JsonFormat注解使用

        @Future(message = "过期时间必须为未来的时间")
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        private LocalDateTime expirationTime;
    
    @Pattern

    十分实用的一个规则校验注解,我们可以在上面通过正则来定义对应的规则

        @Pattern(regexp = "^QiQv[1-9]{2}$",message = "格式错误")
        private String userName;
    
    @Length
        @Length(min = 3,max = 5,message = "长度需在3-5之间")
        private String userName;
    
    @Email
        @Email(regexp = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$",message = "邮件格式错误")
        private String email;
    

    注意,@Pattern注解中还允许我们传递flag属性,用来解决负责场景下的格式校验,有兴趣的可以查阅资料看一下这些模式的用法。

    public static enum Flag {
            UNIX_LINES(1),
            CASE_INSENSITIVE(2),
            COMMENTS(4),
            MULTILINE(8),
            DOTALL(32),
            UNICODE_CASE(64),
            CANON_EQ(128);
        }
    
    @NotBlank
        @NotBlank(message = "邮箱不能为空")
        private String email;
    
    @Range
        @Range(min = 1,max = 300,message = "年龄需在1-300之间")
        private Integer age;
    

    @Email默认的校验规则是.*,一般来说,我们需要根据实际的要求来定义邮箱的格式

    (四)使用分组校验功能

    使用validation注解时我们可能会遇到这样一种场景,同一个Dto可能会在新增和查询接口中使用到,但是这两个接口需要校验的字段规则不同,单独再创建一个相同的dto显然是有点不优雅的,validation已经为我们提供了分组的概念入口,我们可以根据场景需要在同一个dto中划分不同的规则定义。

    步骤一:定义分组

    一般来说,接口的操作类型可以大致分为增删改查四种类型,所以我们在项目中可以根据需要定义分组。比如我们暂时把操作类型分为新增更新这两种类型(分组)。

    public interface Group {
    
        interface Add{
    
        }
        interface Update{
    
        }
        interface Delete{
    
        }
    }
    
    步骤二:在validation注解上面使用分组,注明这个校验应用在哪一种场景

    比如下面的代码,新增和删除的操作场景我们用一套校验规则,更新的时候用另外一套校验规则

        @Range(min = 1,max = 300,message = "年龄需在1-300之间",groups = {Group.Add.class,Group.Delete.class})
        @Range(min = 1,max = 20,message = "年龄需在1-20之间",groups = Group.Update.class)
        private Integer age;
    
    步骤三:在接口中标识接口所适用的分组

    在上个步骤中,我们定义了不同分组的校验规则,下一步的话我们需要在接口上面定义该接口所使用的分组,比如是更新接口,那么就使用更新分组的校验。

        @RequestMapping(value = "/validate",method = RequestMethod.POST)
        public ResponseResult validate(@RequestBody @Validated({Group.Update.class})  ValidateRequest request, 
                                                  BindingResult bindingResult){
            String errorMsg = this.handleBindResult(bindingResult);
            if(StringUtils.hasText(errorMsg)){
                return ResponseResult.FAIL(errorMsg);
            }
            return ResponseResult.OK(null);
        }
    
        private String handleBindResult(BindingResult bindingResult) {
            ...
        }
    

    注意:@Validated注解里面可以传多个值,只需要放入{}中即可

    (五)创建自定义注解

    项目中校验场景难免会有spring validation所涵盖不了的场景,比如校验某个字段的值是否含有空格。针对这种情况,如果出现的频率比较少的话,建议直接在代码里面校验就行。如果需要校验的场景比较多的话,就可以考虑做成一个注解,方便后面使用。

    下面我们就以校验某个字段的值是否含有空格这个功能为例,来创建一个自定义注解

    步骤一:创建自定义注解

    正常定义一个注解,重点在于有message属性和groups属性。我们可以在message属性中定义默认的错误信息。
    同时,需要在自定义注解上加入@Constraint注解,标识由哪个类来做具体的校验逻辑。如下面的代码所示,我们定义了将由BlankValidation类来做是否存在空格的校验。

    PS:如果希望注解支持使用多次的话,可以加上@Repeatable注解,记得要在自定义注解中定义一个内部注解

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Constraint(validatedBy = {BlankValidation.class})
    @Repeatable(NotContainWhiteBlank.List.class) // 让注解可以在同个地方使用多次,比如同个字段上面使用多次来实现分组校验
    public @interface NotContainWhiteBlank {
    
        String message() default "字段中间不允许有空格";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default { };
    
        @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        @interface List {
            NotContainWhiteBlank[] value();
        }
    }
    
    步骤二:创建校验类

    校验类需要实现ConstraintValidator接口,同时需要传入校验类对应的注解名和支持的校验类型。注解名就是我们在步骤一定义的自定义注解,因为我们这个案例是用来校验文本中是否有空格,所以校验的参数就为String。(也可以理解为校验类型就是定义我们校验的是什么数据类型的数据)
    自定义校验类的核心在于实现isValid,为true表示通过校验,为false表示不通过校验。ConstraintValidatorContext 这个参数上下文包含了认证中所有的信息,我们可以利用这个上下文实现获取默认错误提示信息,禁用错误提示信息,改写错误提示信息等操作。

    public class BlankValidation implements ConstraintValidator<NotContainWhiteBlank,String> {
    
        @Override
        public void initialize(NotContainWhiteBlank constraintAnnotation) {
            
        }
    
        @Override
        public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
            return !StringUtils.containsWhitespace(value);
        }
    }
    
    步骤三:在Controller接口中验证我们的自定义接口
    public class ValidateRequest {
    
        @NotContainWhiteBlank
        private String productName;
        ...忽略get、set方法
    }
    
    @RestController
    public class ValidationController {
    
        @RequestMapping(value = "/validate",method = RequestMethod.POST)
        public ResponseResult validate(@RequestBody @Validated ValidateRequest request, BindingResult bindingResult){
            String errorMsg = this.handleBindResult(bindingResult);
            if(StringUtils.hasText(errorMsg)){
                return ResponseResult.FAIL(errorMsg);
            }
            return ResponseResult.OK(null);
        }
    
        private String handleBindResult(BindingResult bindingResult) {
            if(bindingResult.hasErrors()){
                for (ObjectError error : bindingResult.getAllErrors()) {
                    System.out.println("errorMessage = " + error.getDefaultMessage());
                    return error.getDefaultMessage();
                }
            }
            return null;
        }
    }
    

    我们在postman中进行测试,可以看到自定义注解确实已经生效了。

    测试结果

    总结

    注解名称 功能
    @Null 被注释的元素必须为null
    @NotNull 被注释的元素必须不为null
    @AssertTrue 被注释的元素必须为true
    @AssertFalse 被注释的元素必须为false
    @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    @Size(max, min) 被注释的元素必须为null
    @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
    @Past 被注释的元素必须是一个过去的日期
    @Future 被注释的元素必须是一个将来的日期
    @Pattern(value) 被注释的元素必须符合指定的正则表达式
    @Email 被注释的元素必须是电子邮箱地址
    @Length 被注释的字符串的大小必须在指定的范围内
    @NotEmpty 被注释的字符串的必须非空
    @Range 被注释的元素必须在合适的范围内

    参考文章:

    java:validate注解做校验 https://blog.csdn.net/en_joker/article/details/110440190
    Java中JSR303的基本使用详情 https://www.jb51.net/article/263439.htm

    相关文章

      网友评论

          本文标题:Web开发轮子(一)——使用spring validation注

          本文链接:https://www.haomeiwen.com/subject/pzktqdtx.html