【擴】基于注解的參數(shù)校驗器Hibernate Validator

前言

你還在為校驗入?yún)r寫的那一串 if...else... 而苦惱嘛?
你還在為了編寫一個功能全面的參數(shù)校驗器而夜夜不寐嘛?
No~ NoNo~ NoNoNo~ No!
人生苦短,大可不必講寶貴的編程時間耗費在這些事情上~
是時候換個活法啦芥驳!有現(xiàn)成的工具怎栽,拿去用吧愚铡!
賣個萌沥寥。。柠座。(?ω?)

public class User {
    @NotBlank(message = "用戶名不能為空")
    private String name;
 
    @NotBlank(message = "ERP不能為空")
    @Size(min = 3, message = "ERP長度不能小于3")
    private String erp;
 
    @Min(value = 22, message = "年齡不能低于22歲")
    private int age;
 
    // ... getter and setter
}

看看上面這種參數(shù)校驗邑雅,多么的 優(yōu)雅~
直接通過在屬性上寫注解就可以達到給參數(shù)增加校驗規(guī)則的目的啦!


基于注解的參數(shù)校驗器

JSR303

JSR303是一套JavaBean參數(shù)校驗的 標準妈经,它定義了很多常用的校驗注解淮野,我們可以直接將這些注解加在我們JavaBean的屬性上面,就可以在需要校驗的時候進行校驗了吹泡。

image.png

ps. 沒有列舉全骤星,感興趣的可以看JSR303官方文檔

Hibernate Validator

Hibernate Validator在JSR303的基礎(chǔ)上對校驗注解進行了擴展

image.png

ps. 同樣沒列舉全爆哑,大家感興趣可以看Hibernate Validator官方文檔洞难。

Hibernate Validator就決定是你了

基本上Hibernate validator就包括了所有常用的校驗規(guī)則,而且它又是在JSR303基礎(chǔ)上的擴展揭朝,所以直接 推薦使用 Hibernate Validator了队贱。

Hibernate Validator版本

image.png

可以看到Hibernate validator目前有兩個穩(wěn)定的版本,各取所需潭袱。
這篇文章里面的例子用到的版本是 5.4.2.Final柱嫌。

Maven引用

<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-validator</artifactId>
   <version>5.4.2.Final</version>
</dependency>
image.png

代碼實現(xiàn)

對Object的屬性進行校驗

// Bean屬性上添加相應(yīng)的注解,設(shè)置校驗規(guī)則
public class User {
    @NotBlank(message = "用戶名不能為空")
    private String name;
    @NotBlank(message = "ERP不能為空")
    @Size(min = 3, message = "ERP長度不能小于3")
    private String erp;
    @Min(value = 22, message = "年齡不能低于22歲")
    private int age;
 
    // ... getter and setter
}

如上屯换,給 User 這個Bean的三個屬性增加了相應(yīng)的規(guī)則编丘,message 后面規(guī)定的是校驗不通過時報的錯誤信息文案:

  • 用戶名name字段不能為null,也不能為空字符串(會過濾空格)彤悔。
  • 用戶erp字段不能為null嘉抓,也不能為空字符串(會過濾空格),且字符串長度不能小于3蜗巧。
  • 用戶年齡age字段不能小于22掌眠。
// Controller的接口參數(shù)前面添加@Valid注解
import javax.validation.Valid;
 
@Controller
@RequestMapping(value = "/validate")
public class ValidateDemoController {
 
    @RequestMapping(value = "/user", method = RequestMethod.POST)
    @ResponseBody
    public String printValidatedUserInfo(@RequestBody @Valid User user) {
        return user.toString();
    }
 
}

如上,在Controller的接口參數(shù)(剛定義的Bean)之前添加 @Valid 注解幕屹。
這樣就可以用啦蓝丙,校驗不通過的時候會拋出異常,具體怎么處理異常就根據(jù)自己的項目需要來吧望拖。

分組校驗

