Spring Boot 使用 JSR303 實現(xiàn)參數驗證

文章首發(fā)于公眾號《程序員果果》:
文章地址:https://mp.weixin.qq.com/s/Wq1pHIExP6WKUFMV5EJ_sg
本篇源碼:https://github.com/gf-huanchupk/SpringBootLearning

簡介

JSR-303 是 JAVA EE 6 中的一項子規(guī)范,叫做 Bean Validation坐桩。

在任何時候临谱,當你要處理一個應用程序的業(yè)務邏輯,數據校驗是你必須要考慮和面對的事情。應用程序必須通過某種手段來確保輸入進來的數據從語義上來講是正確的见妒。在通常的情況下供搀,應用程序是分層的,不同的層由不同的開發(fā)人員來完成止后。很多時候同樣的數據驗證邏輯會出現(xiàn)在不同的層瞎惫,這樣就會導致代碼冗余和一些管理的問題,比如說語義的一致性等译株。為了避免這樣的情況發(fā)生瓜喇,最好是將驗證邏輯與相應的域模型進行綁定。

Bean Validation 為 JavaBean 驗證定義了相應的元數據模型和 API歉糜。缺省的元數據是 Java Annotations欠橘,通過使用 XML 可以對原有的元數據信息進行覆蓋和擴展。在應用程序中现恼,通過使用 Bean Validation 或是你自己定義的 constraint肃续,例如 @NotNull, @Max, @ZipCode, 就可以確保數據模型(JavaBean)的正確性叉袍。constraint 可以附加到字段始锚,getter 方法,類或者接口上面喳逛。對于一些特定的需求瞧捌,用戶可以很容易的開發(fā)定制化的 constraint。Bean Validation 是一個運行時的數據驗證框架润文,在驗證之后驗證的錯誤信息會被馬上返回姐呐。

Bean Validation 規(guī)范內嵌的約束注解

實例

基本應用

引入依賴

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

給參數對象添加校驗注解

@Data
public class User {
    
    private Integer id;
    @NotBlank(message = "用戶名不能為空")
    private String username;
    @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$", message = "密碼必須為8~16個字母和數字組合")
    private String password;
    @Email
    private String email;
    private Integer gender;

}

Controller 中需要校驗的參數Bean前添加 @Valid 開啟校驗功能,緊跟在校驗的Bean后添加一個BindingResult典蝌,BindingResult封裝了前面Bean的校驗結果曙砂。

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

    @PostMapping("")
    public Result save (@Valid User user , BindingResult bindingResult)  {
        if (bindingResult.hasErrors()) {
            Map<String , String> map = new HashMap<>();
            bindingResult.getFieldErrors().forEach( (item) -> {
                String message = item.getDefaultMessage();
                String field = item.getField();
                map.put( field , message );
            } );
            return Result.build( 400 , "非法參數 !" , map);
        }
        return Result.ok();
    }

}

測試如下:

異常的統(tǒng)一處理

參數校驗不通過時,會拋出 BingBindException 異常骏掀,可以在統(tǒng)一異常處理中鸠澈,做統(tǒng)一處理柱告,這樣就不用在每個需要參數校驗的地方都用 BindingResult 獲取校驗結果了。

@Slf4j
@RestControllerAdvice(basePackages = "com.itwolfed.controller")
public class GlobalExceptionControllerAdvice {

    @ExceptionHandler(value= {MethodArgumentNotValidException.class , BindException.class})
    public Result handleVaildException(Exception e){
        BindingResult bindingResult = null;
        if (e instanceof MethodArgumentNotValidException) {
            bindingResult = ((MethodArgumentNotValidException)e).getBindingResult();
        } else if (e instanceof BindException) {
            bindingResult = ((BindException)e).getBindingResult();
        }
        Map<String,String> errorMap = new HashMap<>(16);
        bindingResult.getFieldErrors().forEach((fieldError)->
                errorMap.put(fieldError.getField(),fieldError.getDefaultMessage())
        );
        return Result.build(400 , "非法參數 !" , errorMap);
    }

}

