Hibernate Validator校驗參數(shù)全攻略

1. 前言

數(shù)據(jù)字段一般都要遵循業(yè)務要求和數(shù)據(jù)庫設計肝集,所以后端的參數(shù)校驗是必須的瞻坝,應用程序必須通過某種手段來確保輸入進來的數(shù)據(jù)從語義上來講是正確的。

2. 數(shù)據(jù)校驗的痛點

為了保證數(shù)據(jù)語義的正確杏瞻,我們需要進行大量的判斷來處理驗證邏輯所刀。而且項目的分層也會造成一些重復的校驗衙荐,產生大量與業(yè)務無關的代碼。不利于代碼的維護浮创,增加了開發(fā)人員的工作量忧吟。

3. JSR 303校驗規(guī)范及其實現(xiàn)

為了解決上面的痛點,將驗證邏輯與相應的領域模型進行綁定是十分有必要的斩披。為此產生了JSR 303 – Bean Validation 規(guī)范溜族。Hibernate ValidatorJSR-303的參考實現(xiàn),它提供了JSR 303規(guī)范中所有的約束(constraint)的實現(xiàn)雏掠,同時也增加了一些擴展斩祭。

Hibernate Validator 提供的常用的約束注解

約束注解 詳細信息
@Null 被注釋的元素必須為 null
@NotNull 被注釋的元素必須不為 null
@AssertTrue 被注釋的元素必須為 true
@AssertFalse 被注釋的元素必須為 false
@Min(value) 被注釋的元素必須是一個數(shù)字,其值必須大于等于指定的最小值
@Max(value) 被注釋的元素必須是一個數(shù)字乡话,其值必須小于等于指定的最大值
@DecimalMin(value) 被注釋的元素必須是一個數(shù)字摧玫,其值必須大于等于指定的最小值
@DecimalMax(value) 被注釋的元素必須是一個數(shù)字,其值必須小于等于指定的最大值
@Size(max, min) 被注釋的元素的大小必須在指定的范圍內
@Digits (integer, fraction) 被注釋的元素必須是一個數(shù)字绑青,其值必須在可接受的范圍內
@Past 被注釋的元素必須是一個過去的日期
@Future 被注釋的元素必須是一個將來的日期
@Pattern(value) 被注釋的元素必須符合指定的正則表達式
@Email 被注釋的元素必須是電子郵箱地址
@Length 被注釋的字符串的大小必須在指定的范圍內
@NotEmpty 被注釋的字符串的必須非空
@Range 被注釋的元素必須在合適的范圍內

4. 驗證注解的使用

Spring Boot開發(fā)中使用Hibernate Validator是非常容易的诬像,引入下面的starter就可以了:

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

一種可以實現(xiàn)接口來定制Validator,一種是使用約束注解闸婴。胖哥覺得注解可以滿足絕大部分的需求坏挠,所以建議使用注解來進行數(shù)據(jù)校驗。而且注解更加靈活邪乍,控制的粒度也更加細降狠。接下來我們來學習如何使用注解進行數(shù)據(jù)校驗。

4.1 約束注解的基本使用

我們對需要校驗的方法入?yún)⑦M行注解約束標記庇楞,例子如下:

@Data
public class Student {

    @NotBlank(message = "姓名必須填")
    private String name;
    @NotNull(message = "年齡必須填寫")
    @Range(min = 1,max =50, message = "年齡取值范圍1-50")
    private Integer age;
    @NotEmpty(message = "成績必填")
    private List<Double> scores;
}

POST請求

然后定義一個POST請求的Spring MVC接口:

@RestController
@RequestMapping("/student")
public class StudentController {

    
    @PostMapping("/add")
    public Rest<?> addStudent(@Valid @RequestBody Student student) {
        return RestBody.okData(student);
    }
}   

通過對addStudent方法入?yún)⑻砑?code>@Valid來啟用參數(shù)校驗榜配。當使用下面數(shù)據(jù)進行請求將會拋出MethodArgumentNotValidException異常,提示age范圍超出1-50吕晌。

POST /student/add HTTP/1.1
Host: localhost:8888
Content-Type: application/json

{
    "name": "felord.cn",
    "age": 77,
    "scores": [
        55
    ]
}

GET請求

如法炮制蛋褥,我們定義一個GET請求的接口:

@GetMapping("/get")
public Rest<?> getStudent(@Valid Student student) {
    return RestBody.okData(student);
}

使用下面的請求可以正確對學生分數(shù)scores進行了校驗,但是拋出的并不是MethodArgumentNotValidException異常睛驳,而是BindException異常烙心。這和使用@RequestBody注解有關系,這對我們后面的統(tǒng)一處理非常十分重要乏沸。

