如何從 if-else 的參數校驗中解放出來?
- 2019 年 10 月 3 日
- 筆記
背景
在開發中經常需要寫一些欄位校驗的程式碼,比如非空,長度限制,郵箱格式驗證等等,導致充滿了
if-else的程式碼,不僅相當冗長,而且很讓人抓狂。
hibernate validator(官方文檔)提供了一套比較完善、便捷的驗證實現方式。它定義了很多常用的校驗註解,我們可以直接將這些註解加在我們JavaBean的屬性上面,就可以在需要校驗的時候進行校驗了。在Spring Boot 火熱的現在,該工具已經包含在spring-boot-starter-web中,不需額外引入其他包。
一、快速入門
1.1 在UserDTO中聲明要檢查的參數
校驗說明見程式碼中注釋
@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註解聲明
@RestController @RequestMapping("/demo") public class ValidatorDemoController { /** * 註解參數校驗案例 * @param userDTO * @return */ @PostMapping("/test") public HttpResult test(@Validated UserDTO userDTO) { return HttpResult.success(userDTO); } } 這裡的
HttpResult是Van 自己封裝的一個結果集,詳見文末Github地址的源碼。
1.3 Web全局異常捕獲
@Valid 在 Spring Boot中進行綁定參數校驗時會拋出異常,需要在Spring Boot中處理。
@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
- 請求方式:POST
- 請求地址:localhost:8080/demo/test
- 請求參數:
userName:Van mobile:17098705205 email:123 - 返回結果:
{ "success": false, "code": 400, "data": null, "message": "郵箱格式不對" } - 說明
- 更多註解,請各位自行嘗試;
- 測試結果證明:參數校驗生效,且按照我們設定的結果集返回異常資訊。
1.5 常見的校驗註解
@Null:被注釋的元素必須為null
@NotNull:被注釋的元素必須不為null
@AssertTrue:被注釋的元素必須為true
@AssertFalse:被注釋的元素必須為false
@Min(value):被注釋的元素必須是一個數字,其值必須大於等於指定的最小值@Max(value):被注釋的元素必須是一個數字,其值必須小於等於指定的最大值@DecimalMin(value):被注釋的元素必須是一個數字,其值必須大於等於指定的最小值@DecimalMax(value):被注釋的元素必須是一個數字,其值必須小於等於指定的最大值@Size(max=, min=):被注釋的元素的大小必須在指定的範圍內
@Digits (integer, fraction):被注釋的元素必須是一個數字,其值必須在可接受的範圍內@Past:被注釋的元素必須是一個過去的日期
@Future:被注釋的元素必須是一個將來的日期
@Pattern(regex=,flag=):被注釋的元素必須符合指定的正則表達式@NotBlank(message =):驗證字元串非null,且長度必須大於0
@Email:被注釋的元素必須是電子郵箱地址
@Length(min=,max=):被注釋的字元串的大小必須在指定的範圍內
@NotEmpty:被注釋的字元串的必須非空
@Range(min=,max=,message=):被注釋的元素必須在合適的範圍內
二、自定義註解校驗
hibernate validator 自帶的註解可以搞定簡單的參數校驗,加上正則的寫法,能解決絕大多數參數校驗情況。但是,有些情況,比如:校驗是否登錄,就需要我們自定義註解校驗了。為了方便測試,我這裡以身份證校驗為例完成自定義校驗的過程。
2.1 身份證校驗工具類
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 自定義註解
@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 需要校驗的欄位添加聲明
/** * 身份證號(校驗:自定義註解校驗) */ @IdentityCardNumber private String idNumber; 2.4 控制層介面
@RestController @RequestMapping("/custom") public class ValidatorCustomController { /** * 自定義註解參數校驗案例 * @param userDTO * @return */ @PostMapping("/test") public HttpResult test(@Validated UserDTO userDTO) { return HttpResult.success(userDTO); } } 2.5 自定義註解的測試
- 請求方式:POST
- 請求地址:localhost:8080/private/test
- 請求參數:
userName:Van mobile:17098705205 email:[email protected] idNumber:350583199108290106 - 返回結果:
{ "success": false, "code": 400, "data": null, "message": "身份證號碼格式不正確" } 三、分組校驗
除了上述的校驗外,可能還有這種需求:
在創建用戶資訊時,不需要校驗userId;但在更新用戶資訊時,需要校驗userId,而用戶名,郵箱等兩種情況都得校驗。這種情況,就可以分組校驗來解決了。
3.1 定義分組介面
Create.java
import javax.validation.groups.Default; public interface Create extends Default { } Update.java
import javax.validation.groups.Default; public interface Update extends Default { } 3.2 在UserDTO 需要校驗的欄位添加聲明
/** * 用戶id(只有在有Update分組中校驗非空) */ @NotNull(message = "id 不能為空", groups = Update.class) private Long userId; 3.3 控制層入參位置進行聲明
@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 分組校驗的測試-新增測試
- 請求方式:POST
- 請求地址:localhost:8080/groups/create
- 請求參數:
userName:Van mobile:17098705205 email:[email protected] idNumber:350583197106150219 userId: - 返回結果:
{ "success": true, "code": 200, "data": { "userId": null, "sex": null, "userName": "Van", "mobile": "17098705205", "email": "[email protected]", "idNumber": "350583197106150219", "passWord": null }, "message": null } 請求成功,說明新增請求,不檢驗userId,即userId可以為空。
3.5 分組校驗的測試-更新測試
- 請求方式:POST
- 請求地址:localhost:8080/groups/update
- 請求參數:同上(3.4)
- 返回結果:
{ "success": false, "code": 400, "data": null, "message": "id 不能為空" } 請求失敗,說明更新請求,檢驗userId,即userId不能為空。
結合 3.4 與 3.5 的測試結果,說明分組校驗成功。
四、總結
希望大家寫的每一行程式碼都是業務需要,而不是無聊且無窮無盡的參數校驗。