Spring boot使用Javax.validation和ControllerAdvice來(lái)進(jìn)行參數(shù)校驗(yàn)

對(duì)于寫(xiě)Java的同學(xué)來(lái)說(shuō),參數(shù)校驗(yàn)是繁瑣且重復(fù)性很高的代碼恢氯。很多時(shí)候我們的業(yè)務(wù)代碼編寫(xiě)之前先要進(jìn)行很多的參數(shù)校驗(yàn)邓馒,浪費(fèi)了大量的時(shí)間和精力墙杯。而java中其實(shí)已經(jīng)內(nèi)置了參數(shù)校驗(yàn)的工具,本篇文章主要介紹如何使用Javax.validation來(lái)進(jìn)行參數(shù)校驗(yàn)丑搔。

@validated注解

@validated是一套幫助我們繼續(xù)對(duì)傳輸?shù)膮?shù)進(jìn)行數(shù)據(jù)校驗(yàn)的注解厦瓢,通過(guò)配置Validation可以很輕松的完成對(duì)數(shù)據(jù)的約束∑≡拢看到一下注解的源碼煮仇,我們可以看到@Validated注解可以作用在類(lèi)、方法和參數(shù)上谎仲。

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
 Class<?>[] value() default {};
}

廢話(huà)不多說(shuō)浙垫,我們直接舉個(gè)例子來(lái)看看@validated到底好不好用。實(shí)際項(xiàng)目中很常見(jiàn)的應(yīng)用是分頁(yè)查詢(xún)的接口郑诺,通常分頁(yè)查詢(xún)至少需要當(dāng)前頁(yè)和頁(yè)大小這兩個(gè)字段夹姥。通常我們會(huì)把分頁(yè)請(qǐng)求需要的參數(shù)封裝成一個(gè)PageQuery。一個(gè)常見(jiàn)的分頁(yè)參數(shù)類(lèi)辙诞,在很多接口中需要使用辙售。我們就可以給這樣的參數(shù)加上@Validated注解。表示此類(lèi)開(kāi)啟參數(shù)校驗(yàn)

public Result<Page<Map<String, Object>>> getPage(@Validated PageQuery pageQuery) {  
 // 設(shè)置頁(yè)大小倘要,當(dāng)前頁(yè)  
    Page<Map<String, Object>> page = new Page<>(pageQuery.getCurrentPage(), pageQuery.getPageSize());  
    page.setRecords(service.findPage(page, pageQuery));  
    return this.responseBody(Result.ResponseEnum.GET_SUCCESS, page);  
}

在類(lèi)的字段上圾亏,我們定義校驗(yàn)的規(guī)則和返回的錯(cuò)誤提示。@validated中所有的校驗(yàn)注解封拧,可以參考下面的表格志鹃。

限制 說(shuō)明
@Null 限制只能為null
@NotNull 限制必須不為null
@AssertFalse 限制必須為false
@AssertTrue 限制必須為true
@DecimalMax(value) 限制必須為一個(gè)不大于指定值的數(shù)字
@DecimalMin(value) 限制必須為一個(gè)不小于指定值的數(shù)字
@Digits(integer,fraction) 限制必須為一個(gè)小數(shù),且整數(shù)部分的位數(shù)不能超過(guò)integer泽西,小數(shù)部分的位數(shù)不能超過(guò)fraction
@Max(value) 限制必須為一個(gè)不大于指定值的數(shù)字
@Min(value) 限制必須為一個(gè)不小于指定值的數(shù)字
@Past 限制必須是一個(gè)過(guò)去的日期
@Future 限制必須是一個(gè)將來(lái)的日期
@Pattern(value) 限制必須符合指定的正則表達(dá)式
@Size(max,min) 限制字符長(zhǎng)度必須在min到max之間
@NotEmpty 驗(yàn)證注解的元素值不為null且不為空(字符串長(zhǎng)度不為0曹铃、集合大小不為0)
@NotBlank 驗(yàn)證注解的元素值不為空(不為null、去除首位空格后長(zhǎng)度為0)捧杉,不同于@NotEmpty陕见,@NotBlank只應(yīng)用于字符串且在比較時(shí)會(huì)去除字符串的空格
@Email 驗(yàn)證注解的元素值是Email秘血,也可以通過(guò)正則表達(dá)式和flag指定自定義的email格式

對(duì)于頁(yè)大小,我們限制為非空且不能小于1评甜;對(duì)于當(dāng)前頁(yè)我們限制為非空灰粮。

