---
url: /article/prt57vpz/index.md
---
## 1. 需求

### 1.1. 什么是 JSR-303

在 JavaWeb 项目开发中，常常需要进行接口参数校验，这个需求在 JSR-303规范中被提到，JSR是Java Specification Requests的缩写，意思是Java 规范提案。是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR，以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。JSR-303 是 JAVA EE 6 中的一项子规范，叫做 Bean Validation，官方参考实现是 Hibernate Validator。

### 1.2. hibernate-validator 简介

`hibernate-validator` 是 JSR-303 规范的一个实现，也是 Java 开发中使用最广泛的参数校验框架，而它也被集成到了 Spring 家族中 `spring-boot-starter-validation` ，你可以轻易的使用它来完成接口参数校验。使用文档参见 [Hibernate Validator](https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-gettingstarted-createproject)。

## 2. 使用前置

### 2.1. 引入依赖

Maven 项目中引入以下任一坐标即可：

```xml
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.0.Final</version>
</dependency>
```

或

```xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>2.5.5</version>
</dependency>
```

### 2.2. 常用注解

| 注解                                         | 可验证的数据类型                                                                                | 说明                                                                                                                                   |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| @AssertFalse                                 | Boolean,boolean                                                                                 | 验证注解的元素值是false                                                                                                                |
| @AssertTrue                                  | Boolean,boolean                                                                                 | 验证注解的元素值是true                                                                                                                 |
| @NotNull                                     | 任意类型                                                                                        | 验证注解的元素值不是null                                                                                                               |
| @Null                                        | 任意类型                                                                                        | 验证注解的元素值是null                                                                                                                 |
| @NotEmpty                                    | CharSequence子类型、Collection、Map、数组                                                       | 验证注解的元素值不为null且不为空（字符串长度不为0、集合大小不为0）                                                                     |
| @NotBlank                                    | CharSequence子类型                                                                              | 验证注解的元素值不为空（不为null、去除首位空格后长度为0），不同于@NotEmpty，@NotBlank只应用于字符串且在比较时会去除字符串的首位空格    |
| @Min(value=值)                               | BigDecimal，BigInteger, byte,short, int, long，等任何Number或CharSequence（存储的是数字）子类型 | 验证注解的元素值大于等于@Min指定的value值                                                                                              |
| @Max（value=值）                             | 和@Min要求一样                                                                                  | 验证注解的元素值小于等于@Max指定的value值                                                                                              |
| @DecimalMin(value=值)                        | 和@Min要求一样                                                                                  | 验证注解的元素值大于等于@ DecimalMin指定的value值                                                                                      |
| @DecimalMax(value=值)                        | 和@Min要求一样                                                                                  | 验证注解的元素值小于等于@ DecimalMax指定的value值                                                                                      |
| @Digits(integer=整数位数, fraction=小数位数) | 和@Min要求一样                                                                                  | 验证注解的元素值的整数位数和小数位数上限                                                                                               |
| @Size(min=下限, max=上限)                    | 字符串、Collection、Map、数组等                                                                 | 验证注解的元素值的在min和max（包含）指定区间之内，如字符长度、集合大小                                                                 |
| @Past                                        | java.util.Date,java.util.Calendar;Joda Time类库的日期类型                                       | 验证注解的元素值（日期类型）比当前时间早                                                                                               |
| @Future                                      | 与@Past要求一样                                                                                 | 验证注解的元素值（日期类型）比当前时间晚                                                                                               |
| @Length(min=下限, max=上限)                  | CharSequence子类型                                                                              | 验证注解的元素值长度在min和max区间内                                                                                                   |
| @Range(min=最小值, max=最大值)               | BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型                  | 验证注解的元素值在最小值和最大值之间                                                                                                   |
| @Email(regexp=正则表达式,flag=标志的模式)    | CharSequence子类型（如String）                                                                  | 验证注解的元素值是Email，也可以通过regexp和flag指定自定义的email格式                                                                   |
| @Pattern(regexp=正则表达式,flag=标志的模式)  | String，任何CharSequence的子类型                                                                | 验证注解的元素值与指定的正则表达式匹配                                                                                                 |
| @Valid                                       | 任何非原子类型                                                                                  | 指定递归验证关联的对象；如用户对象中有个地址对象属性，如果想在验证用户对象时一起验证地址对象的话，在地址对象上加@Valid注解即可级联验证 |
| @CreditCardNumber                            | 数字                                                                                            | 对信用卡号进行一个大致的验证                                                                                                           |
| @URL (protocol=,host,port)                   | 网址                                                                                            | 检查是否是一个有效的URL，如果提供了protocol，host等，则该URL还需满足提供的条件                                                         |

## 3. 开始使用

### 3.1. 快速开始

#### 3.1.1. 创建模型 User

在需要校验的属性上添加合适的注解，方可校验。

