【SpringBoot】 在SpringBoot中使用Hibernate Validate

【SpringBoot】 在SpringBoot中使用Hibernate Validate

前言

在做web相關(guān)的應(yīng)用時业稼,經(jīng)常需要提供接口與用戶交互(獲取數(shù)據(jù)、上傳數(shù)據(jù)等)蚂蕴,由于這個過程需要用戶進(jìn)行相關(guān)的操作低散,為了避免出現(xiàn)一些錯誤的數(shù)據(jù)等,一般需要對數(shù)據(jù)進(jìn)行校驗(yàn)骡楼,隨著接口的增多熔号,校驗(yàn)邏輯的冗余度也越來越大,雖然可以通過抽象出校驗(yàn)的方法來處理鸟整,但還是需要每次手動調(diào)用校驗(yàn)邏輯引镊,相對來說還是不方便。

為了解決這個問題篮条,Java中提供了Bean Validation的標(biāo)準(zhǔn)弟头,該標(biāo)準(zhǔn)規(guī)定了校驗(yàn)的具體內(nèi)容,通過簡單的注解就能完成必要的校驗(yàn)邏輯了兑燥,相對來說就方便了很多,而該規(guī)范其實(shí)只是規(guī)范琴拧,并沒有具體的實(shí)現(xiàn)降瞳,Hibernate提供了具體的實(shí)現(xiàn),也即Hibernate Validator,這個也是目前使用得比較多的驗(yàn)證器了挣饥。

在SpringBoot中使用Hibernate Validate

首先新建一個spring boot項目除师,引入web依賴

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

在web依賴中,已經(jīng)引入了hibernate-validator的支持扔枫,所以只需要引入web依賴即可汛聚。

mvn dependency:tree命令可以查看依賴情況

...
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.1.0.RELEASE:compile
[INFO] |  +- org.hibernate.validator:hibernate-validator:jar:6.0.13.Final:compile
[INFO] |  |  +- javax.validation:validation-api:jar:2.0.1.Final:compile
[INFO] |  |  +- org.jboss.logging:jboss-logging:jar:3.3.2.Final:compile
[INFO] |  |  \- com.fasterxml:classmate:jar:1.4.0:compile
...

如果你所使用的版本沒有支持,或者不是使用SpringBoot項目短荐,具體的請參考文檔解決倚舀。

然后配置一下validator,由于默認(rèn)情況下忍宋,Hibernate-validator使用的校驗(yàn)策略是依次校驗(yàn)痕貌,并且將不通過的結(jié)果保存,最后再統(tǒng)一拋出異常信息糠排,但實(shí)際上舵稠,當(dāng)校驗(yàn)出現(xiàn)第一個不滿足情況的時候,就可以停止了(當(dāng)然入宦,如果選擇全部驗(yàn)證完也是可以的)哺徊,所以我們手動配置一下

ValidatorConfig

@Configuration
public class ValidatorConfig {

    @Bean
    public Validator validator() {
        ValidatorFactory factory = Validation.byProvider(HibernateValidator.class)
                .configure()
                // 將fail_fast設(shè)置為true即可,如果想驗(yàn)證全部乾闰,則設(shè)置為false或者取消配置即可
                .addProperty("hibernate.validator.fail_fast", "true")
                .buildValidatorFactory();
        return factory.getValidator();
    }
}

接下來編寫需要進(jìn)行驗(yàn)證的Bean

User.java

public class User {

    @NotBlank(message = "用戶名不能為空")
    private String name;

    @Max(value = 120, message = "年齡不能超過120歲")
    private int age;

    @NotNull
    @Size(min = 8, max = 20, message = "密碼必須大于8位并且小于20位")
    private String password;

    @Email(message = "請輸入符合格式的郵箱")
    private String email;

    // set落追、get方法
}

上面的注解已經(jīng)很能夠見名知意了,所以這里就先不講解汹忠,后面再補(bǔ)充常用的驗(yàn)證注解及作用總結(jié)淋硝。

定義一個簡單的測試接口