public class PageQuery implements Serializable {  
 private static final long serialVersionUID = 1L;  
  
/**  
 * 頁(yè)大小  
 */  
 @NotNull(message = "頁(yè)大小不能為空")  
 @Min(message = "頁(yè)大小不能小于1", value = 1)  
 Integer pageSize;  
  
/**  
 * 當(dāng)前頁(yè)  
 */  
 @NotNull(message = "當(dāng)前頁(yè)不能為空")  
 Integer currentPage;  
 

我們用一個(gè)明顯校驗(yàn)不通過(guò)的參數(shù)來(lái)請(qǐng)求下這個(gè)接口,看看會(huì)返回什么忍坷。我在請(qǐng)求參數(shù)中不傳遞pageSize參數(shù)粘舟,然后發(fā)送請(qǐng)求。

{
    "timestamp": "2021-12-16T03:09:36.238+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "NotNull.pageQuery.currentPage",
                "NotNull.currentPage",
                "NotNull.java.lang.Integer",
                "NotNull"
            ],
            "arguments": [
                {
                    "codes": [
                        "pageQuery.currentPage",
                        "currentPage"
                    ],
                    "arguments": null,
                    "defaultMessage": "currentPage",
                    "code": "currentPage"
                }
            ],
            "defaultMessage": "當(dāng)前頁(yè)不能為空",
            "objectName": "pageQuery",
            "field": "currentPage",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotNull"
        },
        {
            "codes": [
                "NotNull.pageQuery.env",
                "NotNull.env",
                "NotNull.java.lang.String",
                "NotNull"
            ],
            "arguments": [
                {
                    "codes": [
                        "pageQuery.env",
                        "env"
                    ],
                    "arguments": null,
                    "defaultMessage": "env",
                    "code": "env"
                }
            ],
            "defaultMessage": "設(shè)備所屬環(huán)境信息不能為空",
            "objectName": "pageQuery",
            "field": "env",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotNull"
        },
        {
            "codes": [
                "NotNull.pageQuery.pageSize",
                "NotNull.pageSize",
                "NotNull.java.lang.Integer",
                "NotNull"
            ],
            "arguments": [
                {
                    "codes": [
                        "pageQuery.pageSize",
                        "pageSize"
                    ],
                    "arguments": null,
                    "defaultMessage": "pageSize",
                    "code": "pageSize"
                }
            ],
            "defaultMessage": "頁(yè)大小不能為空",
            "objectName": "pageQuery",
            "field": "pageSize",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotNull"
        }
    ],
    "message": "Validation failed for object='pageQuery'. Error count: 3",
    "path": "/localLoadBalance/approval/getApproval"
}

可以看到佩研,返回的結(jié)果中包含了我們之前預(yù)設(shè)的校驗(yàn)提示和內(nèi)容柑肴。不過(guò)到這里我們的任務(wù)還沒(méi)有結(jié)束,實(shí)際項(xiàng)目中旬薯,我們不允許接口返回這樣的類(lèi)型晰骑。大多數(shù)情況下,我們希望接口的返回結(jié)果有通用的模板格式绊序。而上面那樣的返回方式需要前端做大量的解析硕舆,而且也不符合后端接口的規(guī)范。因此我們希望能有一個(gè)全局的處理器政模,來(lái)解析@validated拋出的異常岗宣。

使用全局異常處理類(lèi)來(lái)進(jìn)行統(tǒng)一的異常處理

在Spring boot項(xiàng)目中,我們可以使用@ControllerAdvice注解來(lái)進(jìn)行全局的異常處理淋样,當(dāng)然@ControllerAdvice的用處不止是異常處理耗式,還可以實(shí)現(xiàn)統(tǒng)一的參數(shù)綁定和數(shù)據(jù)的預(yù)處理。詳情可以參考# SpringMVC 中 @ControllerAdvice 注解的三種使用場(chǎng)景趁猴!

首先我們新增一個(gè)handler刊咳,當(dāng)然你也可以指定一個(gè)包來(lái)掃描包下的所有controller。如@ControllerAdvice(basePackages="com.test.controller")儡司,然后我們使用@ExceptionHandler來(lái)進(jìn)行異常的處理娱挨。

