基於註解的通用資料驗證
前言
在專案開發互動過程中,難免會遇到一些資料校驗。以校驗客戶端傳送資料的合法性,對於一些非空校驗,我們也許可以使用@NonNull,@NotNull 等註解,可是對於一些常規的,如手機號,身份證等等的校驗,我們就還要判斷處理每個請求的引數的合法性。
但是合法性的判斷是難以避免的,我們是否可以精簡工作量、提高工作效率呢。
思考
我們或許應該從@NonNull @NotNull等其他註解那裡受到些啟發。
我們或許可以結合正則表示式及註解對某些通用資料進行驗證。
註解可以設定引數,我們可以設定引數為校驗規則,通過列舉列舉出來,同時也應該允許使用者自定義正則等校驗。
我們知道,註解有三種類型
RetentionPolicy.SOURCE
RetentionPolicy.CLASS
RetentionPolicy.RUNTIME。
SOURCE主要用於編譯之前,編譯過程中會被丟棄如@Override註解。
CLASS主要用於編譯,執行時會被丟棄。
RUNTIME在原始碼,編譯,執行時始終會存在。
可以利用反射,拿到具有特定註解的bean,並處理。所以我們定義的註解應該是RUNTIME型別。同時宣告注作用範圍為FIELD及PARAMETER。
實踐
定義註解
/**
* 資料驗證註解
*/
@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的用途。
定義常用正則列舉
/**
* 正則型別列舉
*/
public enum RegexType {
NONE,
SPECIALCHAR,
CHINESE,
EMAIL,
IP,
NUMBER,
NUMBERORNIL,
PHONENUMBER,
ID;
}
列出幾種常用列舉。非空,特殊字元,中文,郵箱,IP,數字等等
列舉規則
定義了列舉,要定義它們的具體對應的方法,以便後續呼叫。
/**
* 常用正則表示式
*/
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型別的註解在程式執行時也會被保留,我們可以利用反射,拿到具體註解引數資訊,進行相關處理。
/**
* 註解解析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的反射機制。
以上。
今天就到這裡啦,中秋節快樂~~