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;
}
|
要完成动态校验就需要对应的实体实现DynamicValidator
。DynamicValidator
已经集成到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"
}
|
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": "昵称不能为空"
}
|
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"
}
|
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": "身份信息不合法"
}
|
- 用户类型为
Wechat
,age >= 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"
}
|
- 用户类型为
Wechat
,age >= 18
,仅提供了idCard
的id
数据时
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"
}
|
- 用户类型为
Wechat
,age >= 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
可以极大的简化针对动态校验的场景,也方便了在不规模修改的情况下,对静态和动态校验进行了整合。
这里是完成的代码示例。