```java
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class User implements Serializable {
    private static final long serialVersionUID = -7242397021777229674L;

    @NotEmpty
    private String name;
    @Min(1)
    private Integer age;
    @Email
    private String email;
    @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}")
    private String phone;
}
```

#### 3.1.2. 创建 Handler Method `get` 和 `post`

在请求参数前加 `@Valid` 或 `@Validated` 注解任一即可。
区别在于 `@Valid` 是 Java 的注解，`@Validated` 是 Spring 的注解，是对  `@Valid` 注解的增强，兼容 `@Valid` 注解。

```java
@RestController
@RequestMapping
public class DemoController {

    @GetMapping
    public String get(@Valid User user) {
        return ObjUtils.toJsonStr(user);
    }

    @PostMapping
    public String post(@Validated @RequestBody User user) {
        return ObjUtils.toJsonStr(user);
    }
}
```

#### 3.1.3. 统一异常处理，统一响应

当实体类中的注解校验不通过时，会抛出 `BindException` 异常，这里捕获到异常，获取到属性名和错误提示，组装返回。

```java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BindException.class)
    public R bindExceptionHandler(BindException e) {
        log.debug("bindExcept: {}", e.getMessage());
        FieldError fieldError = e.getBindingResult().getFieldError();
        assert fieldError != null;
        return R.fail(String.format("%s %s", fieldError.getField(), fieldError.getDefaultMessage()));
    }
}
```

GET 请求 <http://127.0.0.1:8080> ，响应体：

```json
{
    "code": 500,
    "data": null,
    "success": false,
    "message": "name must not be empty"
}
```

### 3.2. 自定义校验

有时候内置注解不能满足需求，我们可以自定义校验方式，参照文档 [Creating custom constraints](https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-customconstraints) 一节。
如，前端传来一个值，我们需要验证值是否被包含在我们指定的值中。

#### 3.2.1. 创建自定义注解

`strValues` 指定字符串类型数组；`intValues` 指定数字类型数组；`enumClass` 指定一个枚举类。当指定的数组元素较多时，可以将其封装到枚举类中，简化编码。为了规范使用，这里定义了一个接口，枚举类必须实现这个接口才可以实现校验。

```java
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {MatchAnyValidator.class})
@Repeatable(MatchAny.List.class)// 使注解支持重复定义
public @interface MatchAny {

    String message() default "not match any one";

    String[] strValues() default {};

    int[] intValues() default {};

    Class<? extends ValidateAble> enumClass() default EmptyValidateEnum.class;

    //分组
    Class<?>[] groups() default {};

    //负载
    Class<? extends Payload>[] payload() default {};

    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        MatchAny[] value();
    }
}
```

```java
public interface ValidateAble {

    String getValidateValue();
}
```

#### 3.2.2. 实现校验逻辑

这里需要实现 `ConstraintValidator<MatchAny, Object>` 接口，泛型的第一个参数是自定义注解类，第二个参数是可以校验的数据类型，因为我们自定义的注解可以校验 String 和 int 类型，所以这里使用了 Object。
`initialize` 方法用来获取注解中的值，`isValid` 方法是校验逻辑，校验通过返回 `true`，否则返回 `false`。

```java
// 该类在 Spring 容器启动时即加载，无需添加 @Component 注解
public class MatchAnyValidator implements ConstraintValidator<MatchAny, Object> {
    private String[] strValues;
    private int[] intValues;
    private Class<? extends ValidateAble> clazz;

    @Override
    public void initialize(MatchAny constraintAnnotation) {
        strValues = constraintAnnotation.strValues();
        intValues = constraintAnnotation.intValues();
        clazz = constraintAnnotation.enumClass();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (Objects.isNull(value)) {
            return true;
        }
        boolean pass;
        // 集合
        if (value instanceof Collection) {
            pass = ((Collection<?>) value).stream().allMatch(this::validValue);
            // 数组
        } else if (value.getClass().isArray()) {
            pass = Arrays.stream(((Object[]) value)).allMatch(this::validValue);
            // 单值
        } else {
            pass = validValue(value);
        }
        if (!pass) {
            // 禁用默认的错误提示
            context.disableDefaultConstraintViolation();
            String valueStr = getValueStr();
            context.buildConstraintViolationWithTemplate(String.format("must be one of [%s]", valueStr)).addConstraintViolation();
            return false;
        }
        return true;
    }

    /**
     * @param value
     * @Description: 校验单个值，值可能是数字或字符串
     * @return: boolean
     * @Author: Bobby.Ma
     * @Date: 2021/12/15 15:06
     */
    private boolean validValue(Object value) {
        if (clazz != EmptyValidateEnum.class) {
            return Arrays.stream(clazz.getEnumConstants()).map(ValidateAble::getValidateValue).anyMatch(o -> Objects.equals(o, value));
        } else if (value instanceof String) {
            return Arrays.asList(strValues).contains(value);
        } else if (value instanceof Integer) {
            return IntStream.of(intValues).anyMatch(i -> Objects.equals(i, value));
        } else {
            // 只支持校验 数字或字符串
            return false;
        }
    }

    private String getValueStr() {
        if (clazz.isEnum() && clazz != EmptyValidateEnum.class) {
            ValidateAble[] validateAbles = clazz.getEnumConstants();
            return Arrays.stream(validateAbles).map(ValidateAble::getValidateValue).map(Object::toString).collect(Collectors.joining(","));
        } else {
            if (ArrayUtils.isNotEmpty(strValues)) {
                return String.join(",", strValues);
            } else {
                return Arrays.stream(intValues).mapToObj(Objects::toString).collect(Collectors.joining(","));
            }
        }
    }
}
```

