SpringBoot分組校驗(yàn)及自定義校驗(yàn)注解

image

前言

??在日常的開發(fā)中,參數(shù)校驗(yàn)是非常重要的一個(gè)環(huán)節(jié),嚴(yán)格參數(shù)校驗(yàn)會(huì)減少很多出bug的概率,增加接口的安全性俭缓。在此之前寫過一篇SpringBoot統(tǒng)一參數(shù)校驗(yàn)主要介紹了一些簡單的校驗(yàn)方法克伊。而這篇?jiǎng)t是介紹一些進(jìn)階的校驗(yàn)方式。比如說:在某個(gè)接口編寫的過程中肯定會(huì)遇到华坦,當(dāng)xxType值為A愿吹,paramA值必傳。xxType值為B惜姐,paramB值必須傳犁跪。對(duì)于這樣的,通常的做法就是在controller加上各種if判斷歹袁。顯然這樣的代碼是不夠優(yōu)雅的坷衍,而分組校驗(yàn)及自定義參數(shù)校驗(yàn),就是來解決這個(gè)問題的条舔。

PathVariable參數(shù)校驗(yàn)

??Restful的接口枫耳,在現(xiàn)在來講應(yīng)該是比較常見的了,常用的地址欄的參數(shù)孟抗,我們都是這樣校驗(yàn)的迁杨。

/**
 * 獲取電話號(hào)碼信息
 */
@GetMapping("/phoneInfo/{phone}")
public ResultVo phoneInfo(@PathVariable("phone") String phone){
    // 驗(yàn)證電話號(hào)碼是否有效
    String pattern = "^[1][3,4,5,7,8][0-9]{9}$";
    boolean isValid =  Pattern.matches(pattern, phone);
    if(isValid){
        // 執(zhí)行相應(yīng)邏輯
        return ResultVoUtil.success(phone);
    } else {
        // 返回錯(cuò)誤信息
        return ResultVoUtil.error("手機(jī)號(hào)碼無效");
    }
}

很顯然上面的代碼不夠優(yōu)雅,所以我們可以在參數(shù)后面凄硼,添加對(duì)應(yīng)的正則表達(dá)式phone:正則表達(dá)式來進(jìn)行驗(yàn)證铅协。這樣就省去了在controller編寫校驗(yàn)代碼了。

/**
 * 獲取電話號(hào)碼信息
 */
@GetMapping("/phoneInfo/{phone:^[1][3,4,5,7,8][0-9]{9}$}")
public ResultVo phoneInfo(@PathVariable("phone") String phone){
    return ResultVoUtil.success(phone);
}

雖然這樣處理后代碼更精簡了帆喇。但是如果傳入的手機(jī)號(hào)碼,不符合規(guī)則會(huì)直接返回404亿胸。而不是提示手機(jī)號(hào)碼錯(cuò)誤坯钦。錯(cuò)誤信息如下:

image

自定義校驗(yàn)注解

??我們以校驗(yàn)手機(jī)號(hào)碼為例,雖然validation提供了@Pattern這個(gè)注解來使用正則表達(dá)式進(jìn)行校驗(yàn)侈玄。如果被使用在多處婉刀,一旦正則表達(dá)式發(fā)生更改,則需要一個(gè)一個(gè)的進(jìn)行修改序仙。很顯然為了避免做這樣的無用功突颊,自定義校驗(yàn)注解就是你的好幫手。

@Data
public class PhoneForm {

    /**
     * 電話號(hào)碼
     */
    @Pattern(regexp = "^[1][3,4,5,7,8][0-9]{9}$" , message = "電話號(hào)碼有誤")
    private String phone;

}

??要實(shí)現(xiàn)一個(gè)自定義校驗(yàn)注解潘悼,主要是有兩步律秃。一是注解本身,二是校驗(yàn)邏輯實(shí)現(xiàn)類治唤。

PhoneVerify 校驗(yàn)注解

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
 
    String message() default "手機(jī)號(hào)碼格式有誤";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

PhoneValidator 校驗(yàn)實(shí)現(xiàn)類

public class PhoneValidator implements ConstraintValidator<Phone, Object> {

