如何逃離令人抓狂的 if-else 參數校驗的代碼,Van 帶你用validator快速搞定,節省更多的時間勾搭小姐姐。 ...
背景
在開發中經常需要寫一些欄位校驗的代碼,比如非空,長度限制,郵箱格式驗證等等,導致充滿了
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:110@qq.com
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:110@qq.com
idNumber:350583197106150219
userId:
- 返回結果:
{
"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 分組校驗的測試-更新測試
- 請求方式:POST
- 請求地址:localhost:8080/groups/update
- 請求參數:同上(3.4)
- 返回結果:
{
"success": false,
"code": 400,
"data": null,
"message": "id 不能為空"
}
請求失敗,說明更新請求,檢驗userId
,即userId
不能為空。
結合 3.4 與 3.5 的測試結果,說明分組校驗成功。
四、總結
希望大家寫的每一行代碼都是業務需要,而不是無聊且無窮無盡的參數校驗。