#### 3.2.3. 定义一个枚举类

枚举类要实现 `ValidateAble` 接口。

```java
@AllArgsConstructor
public enum PermissionEnum implements ValidateAble {
    ADMIN("admin"),
    STAFF("staff"),
    BOSS("boss");

    private final String value;

    @Override
    public String getValidateValue() {
        return this.value;
    }
}
```

#### 3.2.4. 使用自定义注解

在模型 User 中新增 `permission` 属性，并添加 `@MatchAny` 注解。

```java
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class User implements Serializable {
    private static final long serialVersionUID = -7242397021777229674L;

    @NotEmpty
    private String name;
    @Min(1)
    private Integer age;
    @Email
    private String email;
    @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}")
    private String phone;

    @MatchAny(enumClass = PermissionEnum.class)
    private String permission;
}
```

GET 请求 <http://127.0.0.1:8080?name=bobby&permission=undefine> ，响应体：

```json
{
    "code": 500,
    "data": null,
    "success": false,
    "message": "permission must be one of [admin,staff,boss]"
}
```

### 3.3. PathVariable 数据校验

如果参数是在 uri 中该如何校验呢？

#### 3.3.1. 新增 Handler Method

在 `DemoController` 中新增一个 `path` 方法，其参数注解有 `@PathVariable("age")`。注意，需要在当前类上注解 `@Validated` 才生效，`@Valid` 无效，因此尽量使用  `@Validated` 注解。
`@Validated` 注解在类型和注解在方法参数上并不冲突，如 `post` 方法中的 `@Validated` 只对 `User` 实体中的校验注解生效，而 `DemoController` 类上的  `@Validated` 只对 `path` 方法中的参数生效。

```java
@Validated
@RestController
@RequestMapping
public class DemoController {

    @GetMapping
    public String get(@Valid User user) {
        return ObjUtils.toJsonStr(user);
    }

    @PostMapping
    public String post(@Validated @RequestBody User user) {
        return ObjUtils.toJsonStr(user);
    }

    @GetMapping("/{age}")
    public String path(@PathVariable("age") @Min(1) Integer age) {
        return age.toString();
    }
}
```

#### 3.3.2. 增加异常捕获

方法中校验的参数若不是一个实体类，则用上述方式校验，此时抛出的异常为 `ConstraintViolationException` ，因此新增异常捕获。

```java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BindException.class)
    public R bindExceptionHandler(BindException e) {
        log.debug("bindExcept: {}", e.getMessage());
        FieldError fieldError = e.getBindingResult().getFieldError();
        assert fieldError != null;
        return R.fail(String.format("%s %s", fieldError.getField(), fieldError.getDefaultMessage()));
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public R validationExceptionHandler(ConstraintViolationException e) {
        log.debug("validationException: {}", e.getMessage());
        ConstraintViolation<?> constraintViolation = e.getConstraintViolations().stream().findFirst().orElse(null);
        assert constraintViolation != null;
        String[] path = constraintViolation.getPropertyPath().toString().split("\\.");
        String field = path[path.length - 1];
        String message = constraintViolation.getMessage();
        return R.fail(String.format("%s %s", field, message));
    }

    @ExceptionHandler(Exception.class)
    public R exceptionHandler(Exception e) {
        log.debug("unknown error: {}", e.getMessage());
        return R.fail();
    }
}
```

GET 请求 <http://127.0.0.1:8080/0> ，响应体：

```json
{
    "code": 500,
    "data": null,
    "success": false,
    "message": "age must be greater than or equal to 1"
}
```

### 3.4. 嵌套校验

当模型中的属性嵌套了另一个模型，需要对嵌套的模型进行校验呢？

#### 3.4.1. 新增模型 `Information`

```java
@Data
public class Information implements Serializable {
    private static final long serialVersionUID = -3128793162502773246L;

    @NotEmpty
    private String address;

    private String telPhone;
}
```