    @Override
    public boolean isValid(Object telephone, ConstraintValidatorContext constraintValidatorContext) {
        String pattern = "^1[3|4|5|7|8]\\d{9}$";
        return Pattern.matches(pattern, telephone.toString());
    }
}

CustomForm 表單數(shù)據(jù)

@Data
public class CustomForm {

    /**
     * 電話號(hào)碼
     */
    @Phone
    private String phone;

}

測(cè)試接口

@PostMapping("/customTest")
public ResultVo customTest(@RequestBody @Validated CustomForm form){
    return ResultVoUtil.success(form.getPhone());
}

注解的含義

@Target({ElementType.FIELD})

??注解是指定當(dāng)前自定義注解可以使用在哪些地方棒动,這里僅僅讓他可以使用屬性上。但還可以使用在更多的地方宾添,比如說方法上船惨、構(gòu)造器上等等柜裸。

  • TYPE - 類,接口(包括注解類型)或枚舉
  • FIELD - 字段(包括枚舉常量)
  • METHOD - 方法
  • PARAMETER - 參數(shù)
  • CONSTRUCTOR - 構(gòu)造函數(shù)
  • LOCAL_VARIABLE - 局部變量
  • ANNOTATION_TYPE -注解類型
  • PACKAGE - 包
  • TYPE_PARAMETER - 類型參數(shù)
  • TYPE_USE - 使用類型
@Retention(RetentionPolicy.RUNTIME)

??指定當(dāng)前注解保留到運(yùn)行時(shí)粱锐。保留策略有下面三種:

  • SOURCE - 注解只保留在源文件疙挺,當(dāng)Java文件編譯成class文件的時(shí)候,注解被遺棄怜浅。
  • CLASS - 注解被保留到class文件铐然,但jvm加載class文件時(shí)候被遺棄,這是默認(rèn)的生命周期海雪。
  • RUNTIME - 注解不僅被保存到class文件中锦爵,jvm加載class文件之后,仍然存在奥裸。
@Constraint(validatedBy = PhoneValidator.class)

??指定了當(dāng)前注解使用哪個(gè)校驗(yàn)類來進(jìn)行校驗(yàn)险掀。

分組校驗(yàn)

UserForm

@Data
public class UserForm {

    /**
     * id
     */
    @Null(message = "新增時(shí)id必須為空", groups = {Insert.class})
    @NotNull(message = "更新時(shí)id不能為空", groups = {Update.class})
    private String id;

    /**
     * 類型
     */
    @NotEmpty(message = "姓名不能為空" , groups = {Insert.class})
    private String name;

    /**
     * 年齡
     */
    @NotEmpty(message = "年齡不能為空" , groups = {Insert.class})
    private String age;
    
}

Insert分組

public interface Insert {
}

Update分組

public interface Update {
}

測(cè)試接口

/**
 * 添加用戶
 */
@PostMapping("/addUser")
public ResultVo addUser(@RequestBody @Validated({Insert.class}) UserForm form){
    // 選擇對(duì)應(yīng)的分組進(jìn)行校驗(yàn)
    return ResultVoUtil.success(form);
}

/**
 * 更新用戶
 */
@PostMapping("/updateUser")
public ResultVo updateUser(@RequestBody @Validated({Update.class}) UserForm form){
    // 選擇對(duì)應(yīng)的分組進(jìn)行校驗(yàn)
    return ResultVoUtil.success(form);
}

測(cè)試結(jié)果

添加測(cè)試
image
更新測(cè)試
image

順序校驗(yàn)@GroupSequence

??在@GroupSequence內(nèi)可以指定,分組校驗(yàn)的順序湾宙。比如說@GroupSequence({Insert.class, Update.class, UserForm.class})先執(zhí)行Insert校驗(yàn)樟氢,然后執(zhí)行Update校驗(yàn)。如果Insert分組侠鳄,校驗(yàn)失敗了埠啃,則不會(huì)進(jìn)行Update分組的校驗(yàn)。

@Data
@GroupSequence({Insert.class, Update.class, UserForm.class})
public class UserForm {

