Spring Boot 参数校验之 Validator

在开发中经常需要写一些字段校验的代码,比如非空,长度限制,邮箱格式验证等等,导致充满了if-else 的代码,不仅相当冗长,而且很让人抓狂。

hibernate validator官方文档)提供了一套比较完善、便捷的验证实现方式。它定义了很多常用的校验注解,我们可以直接将这些注解加在我们JavaBean的属性上面,就可以在需要校验的时候进行校验了。在Spring Boot 火热的现在,该工具已经包含在spring-boot-starter-web中,不需额外引入其他包。

一、快速入门

1.1 在UserDTO中声明要检查的参数

校验说明见代码中注释

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 UserDTO {

/**
性别(不校验)
*/
private String sex;

/**
用户名(校验:不能为空,不能超过20个字符串)
*/
@NotBlank(message = "用户名不能为空")
@Length(max = 20, message = "用户名不能超过20个字符")
private String userName;

/**
* 手机号(校验:不能为空且按照正则校验格式)
*/
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
private String mobile;

/**
邮箱(校验:不能唯恐且校验邮箱格式)
*/
@NotBlank(message = "联系邮箱不能为空")
@Email(message = "邮箱格式不对")
private String email;
}

1.2 接口处声明要检查的参数

需要在Controller层的入参位置用@Validated 注解声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/demo")
public class ValidatorDemoController {

/**
* 注解参数校验案例
* @param userDTO
* @return
*/
@PostMapping("/test")
public HttpResult test(@Validated UserDTO userDTO) {
return HttpResult.success(userDTO);
}
}

这里的HttpResultVan 自己封装的一个结果集,详见文末Github地址的源码。

1.3 Web全局异常捕获

@ValidSpring Boot中进行绑定参数校验时会抛出异常,需要在Spring Boot中处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestControllerAdvice
@Slf4j
public class WebExceptionHandler {


/**
* 方法参数校验
* @param e
* @return
*/
@ExceptionHandler(BindException.class)
public HttpResult handleMethodArgumentNotValidException(BindException e) {
log.error(e.getMessage(), e);
return HttpResult.failure(400,e.getBindingResult().getFieldError().getDefaultMessage());
}

@ExceptionHandler(Exception.class)
public HttpResult handleException(Exception e) {
log.error(e.getMessage(), e);
return HttpResult.failure(400, "系统繁忙,请稍后再试");
}
}

1.4 测试

测试工具采用的postman

1
2
3
userName:Van
mobile:17098705205
email:123
  • 返回结果:
1
2
3
4
5
6
{
"success": false,
"code": 400,
"data": null,
"message": "邮箱格式不对"
}
  • 说明
  1. 更多注解,请各位自行尝试;
  2. 测试结果证明:参数校验生效,且按照我们设定的结果集返回异常信息。

