Contents

Spring Boot Bean动态校验

Contents

springboot中的bean校验使用的是hibernate-validator,相对来说,这种通过注解的校验,对业务代码的“污染”会很少很多很多了。但是,这种方式的局限也很明显:

使用注解的方式,可以说是一种明确的、静态的校验方式,因为“校验”都通过注解呈现了,也就是已经固化了。当需要根据接口的入参去校验关联的属性时,就无法完成了。

如果要实现这种“根据接口的入参去校验关联的属性”就不得不进行根据入参的动态校验,同时还不能影响原有的校验机制,就是静态校验和动态校验的并存,且不能对业务有过多的“污染”。这也是此文的由来了,接下来会讲解一种静态、动态校验可并存的机制。

举例:校验用户的信息,如果用户是微信用户,需要校验微信用户相关的信息,如果用户18岁以上,还需要校验身份证信息;如果是微博用户,需要校验微博相关的信息。

这个场景中,使用静态的方式就不能完成所有场景的校验了。那么传统的做法,可能需要根据org.openingo.account.User#userType的具体取值去判断,比如是org.openingo.account.UserTypeEnum#WECHAT时去校验org.openingo.account.User#weChatAccount等等,如果对应的数据不合法,则抛出对应的异常。这其实就是动态校验的核心逻辑了,下面看具体的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Getter
@AllArgsConstructor
public enum UserTypeEnum {

    WECHAT(1),
    WEIBO(2),
    ;

    private Integer code;