#### 3.4.2. 在模型 `User` 中增加模型 `Information` 属性

注意，`Information` 属性上注解的是 `@Valid`，`@Validated` 不支持注解在这里。

```java
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@GroupSequenceProvider(UserGroupSequenceProvider.class)
public class User implements Serializable {
    private static final long serialVersionUID = -7242397021777229674L;

    @Null(groups = {ValidAddGroup.class})
    @NotNull(groups = {ValidUpdateGroup.class})
    private Integer id;

    @NotEmpty(groups = {ValidAddGroup.class, ValidUpdateGroup.class})
    private String name;
    @Min(1)
    private Integer age;
    @Email
    private String email;
    @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}")
    private String phone;

    @MatchAny(enumClass = PermissionEnum.class, groups = {ValidAddGroup.class, ValidUpdateGroup.class})
    private String permission;

    @MatchAny(strValues = {"language", "math", "english"}, groups = {ValidLearningGroup.class})
    private String learning;

    @MatchAny(strValues = {"screw", "brick", "coding"}, groups = {ValidWorkingGroup.class})
    private String working;

    @Valid
    private Information information;
}
```

POST 请求 <http://127.0.0.1:8080/condition> ，请求体：

```json
{
    "age":20,
    "name":"xxx",
    "information":{
        
    }
}
```

响应体：

```json
{
    "code": 500,
    "data": null,
    "success": false,
    "message": "information.address must not be empty"
}
```

### 3.5. 分组校验

有的时候同一个字段，在不同场合下的校验方式不同。例如：新增时我们需要校验 id 字段为空，但更新时需要校验 id 字段不为空，该如何做呢？

#### 3.5.1. 定义两个空接口

接口只需要定义，作为标记，无需任何方法实现.

```java
// 新增时的校验组
public interface ValidAddGroup {
}
```

```java
// 更新时的校验组
public interface ValidUpdateGroup {
}
```

#### 3.5.2. 在被校验字段的注解中加上分组

现在新增一个属性 id，并添加注解同时标记分组 `@Null(groups = {ValidAddGroup.class})` 和 `@NotNull(groups = {ValidUpdateGroup.class})` 。
表示，当指定了 `ValidAddGroup` 组时需要校验 `@Null`；当指定了 `ValidUpdateGroup` 组时需要校验 `@NotNull`。

```java
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class User implements Serializable {
    private static final long serialVersionUID = -7242397021777229674L;

    @Null(groups = {ValidAddGroup.class})
    @NotNull(groups = {ValidUpdateGroup.class})
    private Integer id;

    @NotEmpty
    private String name;
    @Min(1)
    private Integer age;
    @Email
    private String email;
    @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}")
    private String phone;

    @MatchAny(enumClass = PermissionEnum.class)
    private String permission;
}
```

#### 3.5.3. 新增两个 Handler Method

在 `DemoController` 中新增两个方法：`add()` 和 `update()`。在 `add()` 方法中我们指定了 `ValidAddGroup` 组，在 `update()` 中我们指定了 `ValidUpdateGroup`。
注意这里要使用 `@Validated` 注解，而 `@Valid` 不支持分组校验。

```java
@Validated
@RestController
@RequestMapping
public class DemoController {

    @GetMapping
    public R get(@Valid User user) {
        return R.data(user);
    }

    @PostMapping
    public R post(@Validated @RequestBody User user) {
        return R.data(user);
    }

    @GetMapping("/{age}")
    public R path(@PathVariable("age") @Min(1) Integer age) {
        return R.data(age);
    }

    @PostMapping("/add")
    public R add(@RequestBody @Validated({ValidAddGroup.class}) User user) {
        return R.data(user);
    }

    @PutMapping("/update")
    public R update(@RequestBody @Validated({ValidUpdateGroup.class}) User user) {
        return R.data(user);
    }
}
```

POST 请求 <http://127.0.0.1:8080/add> ，请求体：

```json
{
    "id":1
}
```

响应体：

```json
{
    "code": 500,
    "data": null,
    "success": false,
    "message": "id must be null"
}
```

PUT 请求 <http://127.0.0.1:8080/update> ，请求体：

```json
{
    "permission": "sudo"
}
```

响应体：

```json
{
    "code": 500,
    "data": null,
    "success": false,
    "message": "id must not be null"
}
```

***

这时候有些长得帅的朋友可能要问了，model 中那些没有标记分组的注解还生效吗？
我们来试一试，当给 `update()` 方法请求体中给出 id 的值：
PUT 请求  <http://127.0.0.1:8080/update> ，请求体：

```json
{
    "id":1,
    "permission": "sudo"
}
```

响应体：

```json
{
    "code": 0,
    "data": {
        "id": 1,
        "name": null,
        "age": null,
        "email": null,
        "phone": null,
        "permission": "sudo"
    },
    "success": true,
    "message": "success"
}
```

