SpringBoot + Validator 參數(shù)校驗配置 - - - [深度]

前言

本文Spring版本為 SpringBoot-2.0.7票腰,所有源碼相關(guān)類、方法封救、代碼行都以此版本為基礎(chǔ)拇涤。
代碼行數(shù): 使用 IDEA 的同學(xué)通過Maven Projects -> Donwload Sources and Documentation下載源碼及注釋文檔,保證行數(shù)的準確誉结。

非常歡迎您指正在文章中出現(xiàn)的錯誤鹅士,包括但不限于 語句錯誤、描述錯誤惩坑、示例錯誤掉盅、代碼理解錯誤。

參數(shù)校驗是代碼開發(fā)中必不可少的一環(huán)以舒,一個方法中參數(shù)校驗套了一個又一個 if-else趾痘,繁瑣的操作讓廣大程序員詬病。

本文我們就講一下 SpringBoot 結(jié)合 Hibernate-Validtor 校驗參數(shù)蔓钟、簡化工作永票。

開始

spring-boot-starter-web 已經(jīng)默認整合、提供了 Hibernate-Validator 的功能滥沫,只待我們?nèi)ナ褂谩?/p>

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

創(chuàng)建一個實體類侣集,并添加校驗注解。

本小節(jié)只做簡單的使用演示兰绣。
常用注解列表世分、注解說明、注解用法缀辩,以及·自定義校驗注解·的教程罚攀。JSR 303 - Bean Validation 介紹及最佳實踐

public class Student{
    @NotNull
    private String name;
    @NotNull
    private String sex;
    @Min(0)
    @Max(150)
    private int age;
    
    ...get,set...
}

接著編寫 Controller 代碼。

// @RestController
// DemoController

    @GetMapping("/student")
    public String validator(@Validated Student student, BindingResult result) {
        if (result.hasErrors()) {
            return result.getFieldError().getDefaultMessage();
        }
        return "ok";
    }

啟動程序后訪問http://{host:prot}/student雌澄,將會返回:

must not be null

訪問http://{host:prot}/student?name=zhangsan&sex=Male&age=22斋泄,將會返回:

ok

到這,本期的教程結(jié)束...是不可能的镐牺。

進階

上面的教程還太簡單炫掐,很多事情都很朦朧。

  1. 書寫有沒有什么規(guī)則睬涧?
  2. 我怎么知道‘must not be null’是指哪個參數(shù)募胃?跟沒提示一樣。
  3. 每個 Controller 方法都要判斷 BindingResult 還是好麻煩畦浓!我懶得寫痹束!
  4. 校驗規(guī)則太少了,能不能自己寫規(guī)則讶请?
  5. 我想手動校驗怎么辦祷嘶?

書寫規(guī)則

@Validated 和 @Valid 的異同

@Validated 是 Spring 實現(xiàn)的JSR-303的變體 @Valid ,支持驗證組的規(guī)范。 設(shè)計用于方便使用Spring的JSR-303支持论巍,但不支持JSR-303特定烛谊。

@Valid JSR-303標準實現(xiàn)的校驗注解。

注解 范圍 嵌套 校驗組
@Validated 可以標記類嘉汰、方法丹禀、方法參數(shù),不能用在成員屬性(字段)上 不支持 支持
@Valid 可以標記方法鞋怀、構(gòu)造函數(shù)双泪、方法參數(shù)和成員屬性(字段)上 支持 不支持

兩者都可以用在方法入?yún)⑸希紵o法單獨提供嵌套驗證功能密似,都能配合嵌套驗證注解@Valid進行嵌套驗證焙矛。

嵌套驗證示例:

public class ClassRoom{
    @NotNull
    String name;
    
    @Valid  // 嵌套校驗,校驗參數(shù)內(nèi)部的屬性
    @NotNull
    Student student;
}
    @GetMapping("/room")   // 此處可使用 @Valid 或 @Validated, 將會進行嵌套校驗
    public String validator(@Validated ClassRoom classRoom, BindingResult result) {
        if (result.hasErrors()) {
            return result.getFieldError().getDefaultMessage();
        }
        return "ok";
    }

參考:@Validated和@Valid區(qū)別---CSDN:花郎徒結(jié)

BindingResult 的使用