分組解決校驗

新增和修改對于實體的校驗規(guī)則是不同的笑陈,例如id是自增的時际度,新增時id要為空,修改則必須不為空涵妥;新增和修改乖菱,若用的恰好又是同一種實體,那就需要用到分組校驗蓬网。

校驗注解都有一個groups屬性窒所,可以將校驗注解分組,我們看下@NotNull的源碼:

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(List.class)
@Documented
@Constraint(validatedBy = { })
public @interface NotNull {

    String message() default "{javax.validation.constraints.NotNull.message}";

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

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

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {

        NotNull[] value();
    }
}

從源碼可以看出 groups 是一個Class<?>類型的數組拳缠,那么就可以創(chuàng)建一個Groups.

public class Groups {
    public interface Add{}
    public interface  Update{}
}

給參數對象的校驗注解添加分組

@Data
public class User {

    @Null(message = "新增不需要指定id" , groups = Groups.Add.class)
    @NotNull(message = "修改需要指定id" , groups = Groups.Update.class)
    private Integer id;
    @NotBlank(message = "用戶名不能為空")
    @NotNull
    private String username;
    @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$", message = "密碼必須為8~16個字母和數字組合")
    private String password;
    @Email
    private String email;
    private Integer gender;

}

Controller 中原先的@Valid不能指定分組 墩新,需要替換成@Validated

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

    @PostMapping("")
    public Result save (@Validated(Groups.Add.class) User user)  {
        return Result.ok();
    }

}

測試如下:

自定義校驗注解

雖然JSR303和springboot-validator 已經提供了很多校驗注解,但是當面對復雜參數校驗時窟坐,還是不能滿足我們的要求海渊,這時候我們就需要 自定義校驗注解。

例如User中的gender哲鸳,用 1代表男 2代表女臣疑,我們自定義一個校驗注解@ListValue,指定取值只能1和2徙菠。

創(chuàng)建約束規(guī)則

@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface ListValue {
    String message() default "";

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

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

    int[] vals() default { };
}

一個標注(annotation) 是通過@interface關鍵字來定義的. 這個標注中的屬性是聲明成類似方法
的樣式的. 根據Bean Validation API 規(guī)范的要求:

  • message屬性, 這個屬性被用來定義默認得消息模版, 當這個約束條件被驗證失敗的時候,通過
    此屬性來輸出錯誤信息讯沈。
  • groups 屬性, 用于指定這個約束條件屬于哪(些)個校驗組. 這個的默認值必須是Class<?>類型數組。
  • payload 屬性, Bean Validation API 的使用者可以通過此屬性來給約束條件指定嚴重級別. 這個屬性并不被API自身所使用婿奔。

除了這三個強制性要求的屬性(message, groups 和 payload) 之外, 我們還添
加了一個屬性用來指定所要求的值. 此屬性的名稱vals在annotation的定義中比較特
殊, 如果只有這個屬性被賦值了的話, 那么, 在使用此annotation到時候可以忽略此屬性名稱.

另外, 我們還給這個annotation標注了一些元標注( meta
annotatioins):

  • @Target({ METHOD, FIELD, ANNOTATION_TYPE }): 表示此注解可以被用在方法, 字段或者
    annotation聲明上缺狠。
  • @Retention(RUNTIME): 表示這個標注信息是在運行期通過反射被讀取的.
  • @Constraint(validatedBy = ListValueConstraintValidator.class): 指明使用哪個校驗器(類) 去校驗使用了此標注的元素.
  • @Documented: 表示在對使用了該注解的類進行javadoc操作到時候, 這個標注會被添加到
    javadoc當中.