UserController.java

@RestController
@RequestMapping("/users")
public class UserController {

    @PostMapping
    public User addUser(@Valid @RequestBody User user) {
        // 僅測試驗(yàn)證過程,省略其他的邏輯
        return user;
    }
}

注意上面所使用的@Valid注解宽菜,通過該注解能夠使得驗(yàn)證生效谣膳,如果去除的話,可以看到驗(yàn)證邏輯并沒有生效铅乡。

通過上面的一個簡單注解之后继谚,驗(yàn)證的邏輯已經(jīng)能夠生效,然而阵幸,在測試的時候花履,可能會出現(xiàn)下面的情況

{
    "timestamp": "2018-11-09T01:47:56.985+0000",
    "status": 400,
    "error": "Bad Request",
    "errors": [
        {
            "codes": [
                "Size.user.password",
                "Size.password",
                "Size.java.lang.String",
                "Size"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.password",
                        "password"
                    ],
                    "arguments": null,
                    "defaultMessage": "password",
                    "code": "password"
                },
                20,
                8
            ],
            "defaultMessage": "密碼必須大于8位并且小于20位",
            "objectName": "user",
            "field": "password",
            "rejectedValue": "huanfe",
            "bindingFailure": false,
            "code": "Size"
        }
    ]
    # 省略trace信息
}

這是因?yàn)椋J(rèn)情況下挚赊,SpringBoot配置了默認(rèn)異常處理器DefaultHandlerExceptionResolver诡壁,而該處理器僅僅是將異常信息打印出來,顯然荠割,我們并不需要返回如此多的信息妹卿,只需要將對應(yīng)屬性中的message信息給調(diào)用者即可旺矾,解決的方法有兩種。

  1. 在需要驗(yàn)證的方法中加入BindingResult參數(shù)夺克,SpringBoot會自動將異常錯誤信息綁定到該參數(shù)上箕宙,然后處理對應(yīng)的邏輯,如下

    UserController.java

    @PostMapping
    public User addUser(@Valid @RequestBody User user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            // 具體的處理邏輯铺纽,如封裝錯誤信息等
        }
        return user;
    }
    

    但是這種方式不是很優(yōu)雅柬帕,因?yàn)閷τ诿恳粋€需要驗(yàn)證的方法,都需要進(jìn)行這樣的邏輯(雖然封裝處理可以解決狡门,但依舊每次需要手動調(diào)用以及加入BindingResult參數(shù))

  2. 由于在驗(yàn)證失敗的時候陷寝,會拋出異常,所以可以使用全局異常處理器來捕獲該異常融撞,然后進(jìn)行統(tǒng)一處理即可盼铁,具體的異常類型是MethodArgumentNotValidException,具體實(shí)現(xiàn)如下所示

    GlobalExceptionHandler.java

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResultInfo<?> validationErrorHandler(MethodArgumentNotValidException ex) {
            // 同樣是獲取BindingResult對象尝偎,然后獲取其中的錯誤信息
            // 如果前面開啟了fail_fast饶火,事實(shí)上這里只會有一個信息
            //如果沒有,則可能又多個
            List<String> errorInformation = ex.getBindingResult().getAllErrors()
                    .stream()
                    .map(ObjectError::getDefaultMessage)
                    .collect(Collectors.toList());
            return new ResultInfo<>(400, errorInformation.toString(), null);
        }
    }
    

    這里的ResultInfo是自定義的一個結(jié)果對象致扯,用于作為統(tǒng)一的返回對象肤寝,內(nèi)容如下

    ResultInfo.java

    public class ResultInfo<T> {
        private int code;
        private String message;
        private T body;
    
        public ResultInfo(int code, String message, T body) {
            this.code = code;
            this.message = message;
            this.body = body;
        }
    
        public ResultInfo(int code, String message) {
            this(code, message, null);
        }
    
        public ResultInfo(String message) {
            this(200, message, null);
        }
    }
    // get、set方法
    

