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

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

 
测试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

Spring框架中,@Valid 注解是一个强大的工具,用于验证和校验用户输入的数据,确保数据的合法性和完整性。通过结合其他注解和验证器,@Valid 注解可以轻松地实现数据验证,从而提高应用程序的健壮性和安全性。本文将详细介绍 Spring 中的 @Valid 注解,以及如何在应用程序中正确使用它。

什么是 @Valid 注解?

@Valid 注解是 Spring 框架中的一个注解,用于在方法参数或方法返回值上标记数据校验的目标。它告诉 Spring 在处理方法调用时要进行数据验证。该注解通常与其他验证相关的注解结合使用,如 @NotNull@NotBlank@Min@Max 等,用于指定验证规则。

使用 @Valid 注解进行方法参数验证

在方法参数上使用 @Valid 注解,可以在方法调用之前对参数进行验证。这对于确保输入数据的有效性非常有用。

以下是一个示例,展示了如何在 Spring 控制器方法中使用 @Valid 注解进行参数验证:

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @PostMapping("/register")
    public ResponseEntity<String> registerUser(@Valid @RequestBody UserDto userDto) {
        // 处理用户注册逻辑
        return ResponseEntity.ok("User registered successfully");
    }
}

在上面的示例中,@Valid 注解用于 userDto 参数上。Spring 将会在调用 registerUser 方法之前,对 userDto 参数的属性进行验证。

结合其他验证注解

通常情况下,@Valid 注解会与其他验证相关的注解结合使用,以定义验证规则。例如,你可以使用 @NotNull@NotBlank@Min@Max 等注解来对属性进行更具体的验证。

public class UserDto {

    @NotNull
    @NotBlank
    private String username;

    @NotNull
    @Min(18)
    private Integer age;

    // ...其他属性和方法
}

在上面的示例中,username 属性使用了 @NotNull 和 @NotBlank 注解,表示它不能为空且不能只包含空格。age 属性使用了 @NotNull 和 @Min(18) 注解,表示它不能为空且必须大于等于 18。
在Spring框架中,你可以使用各种验证注解来定义不同类型的校验规则。这些注解位于javax.validation.constraints包下,用于对数据进行不同方面的验证。以下是一些常用的验证注解及其对应的校验规则:

  1. @NotNull 验证字段不能为null

  2. @NotBlank 验证字符串不能为空,且至少包含一个非空字符。

  3. @NotEmpty 验证字符串、集合或数组不能为空,不同于@NotBlank,它不要求至少包含一个非空字符。

  4. @Min(value) 验证数字必须大于等于指定的最小值。

  5. @Max(value) 验证数字必须小于等于指定的最大值。

  6. @Size(max, min) 验证字符串、集合或数组的大小必须在指定的范围内。

  7. @Email 验证字符串是否为合法的电子邮件地址。

  8. @Pattern(regexp) 验证字符串是否符合指定的正则表达式。

  9. @Digits(integer, fraction) 验证数字是否符合指定的位数要求,包括整数和小数部分。

  10. @Positive 验证数字必须为正数。

  11. @Negative 验证数字必须为负数。

  12. @Past 验证日期必须为过去的时间。

  13. @Future 验证日期必须为将来的时间。

  14. @AssertTrue 验证字段必须为true

  15. @AssertFalse 验证字段必须为false

  16. @CreditCardNumber 验证字符串是否为合法的信用卡号。

  17. @URL 验证字符串是否为合法的URL。

  18. @Valid 用于标记需要嵌套验证的对象。

这些只是一些常见的验证注解,实际上在Spring中还有更多的验证注解可供使用。你可以根据具体的业务需求,选择合适的验证注解来定义数据的校验规则。另外,你还可以通过自定义验证器来实现更复杂的校验逻辑,以满足特定的验证需求。

自定义验证器

除了使用预定义的验证注解,你还可以创建自定义的验证器来满足特定的验证需求。要创建一个自定义的验证器,需要实现 javax.validation.ConstraintValidator 接口。

以下是一个示例,展示了如何创建一个自定义的验证器:

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class AgeConstraintValidator implements ConstraintValidator<AgeConstraint, Integer> {

    @Override
    public boolean isValid(Integer age, ConstraintValidatorContext context) {
        return age != null && age >= 18;
    }
}

然后,你可以在自定义的注解上使用这个验证器:

import javax.validation.Constraint;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({FIELD, METHOD, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = AgeConstraintValidator.class)
public @interface AgeConstraint {
    String message() default "Invalid age";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

最后,将这个自定义的注解应用到需要验证的属性上:

public class UserDto {

    @NotNull
    @NotBlank
    private String username;

    @AgeConstraint
    private Integer age;

    // ...其他属性和方法
}

总结

@Valid 注解是 Spring 框架中一个重要的工具,用于实现数据的验证和校验。结合其他验证注解和自定义验证器,它能

够轻松地验证用户输入的数据,从而提高应用程序的稳定性和安全性。通过在方法参数或方法返回值上添加 @Valid 注解,你可以确保数据的有效性,并在数据不合法时产生相应的错误信息,从而提供更好的用户体验和数据完整性。

 

posted @ 2024-02-04 16:57  CharyGao  阅读(18)  评论(0编辑  收藏  举报