我们可以看到成功响应了，但我们明明是有校验 `name` 属性 `@NotEmpty` 的，而且请求体中的 `permission` 属性值也不对，却还是通过了校验。这就说明，当我们在方法参数中指定了校验分组时，只有符合分组标记的校验才会生效，不符合或者没标记的一律不生效，因此就需要给  `name` 和 `permission` 注解上指定校验分组。但 `name` 和 `permission` 在新增和更新时都需要校验，此时可以同时添加 `ValidAddGroup` 和 `ValidUpdateGroup` 两种标记，因为校验注解中的 `groups()` 属性值是 一个数组 `Class<?>[]`，`@Validated` 注解中的 `value()` 属性值同样也是一个数组 `Class<?>[]` ，因此也可以指定多个分组。

```java
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class User implements Serializable {
    private static final long serialVersionUID = -7242397021777229674L;

    @Null(groups = {ValidAddGroup.class})
    @NotNull(groups = {ValidUpdateGroup.class})
    private Integer id;

    @NotEmpty(groups = {ValidAddGroup.class,ValidUpdateGroup.class})
    private String name;
    @Min(1)
    private Integer age;
    @Email
    private String email;
    @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}")
    private String phone;

    @MatchAny(enumClass = PermissionEnum.class,groups = {ValidAddGroup.class,ValidUpdateGroup.class})
    private String permission;
}
```

PUT 请求  <http://127.0.0.1:8080/update> ，请求体：

```json
{
    "id":1,
    "permission": "sudo"
}
```

响应体：

```json
{
    "code": 500,
    "data": null,
    "success": false,
    "message": "permission must be one of [admin,staff,boss]"
}
```

### 3.6. 逻辑校验

上面的校验方式仍然比较简单，当我们的需要自定义校验逻辑又需要怎么做呢？
`hibernate-validator` 为我们提供了一个接口 `DefaultGroupSequenceProvider`，这个接口不是 JSR-303标准的，没有默认实现类。
例如，我们需要判断模型 `User` 中的 `age` 属性值，当 `age <= 22` 时，校验 `learning` 属性值，否则校验 `working` 属性值。其实，核心思想就是动态指定校验分组。

#### 3.6.1. `User` 模型中新增属性 `learning` 和 `working`

```java
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@GroupSequenceProvider(UserGroupSequenceProvider.class)
public class User implements Serializable {
    private static final long serialVersionUID = -7242397021777229674L;

    @Null(groups = {ValidAddGroup.class})
    @NotNull(groups = {ValidUpdateGroup.class})
    private Integer id;

    @NotEmpty(groups = {ValidAddGroup.class, ValidUpdateGroup.class})
    private String name;
    @Min(1)
    private Integer age;
    @Email
    private String email;
    @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}")
    private String phone;

    @MatchAny(enumClass = PermissionEnum.class, groups = {ValidAddGroup.class, ValidUpdateGroup.class})
    private String permission;

    @MatchAny(strValues = {"language", "math", "english"}, groups = {ValidLearningGroup.class})
    private String learning;

    @MatchAny(strValues = {"screw", "brick", "coding"}, groups = {ValidWorkingGroup.class})
    private String working;
}
```

#### 3.6.2. 定义两个校验组 `ValidLearningGroup` 和 `ValidWorkingGroup`

```java
public interface ValidLearningGroup {
}
```

```java
public interface ValidWorkingGroup {
}
```

#### 3.6.3. 定义一个 `DefaultGroupSequenceProvider` 的实现

在模型 `User` 中加上 `@GroupSequenceProvider(UserGroupSequenceProvider.class)`。

```java
public class UserGroupSequenceProvider implements DefaultGroupSequenceProvider<User> {
    @Override
    public List<Class<?>> getValidationGroups(User user) {
        List<Class<?>> defaultGroupSequence = new ArrayList<>();
        defaultGroupSequence.add(User.class); // 这一步不能省,否则Default分组都不会执行了，会抛错的
        if (Objects.nonNull(user)) { // 这块判空请务必要做
            Integer age = user.getAge();
            if (age <= 22) {
                defaultGroupSequence.add(ValidLearningGroup.class);
            } else {
                defaultGroupSequence.add(ValidWorkingGroup.class);
            }
        }
        return defaultGroupSequence;
    }
}
```

#### 3.6.4. 新增一个 Handler Method `condition`

```java
@Validated
@RestController
@RequestMapping
public class DemoController {

    @GetMapping
    public R get(@Valid User user) {
        return R.data(user);
    }

    @PostMapping
    public R post(@Validated @RequestBody User user) {
        return R.data(user);
    }

    @GetMapping("/{age}")
    public R path(@PathVariable("age") @Min(1) Integer age) {
        return R.data(age);
    }

    @PostMapping("/add")
    public R add(@RequestBody @Validated({ValidAddGroup.class}) User user) {
        return R.data(user);
    }

    @PutMapping("/update")
    public R update(@RequestBody @Validated({ValidUpdateGroup.class}) User user) {
        return R.data(user);
    }

    @PostMapping("/condition")
    public R condition(@Validated @RequestBody User user) {
        return R.data(user);
    }
}
```