GET /student/get?name=felord.cn&age=12 HTTP/1.1
Host: localhost:8888

自定義注解

可能有些同學注意到上面的年齡我進行了這樣的標記:

@NotNull(message = "年齡必須填寫")
@Range(min = 1,max =50, message = "年齡取值范圍1-50")
private Integer age;

這是因為@Range不會去校驗為空的情況淫茵,它只處理非空的時候是否符合范圍約束。所以要用多個注解來約束屎蜓。如果我們某些場景需要重復的捆綁多個注解來使用時痘昌,可以使用自定義注解將它們封裝起來組合使用,下面這個注解就是將@NotNull@Range進行了組合炬转,你可以仿一個出來用用看辆苔。

import org.hibernate.validator.constraints.Range;

import javax.validation.Constraint;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.NotNull;
import javax.validation.constraintvalidation.SupportedValidationTarget;
import javax.validation.constraintvalidation.ValidationTarget;
import java.lang.annotation.*;

/**
 * @author a
 * @since 17:31
 **/
@Constraint(
        validatedBy = {}
)
@SupportedValidationTarget({ValidationTarget.ANNOTATED_ELEMENT})
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, 
        ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, 
        ElementType.PARAMETER, ElementType.TYPE_USE})
@NotNull
@Range(min = 1, max = 50)
@Documented
@ReportAsSingleViolation
public @interface Age {
    // message 必須有
    String message() default "年齡必須填寫,且范圍為 1-50 ";

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

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

還有一種情況扼劈,我們在后臺定義了枚舉值來進行狀態(tài)的流轉驻啤,也是需要校驗的,比如我們定義了顏色枚舉:

public enum Colors {

    RED, YELLOW, BLUE

}

我們希望入?yún)⒉荒艹?code>Colors的范圍["RED", "YELLOW", "BLUE"]荐吵,這就需要實現(xiàn)ConstraintValidator<A extends Annotation, T>接口來定義一個顏色約束了骑冗,其中泛型A為自定義的約束注解,泛型T為入?yún)⒌念愋拖燃澹@里使用字符串,然后我們的實現(xiàn)如下:

/**
 * @author felord.cn
 * @since 17:57
 **/
public class ColorConstraintValidator implements ConstraintValidator<Color, String> {
    private static final Set<String> COLOR_CONSTRAINTS = new HashSet<>();

    @Override
    public void initialize(Color constraintAnnotation) {
        Colors[] value = constraintAnnotation.value();
        List<String> list = Arrays.stream(value)
                .map(Enum::name)
                .collect(Collectors.toList());
        COLOR_CONSTRAINTS.addAll(list);

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return COLOR_CONSTRAINTS.contains(value);
    }
}

然后聲明對應的約束注解Color贼涩,需要在元注解@Constraint中指明使用上面定義好的處理類ColorConstraintValidator進行校驗。

/**
 * @author felord.cn
 * @since 17:55
 **/
