SpringBoot 参数校验

2024/1/22 SpringBoot 框架

# 1、JSR303规范

# 1.1、以往检验案例

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("add")
    public Boolean add(UserPO user) {
        if(user.getName()==null) {
            return ResponseResult.fail("user name should not be empty");
        } else if(user.getName().length()<5 || user.getName().length()>50){
            return ResponseResult.fail("user name length should between 5-50");
        }
        if(user.getAge()< 1 || user.getAge()> 150) {
            return ResponseResult.fail("invalid age");
        }

        return Boolean.TRUE;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

  • 前端传递给接口的参数,后端是需要二次校验的,以防止不合法数据导致的一系列问题。
  • 以往的校验案例来看,这种校验方式显得很臃肿,不美观,代码可读性不强。
  • 针对这种普遍的不规范,Java开发者在Java API规范 (JSR303) 定义了Bean校验的标准validation-api,但没有提供实现。

# 1.2、JSR303规范说明

了解更多可以查看:JSR 303: Bean Validation (opens new window)

  • Java开发者在Java API规范 (JSR303) 定义了Bean校验的标准validation-api,但没有提供实现。
  • hibernate validation是对这个规范的实现,并增加了校验注解如@Email、@Length等。
  • Spring Validation是对hibernate validation的二次封装,用于支持spring mvc参数自动校验。

# 2、JSR303实现案例

# 2.1、引入依赖

  • SpringBoot 项目的旧版 spring-boot-starter-web 依赖中已经有 hibernate-validator 包,不需要引用相关依赖。
  • 在新的版本中,spring-boot-starter-web 依赖中不再有 hibernate-validator 包(如 2.3.11.RELEASE),需要自己引入 spring-boot-starter-validation 依赖。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>3.2.1</version>
</dependency>
1
2
3
4
5

# 2.2、DTO参数封装

  • PO(Persistent Object)是持久化对象,与数据库表对应。
  • VO(Value Object)是值对象,用于数据展示和传输。
  • DTO(Data Transfer Object)是数据传输对象,用于不同层或模块之间的数据传输。
@Data
public class StudentDTO implements Serializable {

    private Long id;

    @NotEmpty(message = "name is not null")
    private String name;

    @Min(value = 1, message = "age must be greater than 1")
    private Integer age;

    @Email(message = "email is not valid")
    private String email;

    @Pattern(regexp = "[01]", message = "only can one or zero")
    private String sex;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 2.3、测试使用

接收UserDTO对象时,加入@Valid或者@Validated注解。如果需要查看错误信息结果,可以在后面接BindingResult类进行参数校验结果绑定,注意BindingResult紧跟其后,中间不能有别的参数。

  • @Valid 和 @Validated 都用来触发一次校验, @Valid 是 JSR 303规范的注解, @Validated 是Spring 加强版的注解。
  • @Validated加强功能:分组。
@ResponseResultApi
@PostMapping("/add2")
public Boolean addStudent2(@Valid @RequestBody StudentDTO student, BindingResult bindingResult){
    if (bindingResult.hasErrors()) {
        List<ObjectError> errors = bindingResult.getAllErrors();
        errors.forEach(p -> {
            FieldError fieldError = (FieldError) p;
            // 打印错误
            log.error("Invalid Parameter : object - {},field - {},errorMessage - {}", fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage());
        });
        throw new RuntimeException("Invalid Parameter");
    }

    StudentPO studentPO = new StudentPO();
    BeanUtils.copyProperties(student,studentPO);
    return studentService.save(studentPO);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

校验结果

调用接口响应结果,这里发现我们使用@ResponseResultApi注解返回时,没法按我们统一接口返回错误,可以参考:统一异常处理

# 3、@Validated

# 3.1、@Validated分组校验

上面的例子中,其实存在一个问题,UserDTO既可以作为add的参数(id为空),又可以作为update的参数(id不能为空),这时候就可以用@Validated分组校验了。

  • 定义分组接口,无需实现
public interface AddValidationGroup {
}
public interface EditValidationGroup {
}
1
2
3
4
  • DTO使用分组校验
@Data
@Builder
public class StudentDTO implements Serializable {

    // 编辑更新时时不能为空
    @NotEmpty(message = "id is not Empty", groups = {EditValidationGroup.class})
    private Long id;

}
1
2
3
4
5
6
7
8
9
  • 使用分组校验测试 直接使用@Validated标注为哪个分组即可
@ResponseResultApi
@PostMapping("/add3")
public Boolean addStudent3(@Validated(AddValidationGroup.class) @RequestBody StudentDTO student){
    StudentPO studentPO = new StudentPO();
    BeanUtils.copyProperties(student,studentPO);
    return studentService.save(studentPO);
}

@ResponseResultApi
@PostMapping("/update")
public Boolean updateStudent(@Validated(EditValidationGroup.class) @RequestBody StudentDTO student){
    StudentPO studentPO = new StudentPO();
    BeanUtils.copyProperties(student,studentPO);
    return studentService.updateById(studentPO);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3.2、@Validated 对比 @Valid

  • @Valid 是 JSR 303规范的注解, @Validated 是Spring 加强版的注解。
  • @Validated 提供了分组功能,而@Valid没有分组功能。
  • @Validated:可以用在类型、方法和方法参数上。但是不能用在成员属性(字段)上。@Valid:可以用在方法、构造函数、方法参数和成员属性(字段)上。
  • 嵌套类型只能用@Valid。

嵌套类型只能用@Valid,其实也相当于成员属性了。

@Data
@Builder
public class UserDTO implements Serializable {

    // 这里只能用@Valid
    @Valid 
    private AddressDTO address;

}
1
2
3
4
5
6
7
8
9

# 3.3、自定义validation

当Validation中提供的注解不足以满足我们的需求,我们还可以自定义。

  • 定义注解
@Target({ElementType.METHOD,ElementType.FIELD,ElementType.ANNOTATION_TYPE,ElementType.CONSTRUCTOR,
        ElementType.PARAMETER,ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {TelephoneNumberValidator.class}) // 指定校验器
public @interface TelephoneNumber {
    String message() default "Invalid telephone number";
    Class<?>[] groups() default { };
    Class<? extends Payload>[] payload() default { };
}
1
2
3
4
5
6
7
8
9
10
  • 定义校验器
/**
 * 手机号验证器
 */
public class TelephoneNumberValidator implements ConstraintValidator<TelephoneNumber, String> {
    private static final String REGEX_TEL = "0\\d{2,3}[-]?\\d{7,8}|0\\d{2,3}\\s?\\d{7,8}|13[0-9]\\d{8}|15[1089]\\d{8}";

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        try {
            return Pattern.matches(REGEX_TEL, s);
        } catch (Exception e) {
            return false;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 使用校验器
@Data
@Builder
public class StudentDTO implements Serializable {

    @TelephoneNumber(message = "invalid telephone number")
    private String phoneNumber;

}
1
2
3
4
5
6
7
8

# 4、常见的校验注解

# 4.1、JSR-303/JSR-349

强调一点

  • JSR303/JSR-349是Java的校验规范,用于对Java对象进行参数校验和数据验证。这些规范定义了一些注解,开发人员可以将这些注解应用于Java对象的属性上,从而定义校验规则。JSR303提供了一些基本的校验注解,如@Null(检查属性是否为null)、@NotNull(检查属性是否不为null)、@Pattern(检查属性是否匹配指定的正则表达式)等。这些注解位于javax.validation.constraints包下。
  • JSR-349是JSR303的升级版本,添加了一些新特性,如可重复的校验注解、集合容器的校验支持等。可重复的校验注解使开发人员可以使用相同的校验规则多次应用于同一个属性。集合容器的校验支持使开发人员可以对集合类型的属性进行校验,如检查是否为空、是否满足指定数量的元素等。
  • 尽管JSR303和JSR-349提供了校验规范,但它们并不提供具体的实现。开发人员需要使用实现了这些规范的校验框架,如Hibernate Validator、Spring Framework中的Validator等来进行校验操作。这些校验框架会根据注解的规范进行相应的校验并返回校验结果。
@AssertFalse            // 元素只能为false
@AssertTrue             // 元素只能为true
@DecimalMax             // 元素必须小于或等于{value}
@DecimalMin             // 元素必须大于或等于{value}
@Digits                 // 元素数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
@Email                  // 元素不是一个合法的电子邮件地址
@Future                 // 元素需要是一个将来的时间
@FutureOrPresent        // 元素需要是一个将来或现在的时间
@Max                    // 元素最大不能超过{value}
@Min                    // 元素最小不能小于{value}
@Negative               // 元素必须是负数
@NegativeOrZero         // 元素必须是负数或零
@NotBlank               // 元素不能为空,非null并且去除空格后为非空字符串,或者只包含空白字符也不行
@NotEmpty               // 元素不能为空,非null并且去除空格后为非空字符串
@NotNull                // 元素不能为null
@Null                   // 元素必须为null
@Past                   // 元素需要是一个过去的时间
@PastOrPresent          // 元素需要是一个过去或现在的时间
@Pattern                // 元素需要匹配正则表达式"{regexp}"
@Positive               // 元素必须是正数
@PositiveOrZero         // 元素必须是正数或零
@Size                   // 元素个数必须在{min}和{max}之间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 4.2、Hibernate Validation

Hibernate Validator是对JSR303/JSR-349规范的具体实现,它提供了一套强大的校验框架,并扩展了一些其他的校验注解,如@Email(检查属性是否符合邮箱格式)、@Length(检查属性的长度是否在指定范围内)、@Range(检查属性的值是否在指定范围内)等等。

@CreditCardNumber        // 元素不合法的信用卡号码
@Currency                // 元素不合法的货币 (必须是{value}其中之一)
@EAN                     // 元素不合法的{type}条形码
@Email                   // 元素不是一个合法的电子邮件地址  (已过期)
@Length                  // 元素长度需要在{min}和{max}之间
@CodePointLength         // 元素长度需要在{min}和{max}之间
@LuhnCheck               // 元素${validatedValue}的校验码不合法, Luhn模10校验和不匹配
@Mod10Check              // 元素${validatedValue}的校验码不合法, 模10校验和不匹配
@Mod11Check              // 元素${validatedValue}的校验码不合法, 模11校验和不匹配
@ModCheck                // 元素${validatedValue}的校验码不合法, ${modType}校验和不匹配  (已过期)
@NotBlank                // 元素不能为空  (已过期)
@NotEmpty                // 元素不能为空  (已过期)
@ParametersScriptAssert  // 元素执行脚本表达式"{script}"没有返回期望结果
@Range                   // 元素需要在{min}和{max}之间
@SafeHtml                // 元素可能有不安全的HTML内容
@ScriptAssert            // 元素执行脚本表达式"{script}"没有返回期望结果
@URL                     // 元素需要是一个合法的URL
@DurationMax             // 元素必须小于指定时间
@DurationMin             // 元素必须大于指定时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 4.3、Spring validation

Spring框架对Hibernate Validator进行了二次封装,在Spring MVC模块中添加了自动校验的功能,并将校验的结果封装到特定的类中。上面我们已经讲过validation的分组使用了,这里不再赘述。