此處需要說(shuō)明的是,我們是針對(duì)@validated進(jìn)行的異常的處理捕犬,因此我們希望異常校驗(yàn)類(lèi)只攔截@validated注解拋出的異常跷坝。所以在本方法中,我只讓@ExceptionHandler攔截了BindException碉碉。其次柴钻,針對(duì)參數(shù)校驗(yàn)出現(xiàn)多個(gè)異常的情況,我們把多個(gè)錯(cuò)誤信息通過(guò)逗號(hào)分隔開(kāi)來(lái)垢粮。

@ControllerAdvice  
public class GlobalHandler {  
 private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class);  
  
    /**  
 * 全局處理所有使用了@validation校驗(yàn)參數(shù)的controller  
 * @param e 捕獲到validation拋出異常  
 * @return 返回參數(shù)中所有的校驗(yàn)錯(cuò)誤贴届,以,分隔不用的錯(cuò)誤信息  
 */  
 @ResponseBody  
 @ExceptionHandler(BindException.class)  
 public Result<Void> exceptionHandler(BindException e) {  
 String errors=e.getBindingResult().getAllErrors().stream()  
 .map(ObjectError::getDefaultMessage)  
 .collect(Collectors.joining(","));  
        logger.error("Request params error,caught by global exception handler,{}",errors);  
        return Result.<Void>toBuilder()  
 .code(0)  
 .msg(errors)  
 .builder();  
    }  
}

再次準(zhǔn)備一個(gè)含有錯(cuò)誤參數(shù)的請(qǐng)求,這次我們不傳currentPage,pageSize的值為-1毫蚓。我們看看會(huì)返回什么占键。

{
 "code": 0,
 "msg": "當(dāng)前頁(yè)不能為空,頁(yè)大小不能小于1",
 "data": null
}

可以看到,返回的結(jié)果符合我們的預(yù)期元潘。

在獲取錯(cuò)誤信息的地方我們看到有針對(duì)BindException的異常信息解析畔乙,涉及了多個(gè).操作。有經(jīng)驗(yàn)的老鳥(niǎo)可能覺(jué)得這里容易出現(xiàn)空指針異常翩概。不過(guò)此處你大可放心啸澡,BindException中的BindingResult是絕對(duì)不會(huì)為null的。我們看下源碼氮帐,可以看到內(nèi)部是用斷言來(lái)保證結(jié)果不為空的。

    /**
     * Create a new BindException instance for a BindingResult.
     * @param bindingResult the BindingResult instance to wrap
     */
    public BindException(BindingResult bindingResult) {
        Assert.notNull(bindingResult, "BindingResult must not be null");
        this.bindingResult = bindingResult;
    }

參考文章

SpringMVC 中 @ControllerAdvice 注解的三種使用場(chǎng)景洛姑! - 江南一點(diǎn)雨 - 博客園 (cnblogs.com)

javax.validation 參數(shù)驗(yàn)證 - 不朽丶 - 博客園 (cnblogs.com)

@Validated詳解 - yuxinkuan - 博客園 (cnblogs.com)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末上沐,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子楞艾,更是在濱河造成了極大的恐慌参咙,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件硫眯,死亡現(xiàn)場(chǎng)離奇詭異蕴侧,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)两入,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)净宵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人裹纳,你說(shuō)我怎么就攤上這事择葡。” “怎么了剃氧?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵敏储,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我朋鞍,道長(zhǎng)已添,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任滥酥,我火速辦了婚禮更舞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘恨狈。我一直安慰自己疏哗,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著返奉,像睡著了一般贝搁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芽偏,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天雷逆,我揣著相機(jī)與錄音,去河邊找鬼污尉。 笑死膀哲,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的被碗。 我是一名探鬼主播某宪,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼锐朴!你這毒婦竟也來(lái)了兴喂?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤焚志,失蹤者是張志新(化名)和其女友劉穎衣迷,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體酱酬,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡壶谒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了膳沽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汗菜。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖挑社,靈堂內(nèi)的尸體忽然破棺而出呵俏,到底是詐尸還是另有隱情,我是刑警寧澤滔灶,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布普碎,位于F島的核電站,受9級(jí)特大地震影響录平,放射性物質(zhì)發(fā)生泄漏麻车。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一斗这、第九天 我趴在偏房一處隱蔽的房頂上張望动猬。 院中可真熱鬧,春花似錦表箭、人聲如沸赁咙。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)彼水。三九已至崔拥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間凤覆,已是汗流浹背链瓦。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盯桦,地道東北人慈俯。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像拥峦,于是被迫代替她去往敵國(guó)和親贴膘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容