創(chuàng)建約束校驗器

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

    private Set<Integer> set = new HashSet<>();
    /**
     * 初始化方法
     */
    @Override
    public void initialize(ListValue constraintAnnotation) {

        int[] vals = constraintAnnotation.vals();
        for (int val : vals) {
            set.add(val);
        }

    }

    /**
     * 判斷是否校驗成功
     *
     * @param value 需要校驗的值
     * @param context
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {

        return set.contains(value);
    }
}

ListValueConstraintValidator定義了兩個泛型參數, 第一個是這個校驗器所服務到標注類型(在我們的例子中即ListValue), 第二個這個校驗器所支持到被校驗元素的類型 (即Integer)。

如果一個約束標注支持多種類型的被校驗元素的話, 那么需要為每個所支持的類型定義一個ConstraintValidator,并且注冊到約束標注中萍摊。

這個驗證器的實現(xiàn)就很平常了, initialize() 方法傳進來一個所要驗證的標注類型的實例, 在本
例中, 我們通過此實例來獲取其vals屬性的值,并將其保存為Set集合中供下一步使
用挤茄。

isValid()是實現(xiàn)真正的校驗邏輯的地方, 判斷一個給定的int對于@ListValue這個約束條件來說
是否是合法的。

在參數對象中使用@ListValue注解冰木。

@Data
public class User {

    @Null(message = "新增不需要指定id" , groups = Groups.Add.class)
    @NotNull(message = "修改需要指定id" , groups = Groups.Update.class)
    private Integer id;
    @NotBlank(message = "用戶名不能為空")
    @NotNull
    private String username;
    @Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$", message = "密碼必須為8~16個字母和數字組合")
    private String password;
    @Email
    private String email;
    @ListValue( message = "性別應指定相應的值" , vals = {1,2} , groups = {Groups.Add.class , Groups.Update.class})
    private Integer gender;

}

測試如下:

源碼地址

https://github.com/gf-huanchupk/SpringBootLearning

參考

https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/index.html
https://docs.jboss.org/hibernate/validator/4.3/reference/zh-CN/pdf/hibernate_validator_reference.pdf

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末穷劈,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子踊沸,更是在濱河造成了極大的恐慌歇终,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逼龟,死亡現(xiàn)場離奇詭異评凝,居然都是意外死亡,警方通過查閱死者的電腦和手機审轮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門肥哎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辽俗,“玉大人疾渣,你說我怎么就攤上這事篡诽。” “怎么了榴捡?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵杈女,是天一觀的道長。 經常有香客問我吊圾,道長达椰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任项乒,我火速辦了婚禮啰劲,結果婚禮上,老公的妹妹穿的比我還像新娘檀何。我一直安慰自己蝇裤,他們只是感情好,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布频鉴。 她就那樣靜靜地躺著栓辜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪垛孔。 梳的紋絲不亂的頭發(fā)上藕甩,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機與錄音周荐,去河邊找鬼狭莱。 笑死,一個胖子當著我的面吹牛概作,可吹牛的內容都是我干的腋妙。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼仆嗦,長吁一口氣:“原來是場噩夢啊……” “哼辉阶!你這毒婦竟也來了?” 一聲冷哼從身側響起瘩扼,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤谆甜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后集绰,有當地人在樹林里發(fā)現(xiàn)了一具尸體规辱,經...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年栽燕,在試婚紗的時候發(fā)現(xiàn)自己被綠了罕袋。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片改淑。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖浴讯,靈堂內的尸體忽然破棺而出朵夏,到底是詐尸還是另有隱情,我是刑警寧澤榆纽,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布仰猖,位于F島的核電站,受9級特大地震影響奈籽,放射性物質發(fā)生泄漏饥侵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一衣屏、第九天 我趴在偏房一處隱蔽的房頂上張望躏升。 院中可真熱鬧,春花似錦狼忱、人聲如沸膨疏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽成肘。三九已至,卻和暖如春斧蜕,著一層夾襖步出監(jiān)牢的瞬間双霍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工批销, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留洒闸,地道東北人。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓均芽,卻偏偏與公主長得像丘逸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子掀宋,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容