有的時候渺尘,在不同場景下我們需要對同一個Bean里面的不同參數(shù)進行校驗。
比如說说敏,在新增用戶的時候我需要校驗姓名鸥跟、erp和年齡,而在修改用戶的時候我只需要校驗erp。
又或者医咨,部門A的員工的年齡不能低于22歲枫匾,無上限;而部門B的員工年齡不能高于35歲拟淮,無下限干茉。
難道我們需要根據(jù)每一個場景都增加一個Bean嘛?會不會有點太浪費很泊?
不用的角虫!這里提供了一個 分組 的概念,不同的規(guī)則可以劃分到不同的組里面委造,校驗的時候選擇相應(yīng)的組戳鹅,就會只校驗該組下面的所有規(guī)則。

// 首先定義兩個接口昏兆,作為兩個分組
public interface UserValidGroupOne {
}
 
public interface UserValidGroupTwo {
}

定義兩個接口 UserValidGroupOneUserValidGroupTwo枫虏,作為兩個分組。

// Bean參數(shù)校驗規(guī)則劃分為兩個組
public class UserByGroup {
    @NotBlank(groups = {UserValidGroupOne.class, UserValidGroupTwo.class}, message = "用戶名不能為空")
    private String name;
 
    @NotBlank(groups = {UserValidGroupOne.class, UserValidGroupTwo.class}, message = "ERP不能為空")
    @Size(min = 3, groups = {UserValidGroupOne.class, UserValidGroupTwo.class}, message = "ERP長度不能小于3")
    private String erp;
 
    @Min(value = 22, groups = {UserValidGroupOne.class}, message = "年齡不能低于22歲")
    @Max(value = 35, groups = {UserValidGroupTwo.class}, message = "年齡不能高于35歲")
    private int age;
 
    // ... getter and setter
}

屬性前寫相應(yīng)注解增加校驗規(guī)則亮垫,注解里面的 groups 屬性表示這條規(guī)則屬于哪個分組模软,不加 groups 則表示在使用 Deafault 規(guī)則時起作用。
比如上面代碼中描述的是:

  • name和erp字段的校驗規(guī)則同屬于兩個分組饮潦。
  • age字段的校驗規(guī)則燃异,在分組1中不能小于22,而在分組2中不能大于35继蜡。
//接口參數(shù)前面注明要用哪個分組的規(guī)則來進行校驗
import org.springframework.validation.annotation.Validated;
 
@Controller
@RequestMapping(value = "/validate")
public class ValidateDemoController {
     
    @RequestMapping(value = "/userByGroup1", method = RequestMethod.POST)
    @ResponseBody
    public String printValidatedUserInfoByGroupOne(@RequestBody @Validated(value = {UserValidGroupOne.class}) UserByGroup userByGroup) {
        return userByGroup.toString();
    }
 
    @RequestMapping(value = "/userByGroup2", method = RequestMethod.POST)
    @ResponseBody
    public String printValidatedUserInfoByGroupTwo(@RequestBody @Validated(value = {UserValidGroupTwo.class}) UserByGroup userByGroup) {
        return userByGroup.toString();
    }
 
}

如上回俐,在Controller的接口參數(shù)(剛定義的Bean)之前添加 @Validated 注解,注意不是 在【對Object的屬性進行校驗】時講的 @Valid 注解稀并。
@Validatedvalue 屬性上注明要使用的規(guī)則分組仅颇。
可寫多個分組,但是只有第一個分組才生效碘举。
若使用 @Valid 則表示使用 默認 校驗規(guī)則忘瓦。

自定義注解

雖然Hibernate Validator提供了基本上所有常用的校驗規(guī)則,可還是有些場景不能滿足引颈。
比如說耕皮,現(xiàn)在需要一個校驗規(guī)則,一個List中不能包含null蝙场。Hibernate Validator并沒有提供相應(yīng)注解凌停。
這時候就需要我們自定義注解,來擴展工具售滤,滿足我們自己的需求罚拟。