1.5 常见的校验注解

  1. @Null:被注释的元素必须为 null
  2. @NotNull:被注释的元素必须不为 null
  3. @AssertTrue:被注释的元素必须为 true
  4. @AssertFalse:被注释的元素必须为 false
  5. @Min(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  6. @Max(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  7. @DecimalMin(value):被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  8. @DecimalMax(value):被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  9. @Size(max=, min=):被注释的元素的大小必须在指定的范围内
  10. @Digits (integer, fraction):被注释的元素必须是一个数字,其值必须在可接受的范围内
  11. @Past:被注释的元素必须是一个过去的日期
    @Future:被注释的元素必须是一个将来的日期
  12. @Pattern(regex=,flag=):被注释的元素必须符合指定的正则表达式
  13. @NotBlank(message =):验证字符串非null,且长度必须大于0
  14. @Email:被注释的元素必须是电子邮箱地址
  15. @Length(min=,max=):被注释的字符串的大小必须在指定的范围内
  16. @NotEmpty:被注释的字符串的必须非空
  17. @Range(min=,max=,message=):被注释的元素必须在合适的范围内

二、自定义注解校验

hibernate validator 自带的注解可以搞定简单的参数校验,加上正则的写法,能解决绝大多数参数校验情况。但是,有些情况,比如:校验是否登录,就需要我们自定义注解校验了。为了方便测试,我这里以身份证校验为例完成自定义校验的过程。

2.1 身份证校验工具类

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
public class IdCardValidatorUtils {

protected String codeAndCity[][] = {{"11", "北京"}, {"12", "天津"},
{"13", "河北"}, {"14", "山西"}, {"15", "内蒙古"}, {"21", "辽宁"},
{"22", "吉林"}, {"23", "黑龙江"}, {"31", "上海"}, {"32", "江苏"},
{"33", "浙江"}, {"34", "安徽"}, {"35", "福建"}, {"36", "江西"},
{"37", "山东"}, {"41", "河南"}, {"42", "湖北"}, {"43", "湖南"},
{"44", "广东"}, {"45", "广西"}, {"46", "海南"}, {"50", "重庆"},
{"51", "四川"}, {"52", "贵州"}, {"53", "云南"}, {"54", "西藏"},
{"61", "陕西"}, {"62", "甘肃"}, {"63", "青海"}, {"64", "宁夏"},
{"65", "新疆"}, {"71", "台湾"}, {"81", "香港"}, {"82", "澳门"},
{"91", "国外"}};

private String cityCode[] = {"11", "12", "13", "14", "15", "21", "22",
"23", "31", "32", "33", "34", "35", "36", "37", "41", "42", "43",
"44", "45", "46", "50", "51", "52", "53", "54", "61", "62", "63",
"64", "65", "71", "81", "82", "91"};


// 每位加权因子
private static int power[] = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};

// 第18位校检码
private String verifyCode[] = {"1", "0", "X", "9", "8", "7", "6", "5",
"4", "3", "2"};

/**
* 验证所有的身份证的合法性
*
* @param idcard
* @return
*/
public static boolean isValidatedAllIdcard(String idcard) {
if (idcard.length() == 15) {
idcard = convertIdcarBy15bit(idcard);
}
return isValidate18Idcard(idcard);
}

/**
* 将15位的身份证转成18位身份证
*
* @param idcard
* @return
*/
public static String convertIdcarBy15bit(String idcard) {
String idcard17 = null;
// 非15位身份证
if (idcard.length() != 15) {
return null;
}

if (isDigital(idcard)) {
// 获取出生年月日
String birthday = idcard.substring(6, 12);
Date birthdate = null;
try {
birthdate = new SimpleDateFormat("yyMMdd").parse(birthday);
} catch (ParseException e) {
e.printStackTrace();
}
Calendar cday = Calendar.getInstance();
cday.setTime(birthdate);
String year = String.valueOf(cday.get(Calendar.YEAR));

idcard17 = idcard.substring(0, 6) + year + idcard.substring(8);

char c[] = idcard17.toCharArray();
String checkCode = "";

if (null != c) {
int bit[] = new int[idcard17.length()];

// 将字符数组转为整型数组
bit = converCharToInt(c);
int sum17 = 0;
sum17 = getPowerSum(bit);

// 获取和值与11取模得到余数进行校验码
checkCode = getCheckCodeBySum(sum17);
// 获取不到校验位
if (null == checkCode) {
return null;
}

// 将前17位与第18位校验码拼接
idcard17 += checkCode;
}
} else { // 身份证包含数字
return null;
}
return idcard17;
}

/**
* @param idCard
* @return
*/
public static boolean isValidate18Idcard(String idCard) {
// 非18位为假
if (idCard.length() != 18) {
return false;
}
// 获取前17位
String idcard17 = idCard.substring(0, 17);
// 获取第18位
String idcard18Code = idCard.substring(17, 18);
char c[] = null;
String checkCode = "";
// 是否都为数字
if (isDigital(idcard17)) {
c = idcard17.toCharArray();
} else {
return false;
}

if (null != c) {
int bit[] = new int[idcard17.length()];
bit = converCharToInt(c);
int sum17 = 0;
sum17 = getPowerSum(bit);

// 将和值与11取模得到余数进行校验码判断
checkCode = getCheckCodeBySum(sum17);
if (null == checkCode) {
return false;
}
// 将身份证的第18位与算出来的校码进行匹配,不相等就为假
if (!idcard18Code.equalsIgnoreCase(checkCode)) {
return false;
}
}
return true;
}

/**
* 18位身份证号码的基本数字和位数验校
*
* @param idCard
* @return
*/
public boolean is18Idcard(String idCard) {
return Pattern.matches("^[1-9]\\d{5}[1-9]\\d{3}((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d{3}([\\d|x|X]{1})$", idCard);
}

/**
* 数字验证
*
* @param str
* @return
*/
public static boolean isDigital(String str) {
return str == null || "".equals(str) ? false : str.matches("^[0-9]*$");
}

/**
* 将身份证的每位和对应位的加权因子相乘之后,再得到和值
*
* @param bit
* @return
*/
public static int getPowerSum(int[] bit) {

int sum = 0;

if (power.length != bit.length) {
return sum;
}

for (int i = 0; i < bit.length; i++) {
for (int j = 0; j < power.length; j++) {
if (i == j) {
sum = sum + bit[i] * power[j];
}
}
}
return sum;
}

/**
* 将和值与11取模得到余数进行校验码判断
*
* @param sum17
* @return 校验位
*/
public static String getCheckCodeBySum(int sum17) {
String checkCode = null;
switch (sum17 % 11) {
case 10:
checkCode = "2";
break;
case 9:
checkCode = "3";
break;
case 8:
checkCode = "4";
break;
case 7:
checkCode = "5";
break;
case 6:
checkCode = "6";
break;
case 5:
checkCode = "7";
break;
case 4:
checkCode = "8";
break;
case 3:
checkCode = "9";
break;
case 2:
checkCode = "x";
break;
case 1:
checkCode = "0";
break;
case 0:
checkCode = "1";
break;
}
return checkCode;
}

/**
* 将字符数组转为整型数组
*
* @param c
* @return
* @throws NumberFormatException
*/
public static int[] converCharToInt(char[] c) throws NumberFormatException {
int[] a = new int[c.length];
int k = 0;
for (char temp : c) {
a[k++] = Integer.parseInt(String.valueOf(temp));
}
return a;
}


public static void main(String[] args) {
String idCardForFalse = "350583199108290106";
String idCardForTrue = "350583197106150219";
if (IdCardValidatorUtils.isValidatedAllIdcard(idCardForTrue)) {
System.out.println("身份证校验正确");
} else {
System.out.println("身份证校验错误!");
}
}
}

2.2 自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
@Documented
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdentityCardNumberValidator.class)
public @interface IdentityCardNumber {

String message() default "身份证号码格式不正确";

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

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

仔细的你会发现,相对于一般的自定义注解,该注解:
@Constraint(validatedBy = IdentityCardNumberValidator.class),该注解的作用就是调用身份证校验的工具。

2.3 在UserDTO 需要校验的字段添加声明

1
2
3
4
5
/**
* 身份证号(校验:自定义注解校验)
*/
@IdentityCardNumber
private String idNumber;

2.4 控制层接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/custom")
public class ValidatorCustomController {

/**
* 自定义注解参数校验案例
* @param userDTO
* @return
*/
@PostMapping("/test")
public HttpResult test(@Validated UserDTO userDTO) {
return HttpResult.success(userDTO);
}

}

2.5 自定义注解的测试

1
2
3
4
userName:Van
mobile:17098705205
email:110@qq.com
idNumber:350583199108290106
  • 返回结果:
1
2
3
4
5
6
{
"success": false,
"code": 400,
"data": null,
"message": "身份证号码格式不正确"
}

三、分组校验

除了上述的校验外,可能还有这种需求:

在创建用户信息时,不需要校验userId;但在更新用户信息时,需要校验userId,而用户名,邮箱等两种情况都得校验。这种情况,就可以分组校验来解决了。

3.1 定义分组接口