    /**
     * id
     */
    @Null(message = "新增時(shí)id必須為空", groups = {Insert.class})
    @NotNull(message = "更新時(shí)id不能為空", groups = {Update.class})
    private String id;

    /**
     * 類型
     */
    @NotEmpty(message = "姓名不能為空" , groups = {Insert.class})
    private String name;

    /**
     * 年齡
     */
    @NotEmpty(message = "年齡不能為空" , groups = {Insert.class})
    private String age;

}
測(cè)試接口
/**
* 編輯用戶
*/
@PostMapping("/editUser")
public ResultVo editUser(@RequestBody @Validated UserForm form){
    return ResultVoUtil.success(form);
}
測(cè)試結(jié)果

??哈哈哈伟恶,測(cè)試結(jié)果其實(shí)是個(gè)死循環(huán)碴开,不管你咋輸入都會(huì)報(bào)錯(cuò),小伙伴可以嘗試一下哦博秫。上面的例子只是個(gè)演示潦牛,在實(shí)際中還是別這樣做了,需要根據(jù)具體邏輯進(jìn)行校驗(yàn)挡育。

自定義分組校驗(yàn)

??對(duì)于之前提到了當(dāng)xxType值為A巴碗,paramA值必傳。xxType值為B即寒,paramB值必須傳這樣的場(chǎng)景橡淆。單獨(dú)使用分組校驗(yàn)和分組序列是無法實(shí)現(xiàn)的。需要使用@GroupSequenceProvider才行母赵。

自定義分組表單

@Data
@GroupSequenceProvider(value = CustomSequenceProvider.class)
public class CustomGroupForm {

    /**
     * 類型
     */
    @Pattern(regexp = "[A|B]" , message = "類型不必須為 A|B")
    private String type;

    /**
     * 參數(shù)A
     */
    @NotEmpty(message = "參數(shù)A不能為空" , groups = {WhenTypeIsA.class})
    private String paramA;

    /**
     * 參數(shù)B
     */
    @NotEmpty(message = "參數(shù)B不能為空", groups = {WhenTypeIsB.class})
    private String paramB;

    /**
     * 分組A
     */
    public interface WhenTypeIsA {

    }

    /**
     * 分組B
     */
    public interface WhenTypeIsB {

    }

}

CustomSequenceProvider

public class CustomSequenceProvider implements DefaultGroupSequenceProvider<CustomGroupForm> {

    @Override
    public List<Class<?>> getValidationGroups(CustomGroupForm form) {
        List<Class<?>> defaultGroupSequence = new ArrayList<>();

        defaultGroupSequence.add(CustomGroupForm.class);

        if (form != null && "A".equals(form.getType())) {
            defaultGroupSequence.add(CustomGroupForm.WhenTypeIsA.class);
        }

        if (form != null && "B".equals(form.getType())) {
            defaultGroupSequence.add(CustomGroupForm.WhenTypeIsB.class);
        }

        return defaultGroupSequence;
    }
}

測(cè)試接口

/**
 * 自定義分組
 */
@PostMapping("/customGroup")
public ResultVo customGroup(@RequestBody @Validated CustomGroupForm form){
    return ResultVoUtil.success(form);
}

測(cè)試結(jié)果

Type類型為A
image
Type類型為B
image

小結(jié)一下

??GroupSequence注解是一個(gè)標(biāo)準(zhǔn)的Bean認(rèn)證注解逸爵。正如之前,它能夠讓你靜態(tài)的重新定義一個(gè)類的凹嘲,默認(rèn)校驗(yàn)組順序痊银。然而GroupSequenceProvider它能夠讓你動(dòng)態(tài)的定義一個(gè)校驗(yàn)組的順序。

注意的一個(gè)點(diǎn)

SpringBoot 2.3.x 移除了validation依賴需要手動(dòng)引入依賴施绎。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

總結(jié)