通過上面的處理之后抖僵,現(xiàn)在如果驗(yàn)證不通過鲤看,則可以以比較優(yōu)雅的方式返回給調(diào)用者了。

{
    "code": 400,
    "message": "[密碼必須大于8位并且小于20位]",
    "body": null
}

這里需要注意耍群,如果上面兩種方式都開啟的話义桂,是以第一種方式優(yōu)先的,所以蹈垢,第二種方式不會生效慷吊。

上面的方式能夠解決@RequestBody標(biāo)注的參數(shù)的驗(yàn)證及錯誤處理,然而曹抬,并不能處理@PathVariable以及@RequestParam標(biāo)注的入?yún)?不生效)溉瓶,而事實(shí)上,這兩種類型的操作也是非常常用的(也是需要對這兩種類型進(jìn)行驗(yàn)證谤民,除了手動驗(yàn)證外豆混,還有一種通用的解決方案薪鹦,也是通過注解來實(shí)現(xiàn)),對于這兩種類型同樣可以使用驗(yàn)證注解進(jìn)行標(biāo)注经瓷,如下所示

UserController.java

@RestController
@RequestMapping("/users")
public class UserController {

    // .....

    @GetMapping("/{name}")
    public User getUserByName(
                    @NotNull 
                    @Size(min = 1, max = 20, message = "用戶名格式有誤")
                    @PathVariable String name) {
        User user = new User();
        user.setName(name);
        return user;
    }

    @GetMapping
    public User getUserByNameParam(
                    @NotNull 
                    @Size(min = 1, max = 20, message = "用戶名格式有誤") 
                    @RequestParam("name") String name) {
        User user = new User();
        user.setName(name);
        return user;
    }
}

為了讓對應(yīng)的注解生效宋税,可以在類的上方使用@Validated進(jìn)行標(biāo)注价说,注意是標(biāo)注在類上方馅精,即

@RestController
@RequestMapping("/users")
@Validated
public class UserController {
    // ...
}

但此時如果驗(yàn)證失敗,會拋出異常信息顺饮,而且,異常類型不是MethodArgumentNotValidException凌那,而是ConstraintViolationException,巨坑R魇拧C钡!块攒,所以還需要捕獲該類型并且進(jìn)行處理励稳,如下所示

GlobalExceptionHandler.java

@ExceptionHandler(ConstraintViolationException.class)
public ResultInfo<?> validationErrorHandler(ConstraintViolationException ex) {
    List<String> errorInformation = ex.getConstraintViolations()
            .stream()
            .map(ConstraintViolation::getMessage)
            .collect(Collectors.toList());
    return new ResultInfo<>(400, errorInformation.toString(), null);
}

到此,基本上的參數(shù)驗(yàn)證就能完成了囱井。

此外驹尼,在搜索解決方案的過程中,也發(fā)現(xiàn)了兩個有用的小技巧庞呕,順便記錄在這里(跟上面的內(nèi)容無關(guān)哈)新翎。

對于@PathVariable還有另外一種解決方案(嚴(yán)格來說作為驗(yàn)證并不完善),通過正則表達(dá)式來進(jìn)行匹配(這種方式不支持長度限制住练,但可以進(jìn)行類型限制地啰,如只包含字符,只包含數(shù)字等)讲逛,如下所示

UserController.java

@GetMapping("/{name:[a-zA-Z]+")
public User getUserByNameRegex(@PathVariable String name) {
    User user = new User();
    user.setName(name);
    return user;
}

對于@RequestParam來說亏吝,可以使用默認(rèn)值來實(shí)現(xiàn)可變化的參數(shù)列表,如下所示

UserController.java

@GetMapping
public User getUser(
    @RequestParam(required = true, value = "name") String name,
    @RequestParam(value = "age", defaultValue = "22") int age) {
    
    return new User();
}

更復(fù)雜的如分頁盏混,排序等等蔚鸥,可以通過默認(rèn)參數(shù)的形式,來實(shí)現(xiàn)许赃,而不再需要強(qiáng)制調(diào)用者輸入對應(yīng)的參數(shù)(畢竟這些參數(shù)是可選的嘛)止喷。