  • Create.java
1
2
3
4
import javax.validation.groups.Default;

public interface Create extends Default {
}
  • Update.java
1
2
3
4
import javax.validation.groups.Default;

public interface Update extends Default {
}

3.2 在UserDTO 需要校验的字段添加声明

1
2
3
4
5
/**
* 用户id(只有在有Update分组中校验非空)
*/
@NotNull(message = "id 不能为空", groups = Update.class)
private Long userId;

3.3 控制层入参位置进行声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping("/groups")
public class ValidatorGroupsController {

/**
* 更新数据,需要传入userID
* @param userDTO
* @return
*/
@PostMapping("/update")
public HttpResult updateData(@Validated(Update.class)UserDTO userDTO) {
return HttpResult.success(userDTO);
}
/**
* 新增数据,不需要传入userID
* @param userDTO
* @return
*/
@PostMapping("/create")
public HttpResult createData(@Validated(Create.class)UserDTO userDTO) {
return HttpResult.success(userDTO);
}
}

3.4 分组校验的测试-新增测试

1
2
3
4
5
userName:Van
mobile:17098705205
email:110@qq.com
idNumber:350583197106150219
userId:
  • 返回结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"code": 200,
"data": {
"userId": null,
"sex": null,
"userName": "Van",
"mobile": "17098705205",
"email": "110@qq.com",
"idNumber": "350583197106150219",
"passWord": null
},
"message": null
}

请求成功,说明新增请求,不检验userId,即userId可以为空。

3.5 分组校验的测试-更新测试

1
2
3
4
5
6
{
"success": false,
"code": 400,
"data": null,
"message": "id 不能为空"
}

请求失败,说明更新请求,检验userId,即userId不能为空。

结合 3.4 与 3.5 的测试结果,说明分组校验成功。

四、总结

希望大家写的每一行代码都是业务需要,而不是无聊且无穷无尽的参数校验。

Github 示例代码

Van wechat
最新文章,欢迎您扫一扫上面的微信公众号!
-------------    本文结束  感谢您的阅读    -------------