POST 请求  <http://127.0.0.1:8080/condition> ，请求体：

```json
{
    "age":50,
    "learning":"xxx",
    "working":"xxx"
}
```

响应体：

```json
{
    "code": 500,
    "data": null,
    "success": false,
    "message": "working must be one of [screw,brick,coding]"
}
```

***

这个时候那些长得帅的朋友可能又会问了，其他属性值还会校验吗？
我们来实测一下：

POST 请求  <http://127.0.0.1:8080/condition> ，请求体：

```json
{
    "age":20,
    "name":"",
    "email":"xxx"
}
```

响应体：

```json
{
    "code": 500,
    "data": null,
    "success": false,
    "message": "email must be a well-formed email address"
}
```

我们可以看到，虽然 `age` 有值，但没有给 `learning` 属性，因此不会去校验。而 `name` 属性值虽然不符合，但由于其注解上有标记分组，而我们 handler 方法中没有指定分组(其实是默认分组)，因此也不会校验。剩下的 `email` 属性，由于其注解上没有标记分组，即为默认分组，因此执行校验。
综上，对于分组校验，我们可以总结以下2点：

* 当 handler 中没有指定校验分组(即默认分组)时，模型中的没有指定分组(即默认分组)的属性，以及 `DefaultGroupSequenceProvider` 指定的分组(包含了默认分组)才会被校验；
* 当 handler 中指定了校验分组时，此时仅指定的分组才会被校验。

因此，当需要分组校验时，需要格外注意。

### 3.7. 校验排序

若有多个校验分组时，默认校验是无序的。

#### 3.7.1. 新增一个 Handler Method `sequence`，此时并未指定任何分组

```java
@Validated
@RestController
@RequestMapping
public class DemoController {

    @GetMapping
    public R get(@Valid User user) {
        return R.data(user);
    }

    @PostMapping
    public R post(@Validated @RequestBody User user) {
        return R.data(user);
    }

    @GetMapping("/{age}")
    public R path(@PathVariable("age") @Min(1) Integer age) {
        return R.data(age);
    }

    @PostMapping("/add")
    public R add(@RequestBody @Validated({ValidAddGroup.class}) User user) {
        return R.data(user);
    }

    @PutMapping("/update")
    public R update(@RequestBody @Validated({ValidUpdateGroup.class}) User user) {
        return R.data(user);
    }

    @PostMapping("/condition")
    public R condition(@Validated @RequestBody User user) {
        return R.data(user);
    }

    @GetMapping("/sequence")
    public R sequence(@Validated User user) {
        return R.data(user);
    }
}
```

GET 请求  <http://127.0.0.1:8080/sequence?email=xxx&phone=123>&#x20;

响应体：

```json
{
    "code": 500,
    "data": null,
    "success": false,
    "message": "phone must match \"[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}\""
}
```

可以看到，虽然在模型 `User` 中 `email` 定义在前，请求参数也是 `email` 在前，但还是先校验的 `phone`。

如果我们需要指定分组的校验顺序，先校验 `email` 呢，可以使用 `@GroupSequence` 注解，这是 JSR-303 提供的。

#### 3.7.2. 新增两个分组 `EmailSeq` 和 `PhoneSeq`

```java
public interface EmailSeq {
}
```

```java
public interface PhoneSeq {
}
```

#### 3.7.3. 定义一个分组序列

当指定校验分组为 `UserSequence` 时，会依次按照 `@GroupSequence` 指定的顺序执行校验。

```java
@GroupSequence({EmailSeq.class,PhoneSeq.class})
public interface UserSequence {
}
```

#### 3.7.4. 指定模型 `User` 中的 `email` 和 `phone` 属性的校验分组，并给 Handler Method `sequence` 增加 `UserSequence` 校验组

```java
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@GroupSequenceProvider(UserGroupSequenceProvider.class)
public class User implements Serializable {
    private static final long serialVersionUID = -7242397021777229674L;

    @Null(groups = {ValidAddGroup.class})
    @NotNull(groups = {ValidUpdateGroup.class})
    private Integer id;

    @NotEmpty(groups = {ValidAddGroup.class, ValidUpdateGroup.class})
    private String name;
    @Min(1)
    private Integer age;
    @Email(groups = {EmailSeq.class})
    private String email;
    @Pattern(regexp = "[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}", groups = {PhoneSeq.class})
    private String phone;

    @MatchAny(enumClass = PermissionEnum.class, groups = {ValidAddGroup.class, ValidUpdateGroup.class})
    private String permission;

    @MatchAny(strValues = {"language", "math", "english"}, groups = {ValidLearningGroup.class})
    private String learning;

    @MatchAny(strValues = {"screw", "brick", "coding"}, groups = {ValidWorkingGroup.class})
    private String working;

    @Valid
    private Information information;
}
```

