如何從 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. 更多註解,請各位自行嘗試;
  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 身份證校驗工具類

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 的測試結果,說明分組校驗成功。

四、總結

希望大家寫的每一行程式碼都是業務需要,而不是無聊且無窮無盡的參數校驗。

Github 示例程式碼

Tags: