01.spring-mvc 結(jié)合 Java Validation Api 提升web開(kāi)發(fā)效能

一今艺、痛點(diǎn)韵丑?

在應(yīng)用程序的業(yè)務(wù)邏輯中,經(jīng)常會(huì)碰到參數(shù)校驗(yàn)的情況虚缎,通常我們使用spring-mvc來(lái)接收用戶請(qǐng)求數(shù)據(jù)一般會(huì)封裝成一個(gè)Bean撵彻,需要校驗(yàn)字段值是否空,長(zhǎng)度遥巴,枚舉格式等情況下千康,如果使用SringUtils或者if判斷來(lái)解決,代碼會(huì)閱讀不友好铲掐,維護(hù)成本大拾弃,代碼冗余。 因此有了JSR 303.

Bean Validation為JavaBean提供了相應(yīng)的API來(lái)給我們做參數(shù)的驗(yàn)證摆霉。通過(guò)Bean Validation比如@NotNull @Pattern等方法來(lái)對(duì)我們字段的值做進(jìn)一步的教研豪椿。

Bean Validation 是一個(gè)運(yùn)行時(shí)框架,在驗(yàn)證之后錯(cuò)誤信息會(huì)直接返回携栋。

二搭盾、使用

1. 添加maven依賴

<!--添加依賴-->
<dependency>
 <groupId>javax.validation</groupId>
 <artifactId>validation-api</artifactId>
 <version>2.0.1.Final</version>
</dependency>

注: hibernate-validator 擴(kuò)展了些自定義的validator可供參考。

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.2.0.Final</version>
</dependency>

2. 常用注解說(shuō)明

  • constraints 包下邊即定義好的注解婉支,可直接使用
注解 用途
AssertFalse 用于boolean字段鸯隅,該字段的值只能為false
AssertTrue 用于boolean字段,該字段只能為true
DecimalMax(value) 被注釋的元素必須是一個(gè)數(shù)字向挖,只能大于或等于該值
DecimalMin(value) 被注釋的元素必須是一個(gè)數(shù)字蝌以,只能小于或等于該值
Digits(integer,fraction) 檢查是否是一種數(shù)字的(整數(shù),小數(shù))的位數(shù)
Email 被注釋的元素必須是電子郵箱地址
Future 檢查該字段的日期是否是屬于將來(lái)的日期
FutureOrPresent 判斷日期是否是將來(lái)或現(xiàn)在日期
Max(value) 該字段的值只能小于或等于該值
Min(value) 該字段的值只能大于或等于該值
Negative 判斷負(fù)數(shù)
NegativeOrZero 判斷負(fù)數(shù)或0
NotBlank 只能用于字符串不為null,并且字符串trim()以后length要大于0
NotEmpty 集合對(duì)象的元素不為0何之,即集合不為空跟畅,也可以用于字符串不為null
NotNull 不能為null
Null 必須為 null
Past 檢查該字段的日期是在過(guò)去
PastOrPresent 判斷日期是否是過(guò)去或現(xiàn)在日期
Pattern(value) 被注釋的元素必須符合指定的正則表達(dá)式
Positive 判斷正數(shù)
PositiveOrZero 判斷正數(shù)或0
Size(max, min) 檢查該字段的size是否在min和max之間,可以是字符串溶推、數(shù)組徊件、集合、Map等
Length(max, min) 判斷字符串長(zhǎng)度
CreditCardNumber 被注釋的字符串必須通過(guò)Luhn校驗(yàn)算法蒜危,銀行卡虱痕,信用卡等號(hào)碼一般都用Luhn計(jì)算合法性

三、自定義注解

1. 自定義Mobile注解

//注解作用范圍
@Target({
        ElementType.METHOD,
        ElementType.FIELD,
        ElementType.ANNOTATION_TYPE,
        ElementType.CONSTRUCTOR,
        ElementType.PARAMETER,
        ElementType.TYPE_USE
})
//注解保留階段
@Retention(RetentionPolicy.RUNTIME)
@Documented
//指定驗(yàn)證器
@Constraint(
        validatedBy = MobileValidator.class
)
public @interface Mobile {

    String message() default "手機(jī)號(hào)格式不正確";

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

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

2. 驗(yàn)證器 MobileValidator

實(shí)現(xiàn)ConstraintValidator 的isValid方法即可

public class MobileValidator implements ConstraintValidator<Mobile, String> {
    @Override
    public void initialize(Mobile annotation) {
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 如果手機(jī)號(hào)為空辐赞,默認(rèn)不校驗(yàn)皆疹,即校驗(yàn)通過(guò)
        if (!StringUtils.hasText(value)) {
            return true;
        }
        // 校驗(yàn)手機(jī)
        return ValidationUtil.isMobile(value);
    }
}

3. 編寫(xiě)UserVo

public class UserVo {

    @NotEmpty(message = "用戶名不能為空")
    @Length(min = 4, max = 6, message = "手機(jī)驗(yàn)證碼長(zhǎng)度為 4-6 位")
    @Pattern(regexp = "^[0-9]+$", message = "手機(jī)驗(yàn)證碼必須都是數(shù)字")
    private String name;

    @Mobile()
    private String mobile;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getMobile() {
        return mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    @Override
    public String toString() {
        return new org.apache.commons.lang3.builder.ToStringBuilder(this)
                .append("name", name)
                .append("mobile", mobile)
                .toString();
    }
}

4. 測(cè)試Controller

    @RequestMapping("/validTest")
    @ResponseBody
    public String validTest(@Valid @RequestBody UserVo userVo) {
        String print = userVo.toString();
        log.info(print);
        return print;
    }

注意:如果輸入的參數(shù)不正確,會(huì)拋出MethodArgumentNotValidException 異常占拍,并會(huì)返回400錯(cuò)誤給前端略就, 我們進(jìn)一步美化輸出統(tǒng)一格式的錯(cuò)誤。

如果controller簽名方法 參數(shù)使用

@GetMapping("/label/list")
@ResponseBody
@Validated //加上才能被spring處理
public RdfaResult<List<LabelVo>> label_list_parent( @NotEmpty(message = "缺少參數(shù)") @RequestParam("kpi_level") String kpi_level) throws Exception {

    return RdfaResult.success(ResponseErrorEnum.SUCESS.getCode(), ResponseErrorEnum.SUCESS.getVal(), resp);
}

5. 定義全局異常處理器

/**
 * 全局異常處理器晃酒,將 Exception 翻譯成 CommonResult + 對(duì)應(yīng)的異常編號(hào)
 */
@RestControllerAdvice
public class GlobalExceptionHandler {


    private Logger logger = LoggerFactory.getLogger(getClass());
 
    /**
     * 處理 SpringMVC  參數(shù)校驗(yàn)異常 Validator 校驗(yàn)
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public CommonResult MethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        logger.warn("[MethodArgumentNotValidException]", ex);
        FieldError fieldError = ex.getBindingResult().getFieldError();
        
        //斷言表牢,避免告警
        assert fieldError != null; 
        
        String format = String.format("請(qǐng)求參數(shù)不正確:%s", fieldError.getDefaultMessage());
        CommonResult<String> commonResult =CommonResult.fail(msg); 
        return commonResult;
    }
}
  • spring-web 組件, org.springframework.web.bind即常見(jiàn)的參數(shù)綁定異常


    spring-mvc-bind異常

四贝次、自定義枚舉校驗(yàn)器

檢驗(yàn)器實(shí)現(xiàn)方式有好多種崔兴,基本原理就是從自定義枚舉注解上邊獲取原數(shù)據(jù),然后使用當(dāng)前值與枚舉所有值進(jìn)行比較蛔翅,存在則返回true, 否則返回fase.

  1. 枚舉校驗(yàn)注解
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumCheckValidator.class)
public @interface EnumCheck {
    /**
     * 是否必填 默認(rèn)是必填的
     * @return
     */
    boolean required() default true;
    /**
     * 驗(yàn)證失敗的消息
     * @return
     */
    String message() default "枚舉的驗(yàn)證失敗";
    /**
     * 分組的內(nèi)容
     * @return
     */
    Class<?>[] groups() default {};

    /**
     * 錯(cuò)誤驗(yàn)證的級(jí)別
     * @return
     */
    Class<? extends Payload>[] payload() default {};

    /**
     * 枚舉的Class
     * @return
     */
    Class<? extends Enum<?>> enumClass();

    /**
     * 枚舉中的驗(yàn)證方法
     * @return
     */
    String enumMethod() default "validation";
}
  1. 枚舉校驗(yàn)器
public class EnumCheckValidator implements ConstraintValidator<EnumCheck, Object> {

    private static final Logger logger = LoggerFactory.getLogger(EnumCheckValidator.class);

    private EnumCheck enumCheck;

    @Override
    public void initialize(EnumCheck enumCheck) {
        this.enumCheck =enumCheck;
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
        // 注解表明為必選項(xiàng) 則不允許為空敲茄,否則可以為空
        if (value == null) {
            return !this.enumCheck.required();
        }

        Boolean result = Boolean.FALSE;
        Class<?> valueClass = value.getClass();
        try {
            //通過(guò)反射執(zhí)行枚舉類中validation方法
            Method method = this.enumCheck.enumClass().getMethod(this.enumCheck.enumMethod(), valueClass);
            result = (Boolean)method.invoke(null, value);
            if(result == null){
                return false;
            }
        } catch (Exception e) {
            logger.error("custom EnumCheckValidator error", e);
        }
        return result;
    }
}

六、原理