//自定義參數(shù)校驗注解台诗,校驗List集合中是否有null元素
import com.jd.ershou.service.impl.ListNotHasNullValidatorImpl;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
 
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
/**
 * 自定義參數(shù)校驗注解
 * 校驗 List 集合中是否有null 元素
 * Created by weixiaoyu on 2018/5/2.
 */
@Target({ANNOTATION_TYPE, METHOD, FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = ListNotHasNullValidatorImpl.class)
public @interface ListNotHasNull {
    /**
     * 添加value屬性,可以作為校驗時的條件,若不需要赐俗,可去掉此處定義
     */
    int value() default 0;
 
    String message() default "List集合中不能含有null元素";
 
    Class<?>[] groups() default {};
 
    Class<? extends Payload>[] payload() default {};
 
    /**
     * 定義List拉队,為了讓Bean的一個屬性上可以添加多套規(guī)則
     */
    @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        ListNotHasNull[] value();
    }
 
}

首先定義自定義注解 @ListNotHasNull,校驗List中不能含有null元素阻逮,注明其注解實現(xiàn)類為 ListNotHasNullValidatorImpl.class氏仗。
接下來編寫實現(xiàn)類的邏輯。

//注解@ListNotHasNull的實現(xiàn)類
import org.springframework.stereotype.Service;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.List;
 
/**
 * 自定義注解ListNotHasNull的實現(xiàn)類
 * 用于判斷List集合中是否含有null元素
 * Created by weixiaoyu on 2018/5/2.
 */
@Service
public class ListNotHasNullValidatorImpl implements ConstraintValidator<ListNotHasNull, List> {
     
    private int value;
 
    @Override
    public void initialize(ListNotHasNull constraintAnnotation) {
        // 傳入value值夺鲜,可以在校驗中使用
        this.value = constraintAnnotation.value();
    }
 
    @Override
    public boolean isValid(List list, ConstraintValidatorContext constraintValidatorContext) {
        if (list != null) {
            for (Object object : list) {
                if (object == null) {
                    //如果List集合中含有Null元素,校驗失敗
                    return false;
                }
            }
        }
        return true;
    }
 
}

自定義注解的實現(xiàn)類實現(xiàn)了 ConstraintValidator 接口呐舔。
重要的是要重寫 isValid 方法币励,其中的邏輯就是需要的校驗規(guī)則。
接下來是測試用例珊拼。

//Bean的屬性中含有List
public class Person {
    @NotBlank(message = "姓名不能為空")
    private String name;
 
    @NotBlank(message = "性別不能為空")
    private String sex;
 
    @NotEmpty(message = "家庭成員不能為空")
    @ListNotHasNull(message = "所有家庭成員信息中不能有為null的")
    @Valid // 此處加@Valid注解的原因是注明要遞歸校驗食呻,加上這個注解就會遞歸校驗List中每個元素的屬性是否符合規(guī)則
    private List<FamilyMember> familyMembers;
     
    // ... getter and setter
}

家庭成員 familyMembers 是一個List,其中每一個元素都是 FamilyMember 類澎现。
familyMembers 上增加剛才自定義的 @ListNotHasNull 注解仅胞。
ps. 此處加 @Valid 注解的原因是注明要遞歸校驗,加上這個注解就會遞歸校驗List中每個元素 FamilyMember 的屬性是否符合其內(nèi)部定義規(guī)則剑辫。

//Controller的接口參數(shù)前面添加@Valid注解
import javax.validation.Valid;
 
@Controller
@RequestMapping(value = "/validate")
public class ValidateDemoController {
 
    @RequestMapping(value = "/person", method = RequestMethod.POST)
    @ResponseBody
    public String printValidatedPersonInfo(@RequestBody @Valid Person person) {
        return person.toString();
    }
 
}

同樣干旧,在Controller的接口參數(shù)(剛定義的Bean)之前添加 @Valid 注解,校驗不通過則拋出異常妹蔽。

Spring validator方法級別的校驗

JSR和Hibernate Validator的校驗只能對Object的屬性進行校驗椎眯,不能對單個的參數(shù)進行校驗。
Spring在此基礎(chǔ)上進行了擴展胳岂,添加了 MethodValidationPostProcessor 攔截器编整,可以實現(xiàn)對方法參數(shù)的校驗。
首先需要將 MethodValidationPostProcessor 注入到Spring容器中乳丰。

//注入到Spring容器中
@Configuration
public class PpValidatorBeanConfigurer {
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

這里是通過Java Bean的方式掌测,用 @Bean 注解,將 MethodValidationPostProcessor 注入到Spring容器中的产园。
也可以通過配置 xml 文件的方式汞斧,根據(jù)項目的需要選擇使用。

//在Controller類上添加@Validated注解淆两,在接口方法的每一個參數(shù)前面添加相應(yīng)校驗規(guī)則注解
@Controller
@RequestMapping(value = "/validate")
@Validated
public class ValidateMethodController {
 
    @RequestMapping(value = "/param", method = RequestMethod.GET)
    @ResponseBody
    public String printValidatedParam(
            @NotBlank(message = "用戶名不能為空") String name,
            @Size(min = 3, message = "ERP長度不能小于3") String erp,
            @Min(value = 22, message = "年齡不能低于22歲") int age) {
 
        String msg = "name=" + name + ", erp=" + erp + ", age=" + age;
        return msg;
    }
 
}

需要在Controller類的上方添加 @Validated 注解断箫,然后在接口方法的每一個參數(shù)前面添加相應(yīng)的校驗規(guī)則注解。
這樣方法級別的校驗就比較 靈活 了不是~

異常處理

之前一直在說秋冰,校驗不通過的時候會 拋出異常仲义,具體異常如何處理請根據(jù)自己的項目需要來。
但具體都拋出哪些異常,如何處理埃撵,我這邊在寫測試用例的時候捕獲到了如下 三類 異常:

org.springframework.validation.BindException org.springframework.web.bind.MethodArgumentNotValidException
javax.validation.ConstraintViolationException

推薦 使用 @ControllerAdvice 搭配 @ExceptionHandler 來捕獲Controller層拋出來的異常赵颅。
寫了簡單的處理邏輯,僅供參考暂刘。

//異常處理
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Set;
 
/**
 * Created by weixiaoyu on 2018/5/2.
 */
@ControllerAdvice
public class PpValidatorExceptionHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(PpValidatorExceptionHandler.class);
 
    @ExceptionHandler(Throwable.class)
    @ResponseBody
    public String handleThrowableException(Throwable ex) {
        LOGGER.error("PpValidatorExceptionHandler.handleThrowableException", ex);
        String msg = "Throwable error: " + ex.toString();
        return msg;
    }
 
    @ExceptionHandler(BindException.class)
    @ResponseBody
    public String handleBindException(BindException e1) {
        LOGGER.error("PpValidatorExceptionHandler.handleBindException", e1);
        List<ObjectError> errors = e1.getAllErrors();
        StringBuilder stringBuilder = new StringBuilder();
        for (ObjectError error : errors) {
            if (stringBuilder.length() != 0) {
                stringBuilder.append("饺谬,");
            }
            stringBuilder.append(error.getDefaultMessage());
        }
        String msg ="BindException error: " + stringBuilder.toString();
        return msg;
    }
 
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseBody
    public String handleMethodArgumentNotValidException(MethodArgumentNotValidException e2) {
        List<ObjectError> errors = e2.getBindingResult().getAllErrors();
        StringBuilder stringBuilder = new StringBuilder();
        for (ObjectError error : errors) {
            if (stringBuilder.length() != 0) {
                stringBuilder.append(",");
            }
            stringBuilder.append(error.getDefaultMessage());
        }
        String msg ="MethodArgumentNotValidException error: " + stringBuilder.toString();
        return msg;
    }
 
    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseBody
    public String handleConstraintViolationException(ConstraintViolationException e3) {
        Set<ConstraintViolation<?>> violations = e3.getConstraintViolations();
        StringBuilder stringBuilder = new StringBuilder();
        for (ConstraintViolation<?> violation : violations) {
            if (stringBuilder.length() != 0) {
                stringBuilder.append("谣拣,");
            }
            stringBuilder.append(violation.getMessage());
        }
        String msg ="ConstraintViolationException error: " + stringBuilder.toString();
        return msg;
    }
 
}