    @JsonCreator
    public static UserTypeEnum newByCode(Integer code) {
        return Stream.of(values()).filter(e -> code.equals(e.getCode())).findFirst().orElse(null);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Data
public class User implements Serializable {

    @NotNull(message = "用户类型不可为null")
    private UserTypeEnum userType;

    private WeChatAccount weChatAccount;

    private WeiboAccount weiboAccount;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
public class WeChatAccount implements Serializable {

    @NotBlank(message = "账号id不能为空")
    private String accountId;

    @NotBlank(message = "昵称不能为空")
    private String nickname;

    @NotNull(message = "性别不能为空")
    @Pattern(regexp = "^man$|^woman$", message = "取值不合法")
    private String sex;
    
    @NotNull(message = "年龄不能为空")
    private Integer age;

    @NotBlank(message = "头像不能为空")
    private String avatarUri;

    private IdCard idCard;
}
1
2
3
4
5
6
7
8
9
@Data
public class IdCard implements Serializable {

    @NotBlank(message = "id不能为null")
    private String id;

    @NotBlank(message = "address不能为null")
    private String address;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Data
public class WeiboAccount implements Serializable {

    @NotBlank(message = "账号id不能为空")
    private String accountId;

    @NotBlank(message = "昵称不能为空")
    private String nickname;

    @NotNull(message = "性别不能为空")
    @Pattern(regexp = "^man$|^woman$", message = "取值不合法")
    private String sex;
    
    @NotNull(message = "年龄不能为空")
    @Min(value = 18, message = "只能18岁以上用户可以使用")
    private Integer age;

    @NotBlank(message = "头像不能为空")
    private String avatarUri;

    @NotBlank(message = "签名不能为空")
    private String signature;
}

要完成动态校验就需要对应的实体实现DynamicValidatorDynamicValidator已经集成到spring-boot-x2.9.5及以上版本

DynamicValidator是建立在动态校验参数的基础之上完成的,把具体的细节都封装了,简化了使用。

目前的DynamicValidator支持

  • ABC——链式校验;
  • ACD——跳跃式模式的校验。

可以从这里看到具体的DynamicValidator实现,其提供了:

  • org.openingo.spring.validator.DynamicValidator#silentDynamicValidate静默校验,适合在service层使用,如果有不合法的数据,会返回错误的message
  • org.openingo.spring.validator.DynamicValidator#validateField针对某个具体的字段的校验—— throwValidationException
  • org.openingo.spring.validator.DynamicValidator#validateSelf(T, java.lang.Class<?>…)对实体自身的校验
  • org.openingo.spring.validator.DynamicValidator#dynamicValidate(T, java.lang.String, java.lang.Class<?>...)指定数据不合法返回错误的message及校验组groups
  • org.openingo.spring.validator.DynamicValidator#addConstraintViolation自定义校验时,定义消息模板

针对上面的场景,应该如何实现呢 ? 根据描述的场景适合ACD跳跃校验:User就是入口,org.openingo.account.WeChatAccount#age就是跳跃点。从入口着手,看看具体实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
public class User implements Serializable, DynamicValidator {

    @NotNull(message = "用户类型不可为null")
    private UserTypeEnum userType;

    private WeChatAccount weChatAccount;

    private WeiboAccount weiboAccount;

    @Override
    public void dynamicValidate() {
        // 根据用户的类型确定要校验的数据
        if (UserTypeEnum.WECHAT.equals(this.userType)) {
            // 直接调用dynamicValidate校验对应的字段,
            // 会进行非null及字段的dynamicValidate的进一步校验
            this.dynamicValidate(this.weChatAccount, "微信数据不合法");
        } else {
            this.dynamicValidate(this.weiboAccount, "微博数据不合法");
        }
    }
}

进一步校验就会到具体的数据中进行了,那么看看WeChatAccount中的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Data
public class WeChatAccount implements Serializable, DynamicValidator {

    @NotBlank(message = "账号id不能为空")
    private String accountId;

    @NotBlank(message = "昵称不能为空")
    private String nickname;

    @NotNull(message = "性别不能为空")
    @Pattern(regexp = "^man$|^woman$", message = "取值不合法")
    private String sex;

    @NotNull(message = "年龄不能为空")
    private Integer age;

    @NotBlank(message = "头像不能为空")
    private String avatarUri;

    private IdCard idCard;

    @Override
    public void dynamicValidate() {
        // 如果年龄大于18岁,需要校验身份信息是否合法
        if (this.age >= 18) {
            // 同样的dynamicValidate将对idCard进行非null及进一步的校验
            this.dynamicValidate(this.idCard, "身份信息不合法");
            // IdCard如未实现DynamicValidator可以按照如下方式进行校验
            // this.validateField(null == this.idCard, "身份信息不合法");
            // this.validateSelf(this.idCard);
        }
    }
}

如果到了最后一级校验,如此处的IdCard,其可不实现DynamicValidator

再来看看,实现DynamicValidator后的IdCard

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Data
public class IdCard implements Serializable, DynamicValidator {

    @NotBlank(message = "id不能为null")
    private String id;

    @NotBlank(message = "address不能为null")
    private String address;

    @Override
    public void dynamicValidate() {
        // 啥也不用写
    }
}

因为到了最后一级,这也是为什么说,到最后一级可不实现DynamicValidator的原因。

最后看看,WeiboAccount的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Data
public class WeiboAccount implements Serializable, DynamicValidator {

    @NotBlank(message = "账号id不能为空")
    private String accountId;

    @NotBlank(message = "昵称不能为空")
    private String nickname;

    @NotNull(message = "性别不能为空")
    @Pattern(regexp = "^man$|^woman$", message = "取值不合法")
    private String sex;

    @NotNull(message = "年龄不能为空")
    @Min(value = 18, message = "只能18岁以上用户可以使用")
    private Integer age;

    @NotBlank(message = "头像不能为空")
    private String avatarUri;

    @NotBlank(message = "签名不能为空")
    private String signature;

    @Override
    public void dynamicValidate() {
        // 在这个场景中,WeiboAccount其实与IdCard相当,都是最后一级校验,
        // 所以可以不必实现DynamicValidator
    }
}

在这个场景中,WeiboAccount其实与IdCard相当,都是最后一级校验,所以可以不必实现DynamicValidator。

如何使用?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@RestController
@Validated
public class UserController {

    @Autowired
    IUserService userService;

    @PostMapping("/user/sync")
    public RespData syncUser(@RequestBody @Validated User user) {
        user.dynamicValidate();
        this.userService.syncUser(user);
        return RespData.success();
    }
}

可以看到与通常的使用几乎没有差异,只需在实际业务处理前,“手动”再次触发“dynamicValidate”即可。看看具体的效果:

  • 未提供为何数据时
1
2
3
4
{
    "sc": "400",
    "sm": "用户类型不可为null"
}
  • 用户类型为Wechat
1
2
3
4
{
    "sc": "500",
    "sm": "微信数据不合法"
}
  • 用户类型为Wechat,提供了wechatAccount数据为空对象时
1
2
3
4
5
6
{
    "userType": 1,
    "weChatAccount":{
        
    }
}
1
2
3
4
{
    "sc": "500",
    "sm": "昵称不能为空"
}
  • 用户类型为Wechatage < 18
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "userType": 1,
    "weChatAccount": {
        "nickname": "qicz",
        "avatarUri": "http://...",
        "accountId": "123",
        "age": 12,
        "sex": "man"
    }
}
1
2
3
4
{
    "sc": "success",
    "sm": "response successful"
}
  • 用户类型为Wechatage >= 18
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "userType": 1,
    "weChatAccount": {
        "nickname": "qicz",
        "avatarUri": "http://...",
        "accountId": "123",
        "age": 19,
        "sex": "man"
    }
}
1
2
3
4
{
    "sc": "500",
    "sm": "身份信息不合法"
}
  • 用户类型为Wechatage >= 18,提供了idCard数据为空对象时
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    "userType": 1,
    "weChatAccount": {
        "nickname": "qicz",
        "avatarUri": "http://...",
        "accountId": "123",
        "age": 19,
        "sex": "man",
        "idCard": {}
    }
}
1
2
3
4
{
    "sc": "500",
    "sm": "id不能为null"
}
  • 用户类型为Wechatage >= 18,仅提供了idCardid 数据时
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "userType": 1,
    "weChatAccount": {
        "nickname": "qicz",
        "avatarUri": "http://...",
        "accountId": "123",
        "age": 19,
        "sex": "man",
        "idCard": {
            "id": "idddd"
        }
    }
}
1
2
3
4
{
    "sc": "500",
    "sm": "address不能为null"
}
  • 用户类型为Wechatage >= 18,提供了完成的idCard数据时
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
    "userType": 1,
    "weChatAccount": {
        "nickname": "qicz",
        "avatarUri": "http://...",
        "accountId": "123",
        "age": 19,
        "sex": "man",
        "idCard": {
            "id": "idddd",
            "address": "bj"
        }
    }
}
1
2
3
4
{
    "sc": "success",
    "sm": "response successful"
}

针对微博数据的校验示例,这里就不一一罗列了。

至此,关于动态校验的介绍就over了!通过DynamicValidator可以极大的简化针对动态校验的场景,也方便了在不规模修改的情况下,对静态和动态校验进行了整合。

这里是完成的代码示例。