如何從 if-else 的參數校驗中解放出來?

来源:https://www.cnblogs.com/vandusty/archive/2019/09/11/11509569.html
-Advertisement-
Play Games

如何逃離令人抓狂的 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全局異常捕獲

@ValidSpring 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 示例代碼


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 環境: 1、ionic 2、angular-cli 開發 1、CTRL C + CTRL V 2、圖片路徑的問題 使用‘assets/xxxxx.jpg’,而不使用‘../../assets/xxxxx.jpg’,因為打包後的目錄如下: 伺服器上圖片會報404錯誤。 3、打包問題 打包命令: ion ...
  • HTML5現在已經不是SGML的子集,主要是關於圖像,位置,存儲,多任務等功能的增加。 繪畫canvas; 用於媒介回放的video和audio元素; 本地離線存儲localStorage長期存儲數據,瀏覽器關閉後數據不丟失; sessionStorage的數據在瀏覽器關閉後自動刪除; 語義化更好的 ...
  • jQuery(五): Deferred 有啥用 通常來說,js請求數據,無論是非同步還是同步,都不會立即獲取到結果,通常而言,我們一般是是使用回調函數再執行,而 deferred就是解決jQuery的回調函數方案,總的來說,deferred對象就是為了將某個回調函數延遲到某個時機再執行. 1. aja ...
  • Node.js是一個Javascript運行環境(runtime environment),發佈於2009年5月,由Ryan Dahl開發,實質是對Chrome V8引擎進行了封裝。本文詳細介紹了Node.js的安裝和使用。 一、Node.js介紹 Node.js 不是一個 JavaScript 框 ...
  • 面向微服務的體繫結構如今風靡全球。這是因為更快的部署節奏和更低的成本是面向微服務的體繫結構的基本承諾。 然而,對於大多數試水的公司來說,開發活動更多的是將現有的單塊應用程式轉換為面向微服務的體繫結構,這可能是許多層面上阻礙和衝突的根源。 雖然 "Greenfield" (未開發的)面向微服務的體繫結 ...
  • 一、JVM包含三個記憶體區:棧記憶體、堆記憶體、方法區記憶體 二、註意點 (1)在MyEclipse中字體是紅色的是一個類的名字,並且這個類除了我們自定義的類是JavaSE類庫中自帶的 (2)其實JavaSE類庫中自帶的類,例如:String.class\System.class,這些類的類名也是標識符 ( ...
  • 一個可以沉迷於技術的程式猿,wx加入加入技術群:fsx641385712 ...
  • 作為開發人員,大家都知道,SpringBoot是基於Spring4.0設計的,不僅繼承了Spring框架原有的優秀特性,而且還通過簡化配置來進一步簡化了Spring應用的整個搭建和開發過程。另外SpringBoot通過集成大量的框架使得依賴包的版本衝突,以及引用的不穩定性等問題得到了很好的解決。 S ...
一周排行
    -Advertisement-
    Play Games
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...
  • 目錄前言PostgreSql安裝測試額外Nuget安裝Person.cs模擬運行Navicate連postgresql解決方案Garnet為什麼要選擇Garnet而不是RedisRedis不再開源Windows版的Redis是由微軟維護的Windows Redis版本老舊,後續可能不再更新Garne ...
  • C#TMS系統代碼-聯表報表學習 領導被裁了之後很快就有人上任了,幾乎是無縫銜接,很難讓我不想到這早就決定好了。我的職責沒有任何變化。感受下來這個系統封裝程度很高,我只要會調用方法就行。這個系統交付之後不會有太多問題,更多應該是做小需求,有大的開發任務應該也是第二期的事,嗯?怎麼感覺我變成運維了?而 ...
  • 我在隨筆《EAV模型(實體-屬性-值)的設計和低代碼的處理方案(1)》中介紹了一些基本的EAV模型設計知識和基於Winform場景下低代碼(或者說無代碼)的一些實現思路,在本篇隨筆中,我們來分析一下這種針對通用業務,且只需定義就能構建業務模塊存儲和界面的解決方案,其中的數據查詢處理的操作。 ...
  • 對某個遠程伺服器啟用和設置NTP服務(Windows系統) 打開註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer 將 Enabled 的值設置為 1,這將啟用NTP伺服器功 ...
  • title: Django信號與擴展:深入理解與實踐 date: 2024/5/15 22:40:52 updated: 2024/5/15 22:40:52 categories: 後端開發 tags: Django 信號 松耦合 觀察者 擴展 安全 性能 第一部分:Django信號基礎 Djan ...
  • 使用xadmin2遇到的問題&解決 環境配置: 使用的模塊版本: 關聯的包 Django 3.2.15 mysqlclient 2.2.4 xadmin 2.0.1 django-crispy-forms >= 1.6.0 django-import-export >= 0.5.1 django-r ...
  • 今天我打算整點兒不一樣的內容,通過之前學習的TransformerMap和LazyMap鏈,想搞點不一樣的,所以我關註了另外一條鏈DefaultedMap鏈,主要調用鏈為: 調用鏈詳細描述: ObjectInputStream.readObject() DefaultedMap.readObject ...
  • 後端應用級開發者該如何擁抱 AI GC?就是在這樣的一個大的浪潮下,我們的傳統的應用級開發者。我們該如何選擇職業或者是如何去快速轉型,跟上這樣的一個行業的一個浪潮? 0 AI金字塔模型 越往上它的整個難度就是職業機會也好,或者說是整個的這個運作也好,它的難度會越大,然後越往下機會就會越多,所以這是一 ...
  • @Autowired是Spring框架提供的註解,@Resource是Java EE 5規範提供的註解。 @Autowired預設按照類型自動裝配,而@Resource預設按照名稱自動裝配。 @Autowired支持@Qualifier註解來指定裝配哪一個具有相同類型的bean,而@Resourc... ...