結(jié)語

安利了這么多募寨,覺得好使不?
那必須好使啊~~~
部門在我的安利下所有新項目都在用 Hibernate Validator 參數(shù)校驗器森缠,經(jīng)過了多種線上環(huán)境驗證拔鹰。大家就放心去用吧~
最后,有問題的話咱們共同探討~ 共同學習~ 共同進步哈~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贵涵,一起剝皮案震驚了整個濱河市列肢,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌宾茂,老刑警劉巖瓷马,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異跨晴,居然都是意外死亡欧聘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門坟奥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來树瞭,“玉大人,你說我怎么就攤上這事爱谁∩古纾” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵访敌,是天一觀的道長凉敲。 經(jīng)常有香客問我,道長寺旺,這世上最難降的妖魔是什么爷抓? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮阻塑,結(jié)果婚禮上蓝撇,老公的妹妹穿的比我還像新娘。我一直安慰自己陈莽,他們只是感情好渤昌,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布虽抄。 她就那樣靜靜地躺著,像睡著了一般独柑。 火紅的嫁衣襯著肌膚如雪迈窟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天忌栅,我揣著相機與錄音车酣,去河邊找鬼。 笑死索绪,一個胖子當著我的面吹牛湖员,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播瑞驱,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼破衔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了钱烟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤嫡丙,失蹤者是張志新(化名)和其女友劉穎拴袭,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體曙博,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡拥刻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了父泳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片般哼。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖惠窄,靈堂內(nèi)的尸體忽然破棺而出蒸眠,到底是詐尸還是另有隱情,我是刑警寧澤杆融,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布楞卡,位于F島的核電站,受9級特大地震影響脾歇,放射性物質(zhì)發(fā)生泄漏蒋腮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一藕各、第九天 我趴在偏房一處隱蔽的房頂上張望池摧。 院中可真熱鬧,春花似錦激况、人聲如沸作彤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宦棺。三九已至瓣距,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間代咸,已是汗流浹背蹈丸。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留呐芥,地道東北人逻杖。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像思瘟,于是被迫代替她去往敵國和親荸百。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

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

  • Web系統(tǒng)中滨攻,校驗是必不可少的環(huán)節(jié)够话,校驗一般分為前端校驗和后端校驗,前端校驗一般使用腳本語言光绕,對即將要提交的數(shù)據(jù)進...
    LeaveStyle閱讀 3,014評論 0 0
  • 翻譯:叩丁狼教育吳嘉俊 [譯者注:這篇文章是開源項目CUBA Platform的作者女嘲,在這篇文章中,作者闡述了CU...
    叩丁狼教育閱讀 3,539評論 1 34
  • 在寫程序的時候經(jīng)常需要進行數(shù)據(jù)校驗诞帐,比如服務(wù)端對http請求參數(shù)校驗欣尼,數(shù)據(jù)入庫時對字段長度進行校驗,接口參數(shù)校驗停蕉,...
    dayspring閱讀 9,832評論 0 9
  • 前言 ??本篇文章主要簡單了解下Spring中一些JSR規(guī)范所提供的注解愕鼓,所謂JSR規(guī)范,是Java Specif...
    騎著烏龜去看海閱讀 1,021評論 0 3
  • 流水賬 起來第一件事情就是修車慧起。老爸懷疑是離合器的問題菇晃,然而修理廠的師傅開出去溜了一圈,很肯定是動力問題:離合器有...
    貍貍的守護者閱讀 121評論 0 0