基于注解的通用数据验证

前言

在项目开发交互过程中,难免会遇到一些数据校验。以校验客户端发送数据的合法性,对于一些非空校验,我们也许可以使用@NonNull,@NotNull 等注解,可是对于一些常规的,如手机号,身份证等等的校验,我们就还要判断处理每个请求的参数的合法性。

但是合法性的判断是难以避免的,我们是否可以精简工作量、提高工作效率呢。

思考

我们或许应该从@NonNull @NotNull等其他注解那里受到些启发。

我们或许可以结合正则表达式及注解对某些通用数据进行验证。

注解可以设置参数,我们可以设置参数为校验规则,通过枚举列举出来,同时也应该允许用户自定义正则等校验。

我们知道,注解有三种类型

RetentionPolicy.SOURCE

RetentionPolicy.CLASS

RetentionPolicy.RUNTIME。

SOURCE主要用于编译之前,编译过程中会被丢弃如@Override注解。

CLASS主要用于编译,运行时会被丢弃。

RUNTIME在源码,编译,运行时始终会存在。

可以利用反射,拿到具有特定注解的bean,并处理。所以我们定义的注解应该是RUNTIME类型。同时声明注作用范围为FIELD及PARAMETER。

实践

定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 数据验证注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.PARAMETER})
public @interface DataValid {

//是否可以为空
boolean nullable() default false;

//提供几种常用的正则验证
RegexType regexType() default RegexType.NONE;

//自定义正则验证
String regexExpression() default "";

//参数或者字段描述
String description() default "";

}

定义如上注解,nullable用来校验参数是否可空,默认不可以为空,false。

同时提供几种通用的正则校验,用枚举列出,如手机号码校验,身份证信息校验等等。

同时如果没有规定的正则表达式,可以让用户自定义自己的正则表达式。

另增加描述字段,用来说明这个paramer的用途。

定义常用正则枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 正则类型枚举
*/
public enum RegexType {

NONE,
SPECIALCHAR,
CHINESE,
EMAIL,
IP,
NUMBER,
NUMBERORNIL,
PHONENUMBER,
ID;

}

列出几种常用枚举。非空,特殊字符,中文,邮箱,IP,数字等等

枚举规则

定义了枚举,要定义它们的具体对应的方法,以便后续调用。

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
/**
* 常用正则表达式
*/
public class RegexUtils {

/**
* 判断是否是正确的IP地址
*
* @param ip
* @return boolean true,通过,false,没通过
*/
public static boolean isIp(String ip) {
if (null == ip || "".equals(ip))
return false;
String regex = "^(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|[1-9])\\."
+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)\\."
+ "(1\\d{2}|2[0-4]\\d|25[0-5]|[1-9]\\d|\\d)$";
return ip.matches(regex);
}

/**
* 判断是否是正确的邮箱地址
*
* @param email
* @return boolean true,通过,false,没通过
*/
public static boolean isEmail(String email) {
if (null == email || "".equals(email))
return false;
String regex = "\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*";
return email.matches(regex);
}

/**
* 判断是否含有中文,仅适合中国汉字,不包括标点
* @param text
* @return boolean true,通过,false,没通过
*/
public static boolean isChinese(String text) {
if (null == text || "".equals(text))
return false;
Pattern p = Pattern.compile("[\u4e00-\u9fa5]");
Matcher m = p.matcher(text);
return m.find();
}

/**
* 判断是否正整数
*
* @param number
* 数字
* @return boolean true,通过,false,没通过
*/
public static boolean isNumber(String number) {
if (null == number || "".equals(number))
return false;
String regex = "[0-9]*";
return number.matches(regex);
}

/**
* 判断是否正整数(可以为空)
*
* @param number
* 数字
* @return boolean true,通过,false,没通过
*/
public static boolean isNumberOrNil(String number) {
if(null == number) return true;
if ("".equals(number.trim())) return true;
String regex = "[0-9]*";
return number.matches(regex);
}

/**
* 判断几位小数(正数)
*
* @param decimal
* 数字
* @param count
* 小数位数
* @return boolean true,通过,false,没通过
*/
public static boolean isDecimal(String decimal, int count) {
if (null == decimal || "".equals(decimal))
return false;
String regex = "^(-)?(([1-9]{1}\\d*)|([0]{1}))(\\.(\\d){" + count
+ "})?$";
return decimal.matches(regex);
}

/**
* 判断是否是手机号码
*
* @param phoneNumber
* 手机号码
* @return boolean true,通过,false,没通过
*/
public static boolean isPhoneNumber(String phoneNumber) {
if (null == phoneNumber || "".equals(phoneNumber))
return false;
String regex = "^1[3|4|5|6|7|8|9][0-9]\\d{8}$";
return phoneNumber.matches(regex);
}

/**
* 判断身份证号格式正确性
*
* @param ID
* 身份证号
* @return boolean true,通过,false,没通过
*/
public static boolean isID(String ID) {
if (null == ID || "".equals(ID))
return false;
String regex = "^(\\d{14}[0-9a-zA-Z])|(\\d{17}[0-9a-zA-Z])$";
return ID.matches(regex);
}

/**
* 判断是否含有特殊字符
*
* @param text
* @return boolean true,通过,false,没通过
*/
public static boolean hasSpecialChar(String text) {
if (null == text || "".equals(text))
return false;
if (text.replaceAll("[a-z]*[A-Z]*\\d*-*_*\\s*", "").length() == 0) {
// 如果不包含特殊字符
return true;
}
return false;
}

/**
* 适应CJK(中日韩)字符集,部分中日韩的字是一样的
*/
public static boolean isChinese2(String strName) {
char[] ch = strName.toCharArray();
for (int i = 0; i < ch.length; i++) {
char c = ch[i];
if (isChinese(c)) {
return true;
}
}
return false;
}

private static boolean isChinese(char c) {
Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|| ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
|| ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
|| ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B
|| ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
|| ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS
|| ub == Character.UnicodeBlock.GENERAL_PUNCTUATION) {
return true;
}
return false;
}
}

实现及调用

基本数据都定义及处理好了,我们应该建立注解与方法之间的关联,RUNTIME类型的注解在程序运行时也会被保留,我们可以利用反射,拿到具体注解参数信息,进行相关处理。

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
/**
* 注解解析Service
*/
public class ValidateService {

/*private static DataValid dataValid;*/

public ValidateService() {
super();
}

/**
* 解析入口
* @param object
* @throws Exception
*/
public static void valid(Object object) throws Exception{
//获取object的类型
Class<? extends Object> clazz=object.getClass();
//获取该类型声明的成员
Field[] fields=clazz.getDeclaredFields();
//遍历属性
for(Field field:fields){
//对于private私有化的成员变量,通过setAccessible来修改器访问权限
field.setAccessible(true);
validate(field,object);
//重新设置会私有权限
field.setAccessible(false);
}
}


public static void validate(Field field,Object object) throws Exception{

String description = null;
Object value = null;
DataValid dataValid = null;

//获取对象的成员的注解信息
dataValid=field.getAnnotation(DataValid.class);
value=field.get(object);

if(dataValid==null)return;

description=dataValid.description().equals("")?field.getName():dataValid.description();

/*************注解解析工作开始******************/
if(!dataValid.nullable() && dataValid.regexType() != RegexType.NUMBERORNIL){
if(value==null|| StringUtils.isBlank(value.toString())){
throw new Exception(description+"不能为空");
}
}

if(dataValid.regexType()!=RegexType.NONE){
switch (dataValid.regexType()) {
case NONE:
break;
case SPECIALCHAR:
if(RegexUtils.hasSpecialChar(value.toString())){
throw new Exception(description+"不能含有特殊字符");
}
break;
case CHINESE:
if(RegexUtils.isChinese2(value.toString())){
throw new Exception(description+"不能含有中文字符");
}
break;
case EMAIL:
if(!RegexUtils.isEmail(value.toString())){
throw new Exception(description+"邮箱地址格式不正确");
}
break;
case IP:
if(!RegexUtils.isIp(value.toString())){
throw new Exception(description+"IP地址格式不正确");
}
break;
case NUMBER:
if(!RegexUtils.isNumber(value.toString())){
throw new Exception(description+"不是数字");
}
break;
case NUMBERORNIL:
if(value == null){
break;
}
if(!RegexUtils.isNumberOrNil(value.toString())){
throw new Exception(description+"格式不正确");
}
break;
case PHONENUMBER:
if(!RegexUtils.isPhoneNumber(value.toString())){
throw new Exception("手机号格式不正确");
}
break;
case ID:
if(!RegexUtils.isID(value.toString())){
throw new Exception("身份证号格式不正确");
}
break;
default:
break;
}
}

if(!dataValid.regexExpression().equals("")){
if(value.toString().matches(dataValid.regexExpression())){
throw new Exception(description+"格式不正确");
}
}
/*************注解解析工作结束******************/
}
}

如上代码。

当然,到具体业务层,应该调用这个Service的valid方法去校验参数。

结论

可以看到,经过这样,我们可以把一些常用的校验通过这种方式封装,大大简化代码量,使业务层更注重业务。

这种也可以添加自己的通用类型,灵活性很强。

这个小小的简单工具最主要的就是利用了Java的反射机制。

以上。

今天就到这里啦,中秋节快乐~~




-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

您的支持就是我创作的动力!

欢迎关注我的其它发布渠道