BindingResult必須跟在被校驗參數(shù)之后,若被校驗參數(shù)之后沒有BindingResult對象辛友,將會拋出BindException薄扁。

    @GetMapping("/room")
    public String validator(@Validated ClassRoom classRoom, BindingResult result) {
        if (result.hasErrors()) {
            return result.getFieldError().getDefaultMessage();
        }
        return "ok";
    }

不要使用 BindingResult 接收,String等簡單對象的錯誤信息废累。簡單對象校驗失敗邓梅,會拋出 ConstraintViolationException
主要就是接不著邑滨,你要寫也算是沒關(guān)系...

    // ? 錯誤用法日缨,也沒有特別的錯,只是 result 是接不到值掖看。
    @GetMapping("/room")
    @Validated  // 啟用校驗
    public String validator(@NotNull String name, BindingResult result) {
        if (result.hasErrors()) {
            return result.getFieldError().getDefaultMessage();
        }
        return "ok";
    }

修改校驗失敗的提示信息

可以通過各個校驗注解的message屬性設(shè)置更友好的提示信息匣距。

public class ClassRoom{
    @NotNull(message = "Classroom name must not be null")
    String name;
    
    @Valid
    @NotNull
    Student student;
}
    @GetMapping("/room")
    @Validated
    public String validator(ClassRoom classRoom, BindingResult result, @NotNull(message = "姓名不能為空") String name) {
        if (result.hasErrors()) {
            return result.getFieldError().getDefaultMessage();
        }
        return "ok";
    }

message屬性配置國際化的消息也可以的,message中填寫國際化消息的code哎壳,在拋出異常時根據(jù)code處理一下就好了毅待。

    @GetMapping("/room")
    @Validated
    public String validator(@NotNull(message = "demo.message.notnull") String name) {
        if (result.hasErrors()) {
            return result.getFieldError().getDefaultMessage();
        }
        return "ok";
    }
// message_zh_CN.properties
demo.message.notnull=xxx消息不能為空

// message_en_US.properties
demo.message.notnull=xxx message must no be null

省略 Controller 中的校驗判斷

可以利用參數(shù)校驗失敗后拋出異常這點,配置·統(tǒng)一異常攔截·归榕,進行異常統(tǒng)一的處理尸红,合理的將錯誤信息返回給前端。

拋磚(僅做示例):

// @RestControllerAdvice

    /*  數(shù)據(jù)校驗處理 */
    @ExceptionHandler({BindException.class, ConstraintViolationException.class})
    public String validatorExceptionHandler(Exception e) {
        String msg = e instanceof BindException ? msgConvertor(((BindException) e).getBindingResult())
            : msgConvertor(((ConstraintViolationException) e).getConstraintViolations());

        return msg;
    }

    /**
     * 校驗消息轉(zhuǎn)換拼接
     *
     * @param bindingResult
     * @return
     */
    public static String msgConvertor(BindingResult bindingResult) {
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        StringBuilder sb = new StringBuilder();
        fieldErrors.forEach(fieldError -> sb.append(fieldError.getDefaultMessage()).append(","));

        return sb.deleteCharAt(sb.length() - 1).toString().toLowerCase();
    }

    private String msgConvertor(Set<ConstraintViolation<?>> constraintViolations) {
        StringBuilder sb = new StringBuilder();
        constraintViolations.forEach(violation -> sb.append(violation.getMessage()).append(","));

        return sb.deleteCharAt(sb.length() - 1).toString().toLowerCase();
    }

注:getMessagegetDefaultMessage 都是直接獲取注解上message屬性的值刹泄,

擴展校驗注解外里、校驗規(guī)則

常用注解列表、注解說明特石、注解用法盅蝗,以及·自定義校驗注解·的教程。JSR 303 - Bean Validation 介紹及最佳實踐

手動校驗

若沒有手動配置Validator對象姆蘸,自然需要從 Spring 容器中獲取校驗器對象墩莫,注入使用芙委。

此處給出一個手動校驗的工具類,供大家參考贼穆。(lay了...寫的自閉题山,如果對代碼有疑問請聯(lián)系我..持續(xù)更新)

代碼中提到的與 Spring 集成兰粉,主要是對代碼返回值的統(tǒng)一故痊。(不支持普通對象...)
若都以注解的message屬性來獲取提示消息,可以刪除 Spring 相關(guān)的代碼玖姑。
若不以message屬性作為消息愕秫,那么可以從bindingResult中獲取字段、類焰络、注解信息戴甩,拼裝成消息碼。

拋磚:

// config
// @Configuration

    @Bean
    public Validator validator() {
        return ValidatorUtils.getValidator();
    }
import org.hibernate.validator.HibernateValidator;
import org.springframework.util.ClassUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.DataBinder;
import org.springframework.validation.SmartValidator;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;

/**
 * hibernate-validator校驗工具類
 */
public class ValidatorUtils {
    private static Validator validator;
    private static SmartValidator validatorAdapter;

    static {
        // 快速返回模式
        validator = Validation.byProvider(HibernateValidator.class)
            .configure()
            .failFast(true)
            .buildValidatorFactory()
            .getValidator();
    }

    public static Validator getValidator() {
        return validator;
    }

    private static SmartValidator getValidatorAdapter(Validator validator) {
        if (validatorAdapter == null) {
            validatorAdapter = new SpringValidatorAdapter(validator);
        }
        return validatorAdapter;
    }

    /**
     * 校驗參數(shù)闪彼,用于普通參數(shù)校驗 [未測試甜孤!]
     *
     * @param
     */
    public static void validateParams(Object... params) {
        Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(params);

        if (!constraintViolationSet.isEmpty()) {
            throw new ConstraintViolationException(constraintViolationSet);
        }
    }

    /**
     * 校驗對象
     *
     * @param object
     * @param groups
     * @param <T>
     */
    public static <T> void validate(T object, Class<?>... groups) {
        Set<ConstraintViolation<T>> constraintViolationSet = validator.validate(object, groups);

        if (!constraintViolationSet.isEmpty()) {
            throw new ConstraintViolationException(constraintViolationSet);
        }
    }

    /**
     * 校驗對象
     * 使用與 Spring 集成的校驗方式。
     * 
     * @param object 待校驗對象
     * @param groups 待校驗的組
     * @throws BindException
     */
    public static <T> void validateBySpring(T object, Class<?>... groups)
        throws BindException {
        DataBinder dataBinder = getBinder(object);
        dataBinder.validate((Object[]) groups);

        if (dataBinder.getBindingResult().hasErrors()) {
            throw new BindException(dataBinder.getBindingResult());
        }
    }

    private static <T> DataBinder getBinder(T object) {
        DataBinder dataBinder = new DataBinder(object, ClassUtils.getShortName(object.getClass()));
        dataBinder.setValidator(getValidatorAdapter(validator));
        return dataBinder;
    }

}
源碼經(jīng)驗寶寶[拓展]
為什么 BindingResult 接收不到簡單對象的校驗信息畏腕?

跟進 Spring MVC 源碼缴川,發(fā)現(xiàn):SpringMVC 在進行方法參數(shù)的注入(將 Http請求參數(shù)封裝成方法所需的參數(shù))時,不同的對象使用不同的解析器注入對象描馅。

聽著好像沒什么關(guān)系把夸。但其實就是,注入實體對象時使用ModelAttributeMethodProcessor中的校驗方法铭污,而注入 String 對象使用AbstractNamedValueMethodArgumentResolver中的校驗方法恋日。正是這個差異導(dǎo)致了BindingResult無法接受到簡單對象(簡單的入?yún)?shù)類型)的校驗信息。

班谀岂膳?你問我什么是簡單對象?emm...
八大基礎(chǔ)類型再加上不同解析器支持的類型對象(不同的參數(shù)類型)磅网,需要看各解析器實現(xiàn)的supportsParameter()方法谈截,文中提到的簡單對象,意思是ModelAttributeMethodProcessor不支持的所有對象知市。

獲取參數(shù)注入解析器的源碼位于HandlerMethodArgumentResolverComposite#resolveArgument():120:

    // HandlerMethodArgumentResolverComposite.class
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        // 獲取 parameter 參數(shù)的解析器
        HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
        // 調(diào)用解析器獲取參數(shù)
        return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    }
    
    // 獲取 parameter 參數(shù)的解析器
    private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
        // 從緩存中獲取參數(shù)對應(yīng)的解析器
        HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
        for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
            // 解析器是否支持該參數(shù)類型
            if (methodArgumentResolver.supportsParameter(parameter)) {
                result = methodArgumentResolver;
                this.argumentResolverCache.put(parameter, result);
                break;
            }
        }
        return result;
    }