??個(gè)人的一些小經(jīng)驗(yàn)溯革,參數(shù)的非空判斷贞绳,這個(gè)應(yīng)該是校驗(yàn)的第一步了,除了非空校驗(yàn)致稀,我們還需要做到下面這幾點(diǎn):

  • 普通參數(shù) - 需要限定字段的長度冈闭。如果會(huì)將數(shù)據(jù)存入數(shù)據(jù)庫,長度以數(shù)據(jù)庫為準(zhǔn)抖单,反之根據(jù)業(yè)務(wù)確定萎攒。
  • 類型參數(shù) - 最好使用正則對(duì)可能出現(xiàn)的類型做到嚴(yán)格校驗(yàn)。比如type的值是【0|1|2】這樣的矛绘。
  • 列表(list)參數(shù) - 不僅需要對(duì)list內(nèi)的參數(shù)是否合格進(jìn)行校驗(yàn)耍休,還需要對(duì)list的size進(jìn)行限制。比如說 100货矮。
  • 日期羊精,郵件,金額囚玫,URL這類參數(shù)都需要使用對(duì)于的正則進(jìn)行校驗(yàn)喧锦。
  • 參數(shù)真實(shí)性 - 這個(gè)主要針對(duì)于 各種Id 比如說 userIdmerchantId抓督,對(duì)于這樣的參數(shù)燃少,都需要進(jìn)行真實(shí)性校驗(yàn),判斷系統(tǒng)內(nèi)是有含有铃在,并且對(duì)應(yīng)的狀態(tài)是否正常阵具。

??參數(shù)校驗(yàn)越嚴(yán)格越好,嚴(yán)格的校驗(yàn)規(guī)則不僅能減少接口出錯(cuò)的概率定铜,同時(shí)還能避免出現(xiàn)臟數(shù)據(jù)阳液,從而來保證系統(tǒng)的安全性和穩(wěn)定性。

錯(cuò)誤的提醒信息需要友好一點(diǎn)哦宿稀,防止等下被前端大哥吐槽哦趁舀。

上期回顧

結(jié)尾

??如果覺得對(duì)你有幫助赖捌,可以多多評(píng)論祝沸,多多點(diǎn)贊哦,也可以到我的主頁看看越庇,說不定有你喜歡的文章罩锐,也可以隨手點(diǎn)個(gè)關(guān)注哦,謝謝卤唉。

??我是不一樣的科技宅涩惑,每天進(jìn)步一點(diǎn)點(diǎn),體驗(yàn)不一樣的生活桑驱。我們下期見竭恬!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末跛蛋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子痊硕,更是在濱河造成了極大的恐慌赊级,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件岔绸,死亡現(xiàn)場(chǎng)離奇詭異理逊,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)盒揉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門晋被,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人刚盈,你說我怎么就攤上這事羡洛。” “怎么了扁掸?”我有些...
    開封第一講書人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵翘县,是天一觀的道長。 經(jīng)常有香客問我谴分,道長锈麸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任牺蹄,我火速辦了婚禮忘伞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘沙兰。我一直安慰自己氓奈,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開白布鼎天。 她就那樣靜靜地躺著舀奶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪斋射。 梳的紋絲不亂的頭發(fā)上育勺,一...
    開封第一講書人閱讀 51,698評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音罗岖,去河邊找鬼涧至。 笑死,一個(gè)胖子當(dāng)著我的面吹牛桑包,可吹牛的內(nèi)容都是我干的南蓬。 我是一名探鬼主播,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼赘方!你這毒婦竟也來了烧颖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤窄陡,失蹤者是張志新(化名)和其女友劉穎倒信,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體泳梆,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鳖悠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了优妙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乘综。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖套硼,靈堂內(nèi)的尸體忽然破棺而出卡辰,到底是詐尸還是另有隱情,我是刑警寧澤邪意,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布九妈,位于F島的核電站,受9級(jí)特大地震影響雾鬼,放射性物質(zhì)發(fā)生泄漏萌朱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一策菜、第九天 我趴在偏房一處隱蔽的房頂上張望晶疼。 院中可真熱鬧,春花似錦又憨、人聲如沸翠霍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽寒匙。三九已至,卻和暖如春躏将,著一層夾襖步出監(jiān)牢的瞬間锄弱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來泰國打工耸携, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留棵癣,地道東北人辕翰。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓拟逮,卻偏偏與公主長得像削解,于是被迫代替她去往敵國和親焚辅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子某弦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355