@Constraint(validatedBy = ColorConstraintValidator.class)
@Documented
@Target({ElementType.METHOD, ElementType.FIELD,
        ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR,
        ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Color {
    // 錯誤提示信息
    String message() default "顏色不符合規(guī)格";

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

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

    // 約束的類型
    Colors[] value();
}

然后我們來試一下薯蝎,先對參數(shù)進行約束:

@Data
public class Param {
    @Color({Colors.BLUE,Colors.YELLOW})
   private String color;
}

接口跟上面幾個一樣遥倦,調用下面的接口將拋出BindException異常:

GET /student/color?color=CAY HTTP/1.1
Host: localhost:8888

當我們把參數(shù)color賦值為BLUE或者YELLOW后,能夠成功得到響應占锯。

4.2 常見問題

在實際使用起來我們會遇到一些問題袒哥,這里總結了一些常見的問題和處理方式。

檢驗基礎類型不生效的問題

上面為了校驗顏色我們聲明了一個Param對象來包裝唯一的字符串參數(shù)color消略,為什么直接使用下面的方式定義呢堡称?

@GetMapping("/color")
public Rest<?> color(@Valid @Color({Colors.BLUE,Colors.YELLOW}) String color) {
    return RestBody.okData(color);
}

或者使用路徑變量:

@GetMapping("/rest/{color}")
public Rest<?> rest(@Valid @Color({Colors.BLUE, Colors.YELLOW}) @PathVariable String color) {
    return RestBody.okData(color);
}

上面兩種方式是不會生效的。不信你可以試一試艺演,起碼在Spring Boot 2.3.1.RELEASE是不會直接生效的却紧。

使以上兩種生效的方法是在類上添加@Validated注解。注意一定要添加到方法所在的類上才行胎撤。這時候會拋出ConstraintViolationException異常晓殊。

集合類型參數(shù)中的元素不生效的問題

就像下面的寫法,方法的參數(shù)為集合時哩照,如何檢驗元素的約束呢挺物?

/**
 * 集合類型參數(shù)元素.
 *
 * @param student the student
 * @return the rest
 */
@PostMapping("/batchadd")
public Rest<?> batchAddStudent(@Valid @RequestBody List<Student> student) {
    return RestBody.okData(student);
}

同樣是在類上添加@Validated注解。注意一定要添加到方法所在的類上才行飘弧。這時候會拋出ConstraintViolationException異常识藤。

嵌套校驗不生效

嵌套的結構如何校驗呢?打個比方次伶,如果我們在學生類Student中添加了其所屬的學校信息School并希望對School的屬性進行校驗痴昧。

@Data
public class Student {

    @NotBlank(message = "姓名必須填")
    private String name;
    @Age
    private Integer age;
    @NotEmpty(message = "成績必填")
    private List<Double> scores;
    @NotNull(message = "學校不能為空")
    private School school;
}


@Data
public class School {
    @NotBlank(message = "學校名稱不能為空")
    private String name;
    @Min(value = 0,message ="校齡大于0" )
    private Integer age;
}

GET請求時正常校驗了School的屬性,但是POST請求卻無法對School的屬性進行校驗冠王。這時我們只需要在該屬性上加上@Valid注解即可赶撰。

@Data
public class Student {

    @NotBlank(message = "姓名必須填")
    private String name;
    @Age
    private Integer age;
    @NotEmpty(message = "成績必填")
    private List<Double> scores;
    @Valid
    @NotNull(message = "學校不能為空")
    private School school;
}

每加一層嵌套都需要加一層@Valid注解。通常在校驗對象屬性時,@NotNull豪娜、@NotEmpty@Valid配合才能起到校驗效果餐胀。

如果你有其它問題可以通過felord.cn聯(lián)系到我探討。

5. 總結

通過校驗框架我們可以專心于業(yè)務開發(fā)瘤载,本文對Hibernate Validator的使用和一些常見問題進行了梳理否灾。我們可以通過Spring Boot統(tǒng)一異常處理來解決參數(shù)校驗的異常信息的提示問題。具體可以通過關注:碼農小胖哥 回復 valid獲取相關DEMO鸣奔。

關注公眾號:碼農小胖哥墨技,獲取更多資訊

個人博客:https://felord.cn

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市挎狸,隨后出現(xiàn)的幾起案子扣汪,更是在濱河造成了極大的恐慌,老刑警劉巖锨匆,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件崭别,死亡現(xiàn)場離奇詭異,居然都是意外死亡统刮,警方通過查閱死者的電腦和手機紊遵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來侥蒙,“玉大人暗膜,你說我怎么就攤上這事”揆茫” “怎么了学搜?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長论衍。 經常有香客問我瑞佩,道長,這世上最難降的妖魔是什么坯台? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任炬丸,我火速辦了婚禮,結果婚禮上蜒蕾,老公的妹妹穿的比我還像新娘稠炬。我一直安慰自己,他們只是感情好咪啡,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布首启。 她就那樣靜靜地躺著,像睡著了一般撤摸。 火紅的嫁衣襯著肌膚如雪毅桃。 梳的紋絲不亂的頭發(fā)上褒纲,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天,我揣著相機與錄音钥飞,去河邊找鬼莺掠。 笑死,一個胖子當著我的面吹牛代承,可吹牛的內容都是我干的汁蝶。 我是一名探鬼主播渐扮,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼论悴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了墓律?” 一聲冷哼從身側響起膀估,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎耻讽,沒想到半個月后察纯,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡针肥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年饼记,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慰枕。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡具则,死狀恐怖,靈堂內的尸體忽然破棺而出具帮,到底是詐尸還是另有隱情博肋,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布蜂厅,位于F島的核電站匪凡,受9級特大地震影響,放射性物質發(fā)生泄漏掘猿。R本人自食惡果不足惜病游,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望稠通。 院中可真熱鬧衬衬,春花似錦、人聲如沸采记。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽唧龄。三九已至兼砖,卻和暖如春奸远,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背讽挟。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工懒叛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人耽梅。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓薛窥,卻偏偏與公主長得像,于是被迫代替她去往敵國和親眼姐。 傳聞我的和親對象是個殘疾皇子诅迷,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353