```java
@Validated
@RestController
@RequestMapping
public class DemoController {

    @GetMapping
    public R get(@Valid User user) {
        return R.data(user);
    }

    @PostMapping
    public R post(@Validated @RequestBody User user) {
        return R.data(user);
    }

    @GetMapping("/{age}")
    public R path(@PathVariable("age") @Min(1) Integer age) {
        return R.data(age);
    }

    @PostMapping("/add")
    public R add(@RequestBody @Validated({ValidAddGroup.class}) User user) {
        return R.data(user);
    }

    @PutMapping("/update")
    public R update(@RequestBody @Validated({ValidUpdateGroup.class}) User user) {
        return R.data(user);
    }

    @PostMapping("/condition")
    public R condition(@Validated @RequestBody User user) {
        return R.data(user);
    }

    @GetMapping("/sequence")
    public R sequence(@Validated(UserSequence.class) User user) {
        return R.data(user);
    }
}
```

GET 请求  <http://127.0.0.1:8080/sequence?phone=123&email=xxx>&#x20;

响应体：

```json
{
    "code": 500,
    "data": null,
    "success": false,
    "message": "email must be a well-formed email address"
}
```

可以看到，`email` 先被校验。

## 4. 扩展

### 4.1. 接口参数多态+参数校验

若有一个 Handler Method ，它的参数可能有多种对象，每种对象的校验方式又不相同，但是参数只定义一个对象接收，该如何做呢？
有两种方式实现：

* 方法一：将所有的子类属性定义在一个对象中，给属性校验添加分组，然后结合逻辑校验来动态指定不同的校验分组。这种方式的缺点在于，将所有子类对象属性揉在一起可能会很多，再添加上一堆校验分组，校验逻辑就很不直观，不便于维护。
* 方法二：实现方法请求参数的多态。定义一个父类，让所有子类都继承它，然后根据一个属性值来判断反序列化为哪种子类，然后在子类中完成各自的属性校验。这种方式就非常直观。

这里我将介绍方法二的实现步骤。

#### 4.1.1. 定义一个父类 `Person`，两个子类 `Teacher` 和 `Student`

父类中重写了子类中的所有方法，主要是为了能够不用强转为子类也可以获取子类的值，当然你可以不实现这些方法，使用的时候强转为目标类型即可。
**说明**：注解 `@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true, defaultImpl = Person.class)` 其中 `property = "type"` 表示判断的属性为 `type`，注意 `defaultImpl = Person.class`，这个如果不指定会反序列化失败，直接抛异常。而指定后表示当 `type` 不存在或者不是给定的值时，默认转为 `Person` 类型，然后就可以校验对象中的 `type` 属性值，`@NotEmpty` 和 `@MatchAny` 会生效。
注解 `@JsonSubTypes` 中的属性表示当 `type`值为 teacher 时实例化为 `Teacher` 类型，当`type`值为 student 时实例化为 `Student` 类型。

```java
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true, defaultImpl = Person.class)
@JsonSubTypes({
        @JsonSubTypes.Type(value = Teacher.class, name = "teacher"),
        @JsonSubTypes.Type(value = Student.class, name = "student")
})
public class Person {

    @NotEmpty
    @MatchAny(strValues = {"teacher", "student"})
    private String type;

    public Integer getStuId() {
        return null;
    }

    public Integer getTeaId() {
        return null;
    }

    public String getName() {
        return null;
    }

    public Integer getAge() {
        return null;
    }

    public String getClassNo() {
        return null;
    }
}
```

```java
@EqualsAndHashCode(callSuper = true)
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Teacher extends Person {

    /**
     * 工号
     */
    private Integer teaId;

    private String name;

    @Min(28)
    private Integer age;
}
```

```java
@EqualsAndHashCode(callSuper = true)
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Student extends Person {

    /**
     * 学号
     */
    private Integer stuId;

    private String name;

    @Min(7)
    private Integer age;

    /**
     * 班号
     */
    private String classNo;
}
```

#### 4.1.2. 新增一个 Handler Method `extend`

实测，`GET` 和 `POST` 请求都可以完成类型映射，但只有为 `POST` 请求时才会校验子类中的属性。

```java
@RestController
@RequestMapping("/extend")
public class ExtendController {

    @PostMapping
    public R extend(@RequestBody @Validated Person person) {
        return R.data(person);
    }
}
```

POST 请求  <http://127.0.0.1:8080/extend> ，请求体：

