Web开发轮子(一)——使用spring validation注解和自定义校验注解

前言

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的时候,就会把错误信息返回给前端,不需要再手写校验代码。

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

注意:实际项目中,我们一般更多的做法是定义全局的异常处理类,在处理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;
    ...忽略getset方法
}

@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

posted @ 2022-12-11 22:25  moutory  阅读(97)  评论(0编辑  收藏  举报  来源