注入 String 參數(shù)時傻盟,在AbstractNamedValueMethodArgumentResolver#resolveArgument()中,不會拋出BindException/ConstraintViolationException異常嫂丙、也不會將 BindingResult 傳入到方法中娘赴。

注入對象時在ModelAttributeMethodProcessor#resolveArgument():154 行的 validateIfApplicable(binder, parameter)語句,進行了參數(shù)校驗,校驗不通過并且實體對象后不存在BindingResult對象跟啤,則會在this#resolveArgument():156拋出BindException诽表。

public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
            
        // bean 參數(shù)綁定和校驗
        WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
        
        // 參數(shù)校驗
        validateIfApplicable(binder, parameter);
        // 校驗結(jié)果包含錯誤唉锌,并且該對象后不存在 BindingResult 對象,就拋出異常
        if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
            throw new BindException(binder.getBindingResult());
        }

        // 在對象后注入 BindingResult 對象
        Map<String, Object> bindingResultModel = bindingResult.getModel();
        mavContainer.removeAttributes(bindingResultModel);
        mavContainer.addAllAttributes(bindingResultModel);
    }
在哪里拋出ConstraintViolationException竿奏?

可能有同學(xué)發(fā)現(xiàn)了袄简,簡單對象注入后并沒有拋出異常,那這個參數(shù)在哪里被校驗?zāi)兀?/p>

被方法級的攔截器攔住了泛啸。

這里的方法攔截器是 MethodValidationInterceptor:

// MethodValidationInterceptor.class

public Object invoke(MethodInvocation invocation) throws Throwable {
        ExecutableValidator execVal = this.validator.forExecutables();
        // 校驗參數(shù)
        try {
            result = execVal.validateParameters(
                    invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        catch (IllegalArgumentException ex) {
            // 解決參數(shù)錯誤異常绿语、再次校驗
            methodToValidate = BridgeMethodResolver.findBridgedMethod(
                    ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
            result = execVal.validateParameters(
                    invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        
        // 執(zhí)行結(jié)果
        Object returnValue = invocation.proceed();
        
        // 校驗返回值
        result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }

        return returnValue;
    }

over.
本文到此結(jié)束。


非常歡迎您指正在文章中出現(xiàn)的錯誤候址,包括但不限于 語句錯誤吕粹、描述錯誤、示例錯誤岗仑、代碼理解錯誤匹耕。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市荠雕,隨后出現(xiàn)的幾起案子稳其,更是在濱河造成了極大的恐慌,老刑警劉巖炸卑,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件既鞠,死亡現(xiàn)場離奇詭異,居然都是意外死亡矾兜,警方通過查閱死者的電腦和手機损趋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來椅寺,“玉大人浑槽,你說我怎么就攤上這事》蹬粒” “怎么了桐玻?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長荆萤。 經(jīng)常有香客問我镊靴,道長,這世上最難降的妖魔是什么链韭? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任偏竟,我火速辦了婚禮,結(jié)果婚禮上敞峭,老公的妹妹穿的比我還像新娘踊谋。我一直安慰自己,他們只是感情好旋讹,可當(dāng)我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布殖蚕。 她就那樣靜靜地躺著轿衔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪睦疫。 梳的紋絲不亂的頭發(fā)上害驹,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天,我揣著相機與錄音蛤育,去河邊找鬼宛官。 笑死,一個胖子當(dāng)著我的面吹牛缨伊,可吹牛的內(nèi)容都是我干的摘刑。 我是一名探鬼主播进宝,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼刻坊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了党晋?” 一聲冷哼從身側(cè)響起谭胚,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎未玻,沒想到半個月后灾而,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡扳剿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年旁趟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片庇绽。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡锡搜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瞧掺,到底是詐尸還是另有隱情耕餐,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布辟狈,位于F島的核電站肠缔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏哼转。R本人自食惡果不足惜明未,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望壹蔓。 院中可真熱鬧趟妥,春花似錦、人聲如沸庶溶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至行疏,卻和暖如春匆光,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酿联。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工终息, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贞让。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓周崭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親喳张。 傳聞我的和親對象是個殘疾皇子续镇,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,435評論 2 359

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