常用驗(yàn)證注解

常用的注解主要有以下幾個,作用及內(nèi)容如下所示

  • @Null图焰,標(biāo)注的屬性值必須為空
  • @NotNull启盛,標(biāo)注的屬性值不能為空
  • @AssertTrue,標(biāo)注的屬性值必須為true
  • @AssertFalse技羔,標(biāo)注的屬性值必須為false
  • @Min僵闯,標(biāo)注的屬性值不能小于min中指定的值
  • @Max,標(biāo)注的屬性值不能大于max中指定的值
  • @DecimalMin藤滥,小數(shù)值鳖粟,同上
  • @DecimalMax,小數(shù)值拙绊,同上
  • @Negative向图,負(fù)數(shù)
  • @NegativeOrZero泳秀,0或者負(fù)數(shù)
  • @Positive,整數(shù)
  • @PositiveOrZero榄攀,0或者整數(shù)
  • @Size嗜傅,指定字符串長度,注意是長度檩赢,有兩個值吕嘀,min以及max,用于指定最小以及最大長度
  • @Digits贞瞒,內(nèi)容必須是數(shù)字
  • @Past偶房,時間必須是過去的時間
  • @PastOrPresent,過去或者現(xiàn)在的時間
  • @Future军浆,將來的時間
  • @FutureOrPresent棕洋,將來或者現(xiàn)在的時間
  • @Pattern,用于指定一個正則表達(dá)式
  • @NotEmpty乒融,字符串內(nèi)容非空
  • @NotBlank掰盘,字符串內(nèi)容非空且長度大于0
  • @Email,郵箱
  • @Range赞季,用于指定數(shù)字庆杜,注意是數(shù)字的范圍,有兩個值碟摆,min以及max

總結(jié)

本小節(jié)主要學(xué)習(xí)了如何在SpringBoot中使用Hibernate-Validator晃财,驗(yàn)證器的作用在于驗(yàn)證參數(shù)是否符合規(guī)定,通過配置驗(yàn)證器以及對應(yīng)的異常處理器典蜕,可以使我們從繁瑣的驗(yàn)證流程中解脫出來断盛,當(dāng)然,對于復(fù)雜的驗(yàn)證愉舔,其實(shí)還是要手動驗(yàn)證的钢猛,驗(yàn)證器能提供的是一些通用的,常規(guī)的驗(yàn)證操作轩缤,當(dāng)然命迈,大部分情況下已經(jīng)足夠了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末火的,一起剝皮案震驚了整個濱河市壶愤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌馏鹤,老刑警劉巖征椒,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異湃累,居然都是意外死亡勃救,警方通過查閱死者的電腦和手機(jī)碍讨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蒙秒,“玉大人勃黍,你說我怎么就攤上這事≡谓玻” “怎么了溉躲?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長益兄。 經(jīng)常有香客問我,道長箭券,這世上最難降的妖魔是什么净捅? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮辩块,結(jié)果婚禮上蛔六,老公的妹妹穿的比我還像新娘。我一直安慰自己废亭,他們只是感情好国章,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著豆村,像睡著了一般液兽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上掌动,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天四啰,我揣著相機(jī)與錄音,去河邊找鬼粗恢。 笑死柑晒,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的眷射。 我是一名探鬼主播匙赞,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼妖碉!你這毒婦竟也來了涌庭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤欧宜,失蹤者是張志新(化名)和其女友劉穎脾猛,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鱼鸠,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡猛拴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年羹铅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片愉昆。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡职员,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出跛溉,到底是詐尸還是另有隱情焊切,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布芳室,位于F島的核電站专肪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏堪侯。R本人自食惡果不足惜嚎尤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望伍宦。 院中可真熱鬧芽死,春花似錦、人聲如沸次洼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卖毁。三九已至揖曾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間亥啦,已是汗流浹背翩肌。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留禁悠,地道東北人念祭。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像碍侦,于是被迫代替她去往敵國和親粱坤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

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