  1. 在配置spring-mvc 時(shí)有個(gè)核心類 WebMvcConfigurationSupport.java 會(huì)配置 Validator 的一個(gè)實(shí)例山析。
@Bean
public Validator mvcValidator() {
    Validator validator = getValidator();
    if (validator == null) {
        if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) {
            Class<?> clazz;
            try {
                String className = "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean";
                clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader());
            }
            catch (ClassNotFoundException | LinkageError ex) {
                throw new BeanInitializationException("Failed to resolve default validator class", ex);
            }
            validator = (Validator) BeanUtils.instantiateClass(clazz);
        }
        else {
            validator = new NoOpValidator();
        }
    }
    return validator;
}

這個(gè)實(shí)例的作用是:用于驗(yàn)證標(biāo)注了@ModelAttribute和@RequestBody注解的方法參數(shù)堰燎。實(shí)例化時(shí),首先委托g(shù)etValidator()笋轨,如果返回null秆剪,則檢查類路徑上是否存在 JSR-303實(shí)現(xiàn)(javax.validation.Validator), 存在則實(shí)例化OptionalValidatorFactoryBean爵政,否則返回一個(gè)無(wú)操作的Validator仅讽。

  1. 在 實(shí)例化 OptionalValidatorFactoryBean 的時(shí)候,初始化了SpringConstraintValidatorFactory 用來(lái)代理ConstraintValidatorFactory(JSR-303)钾挟。

  2. HandlerMethodArgumentResolver 的實(shí)現(xiàn)類 RequestPartMethodArgumentResolver 里邊作注解參數(shù)的解析及驗(yàn)證洁灵。


    image.png

默認(rèn)檢測(cè) @javax.validation.Valid、Spring's Validated掺出、自定義以 "Valid" 開(kāi)頭的注解


protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    Annotation[] annotations = parameter.getParameterAnnotations();
    for (Annotation ann : annotations) {
        Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
        if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
            Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
            Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
            binder.validate(validationHints);
            break;
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載徽千,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末蛛砰,一起剝皮案震驚了整個(gè)濱河市罐栈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌泥畅,老刑警劉巖荠诬,帶你破解...
    沈念sama閱讀 212,599評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異位仁,居然都是意外死亡柑贞,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,629評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門聂抢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)钧嘶,“玉大人,你說(shuō)我怎么就攤上這事琳疏∮芯觯” “怎么了闸拿?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,084評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)书幕。 經(jīng)常有香客問(wèn)我新荤,道長(zhǎng),這世上最難降的妖魔是什么台汇? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,708評(píng)論 1 284
  • 正文 為了忘掉前任苛骨,我火速辦了婚禮,結(jié)果婚禮上苟呐,老公的妹妹穿的比我還像新娘痒芝。我一直安慰自己,他們只是感情好牵素,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,813評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布严衬。 她就那樣靜靜地躺著,像睡著了一般两波。 火紅的嫁衣襯著肌膚如雪瞳步。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 50,021評(píng)論 1 291
  • 那天腰奋,我揣著相機(jī)與錄音单起,去河邊找鬼。 笑死劣坊,一個(gè)胖子當(dāng)著我的面吹牛嘀倒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播局冰,決...
    沈念sama閱讀 39,120評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼测蘑,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了康二?” 一聲冷哼從身側(cè)響起碳胳,我...
    開(kāi)封第一講書(shū)人閱讀 37,866評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎沫勿,沒(méi)想到半個(gè)月后挨约,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,308評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡产雹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,633評(píng)論 2 327
  • 正文 我和宋清朗相戀三年诫惭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蔓挖。...
    茶點(diǎn)故事閱讀 38,768評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡夕土,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瘟判,到底是詐尸還是另有隱情怨绣,我是刑警寧澤角溃,帶...
    沈念sama閱讀 34,461評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站篮撑,受9級(jí)特大地震影響开镣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜咽扇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,094評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望陕壹。 院中可真熱鬧质欲,春花似錦、人聲如沸糠馆。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,850評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)又碌。三九已至九昧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間毕匀,已是汗流浹背铸鹰。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,082評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留皂岔,地道東北人蹋笼。 一個(gè)月前我還...
    沈念sama閱讀 46,571評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像躁垛,于是被迫代替她去往敵國(guó)和親剖毯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,666評(píng)論 2 350

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