```json
{
    "type":"teacher",
    "age":15
}
```

响应体：

```json
{
    "code": 500,
    "data": null,
    "success": false,
    "message": "age must be greater than or equal to 28"
}
```

### 4.2. 请求参数解析+参数校验

最近在把 Node.js 项目重构为 Java 项目，其中一个接口为 `GET https://host:port/uri?list=["aa","bb"]`，Node.js 的框架可以将 list 参数解析为数组类型；而在 Java 中，即使将 list 参数定义为集合或数组类型，Srpingmvc 框架默认还是会把 `["aa","bb"]` 整个解析为集合中的一个元素。参数未能以预期的方式接收，亦无法通过参数校验。
我们知道 Springmvc 可以将 `GET https://host:port/uri?list=aa,bb` 形式的参数解析为集合或数组，但 Node.js 的框架无法解析这种传参方式。因为是重构项目，需要兼容 Node.js ，故不能修改传参方式。而一时间没有找到可以让 Springmvc 正确解析参数的方法，因此决定实现自定义参数解析器。

#### 4.2.1. 自定义参数解析注解

用来标记需要解析的参数。

```java
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ArrayResolver {
}
```

#### 4.2.2. 自定义参数解析器

实现 `HandlerMethodArgumentResolver` 接口，自定义参数解析器

```java
public class ArrayHandlerMethodArgumentResolver extends AbstractCustomizeResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(ArrayResolver.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Object obj = BeanUtils.instantiateClass(parameter.getParameterType());
        BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(obj);
        Iterator<String> parameterNames = webRequest.getParameterNames();
        while (parameterNames.hasNext()) {
            // name 从 request 中获取，可能是下划线格式
            String name = parameterNames.next();
            // 下划线转驼峰
            String camelName = underLineToCamel(name);
            Class<?> propertyType = wrapper.getPropertyType(camelName);
            // 传参在对象中不存在时
            if (Objects.isNull(propertyType)) {
                continue;
            }
            // 属性值(从请求中获取)
            Object o = webRequest.getParameter(name);
            if (Objects.nonNull(o)) {
                // 数组
                if (propertyType.isArray()) {
                    wrapper.setPropertyValue(camelName, value2Array(o));
                } else if (Collection.class.isAssignableFrom(propertyType)) {// 集合
                    wrapper.setPropertyValue(camelName, array2Collection(propertyType, value2Array(o)));
                } else {//其他类型
                    wrapper.setPropertyValue(camelName, o);
                }
            }
        }
        // 参数校验
        valid(parameter, mavContainer, webRequest, binderFactory, obj);
        return obj;
    }

    private Object[] value2Array(Object o) {
        assert o != null;
        // ["a","b","c"] 格式
        if (StringUtils.containsAny(o.toString(), "[", "]")) {
            return JSON.parseArray(o.toString()).toArray();
        } else {
            // a,b,c 格式
            return JSON.parseArray(Arrays.toString(o.toString().split(","))).toArray();
        }
    }

    private Collection<Object> array2Collection(Class<?> propertyType, Object[] array) {
        Collection<Object> collection = CollectionFactory.createCollection(propertyType, array.length);
        Collections.addAll(collection, array);
        return collection;
    }
}
```

#### 4.2.3. 将参数解析器添加到 Spring 容器中

```java
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new ArrayHandlerMethodArgumentResolver());
    }
}
```

#### 4.2.4. 在模型 `Teacher` 中新增 `lessons` 属性

```java
@EqualsAndHashCode(callSuper = true)
@Data
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Teacher extends Person {

    /**
     * 工号
     */
    private Integer teaId;

    private String name;

    @Min(28)
    private Integer age;

    /**
     * 教授课程
     */
    @MatchAny(enumClass = LessonEnum.class)
    private List<String> lessons;
}
```

#### 4.2.5. 新增 Handler Method `arrayResolve`

参数前标记 `@ArrayResolver` 注解。

```java
@RestController
@RequestMapping("/extend")
public class ExtendController {

    @PostMapping
    public R extend(@RequestBody @Validated Person person) {
        return R.data(person);
    }

    @GetMapping("/arrayResolve")
    public R arrayResolve(@ArrayResolver @Validated Teacher teacher) {
        return R.data(teacher);
    }
}
```

GET 请求  [http://127.0.0.1:8080/extend/arrayResolve?lessons=\["math","xxx"\]](http://127.0.0.1:8080/extend/arrayResolve?lessons=%5B%22math%22%2C%22xxx%22%5D)&#x20;

响应体：

```json
{
    "code": 500,
    "data": null,
    "success": false,
    "message": "lessons must be one of [language,math,english]"
}
```

可以看到，参数被正确解析且被校验。

***

Tip：本文完整示例代码已上传至 [GitHub](https://github.com/Blogeee/blog-java/tree/main/_20211209)
