如何優(yōu)雅的進行數據校驗

Java Bean Validation

https://beanvalidation.org/2.0/spec/#constraintsdefinitionimplementation-validationimplementation

Java Bean Validation 是什么?

Java Bean Validation 是一個規(guī)范算行,為了給開發(fā)人員提供一個對象級的約束聲明和驗證工具夷恍,以及約束元數據存儲庫和查詢api王暗。最早定義在JSR303,經過版本的迭代涩澡,從JSR303到JSR349,再到最新的JSR380,也就是現在說的 Bean Validation 2.0昌执。

驗證數據是貫穿應用程序的,包括任意一層诈泼。通常如果在每層單獨進行校驗不僅耗時懂拾,還會是代碼變得冗余。為了避免這種情況铐达,Bean Validation 允許開發(fā)人員將驗證邏輯直接捆綁到域模型中岖赋,將驗證邏輯和域模型的代碼寫在一起。

通常是通過注解的方式進行約束瓮孙,也可以支持xml

如何定義約束唐断?

約束:被校驗的參數應該滿足的條件

約束的定義是由約束注解和約束校驗的實現來組合使用完成的。

約束注解

約束注解可以作用在 types(類杭抠,接口), fields(屬性), methods(方法), constructors(構造器), parameters(參數), container elements(容器元素)以及在組合使用的場景還可以用在其他約束注解上

約束注解還必須被 javax.validation.Constraint 標注

先介紹一下 Constraint 注解

@Documented
@Target({ ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface Constraint {
    Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}

真正的校驗邏輯在 validatedBy() 中指定的類中進行脸甘,該類必須繼承 ConstraintValidator 類。并且必須實現 initialize 方法和 isValid 方法祈争。關于ConstraintValidator后面在實現自定義注解的時候會介紹ConstraintValidator

除了被Constraint注解標注外斤程,約束注解還具有以下屬性。

  • String message() default "{com.acme.constraint.MyConstraint.message}";
    每一個約束注解必須定義一個message元素,用來設置校驗失敗時的錯誤信息

  • Class<?>[] groups() default {};
    groups 元素被定義成有一個class數組組成,默認值是空數組。groups可以用來控制約束的順序和對javaBean進行部分狀態(tài)校驗夸溶。比如比如被標注的groups包含方法上指定的groups時奶躯,才進行校驗

  • Class<? extends Payload>[] payload() default {};
    payLoad() 元素是由實現了Payload的類的數組組成。payLoad 可以將元數據信息和約束生命關聯起來。 payLoad的介紹參考:https://www.logicbig.com/tutorials/java-ee-tutorial/bean-validation/constraint-payload.html`

  • ConstraintTarget validationAppliesTo() default ConstraintTarget.IMPLICIT;
    validationAppliesTo用來聲明約束的目標(ConstraintTarget.IMPLICIT;ConstraintTarget.RETURN_VALUE;ConstraintTarget.PARAMETERS)

例:

//assuming OrderNumberValidator is a generic constraint validator
 
package com.acme.constraint;
 
/**
 * Mark a String as representing a well formed order number
 */
@Documented
@Constraint(validatedBy = OrderNumberValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface OrderNumber {
 
    String message() default "{com.acme.constraint.OrderNumber.message}";
 
    Class<?>[] groups() default {};
 
    Class<? extends Payload>[] payload() default {};
}

多個相同的約束注解可以同時使用。

public class Address {
    @ZipCode(countryCode = "fr", groups = Default.class, message = "zip code is not valid")
    @ZipCode(
        countryCode = "fr",
        groups = SuperUser.class,
        message = "zip code invalid. Requires overriding before saving."
    )
    private String zipCode;
}

同時也可以組合使用

@Pattern(regexp = "[0-9]*")
@Size(min = 5, max = 5)
@Constraint(validatedBy = FrenchZipCodeValidator.class)
@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface FrenchZipCode {
 
    String message() default "Wrong zip code";
 
    Class<?>[] groups() default {};
 
    Class<? extends Payload>[] payload() default {};
 
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        FrenchZipCode[] value();
    }
}

約束校驗實現

約束校驗實現類必須是ConstraintValidator接口的實現

public interface ConstraintValidator<A extends Annotation, T> {
 
    default void initialize(A constraintAnnotation) {
    }
 
    boolean isValid(T value, ConstraintValidatorContext context);
}

范型A表示這個是實現類被哪個約束注解使用,也就是Constraint注解的validatedBy設置的值(Constraint

真正校驗的邏輯是在isvalid方法中實現的望薄。參考下面的例子

public class CollectionSizeLimitValidator implements ConstraintValidator<CollectionSizeLimit, Collection<?>> {
 
    private int limitSize;
 
    @Override
    public void initialize(CollectionSizeLimit constraintAnnotation) {
        limitSize = constraintAnnotation.limitSize();
    }
 
    @Override
    public boolean isValid(Collection<?> objects, ConstraintValidatorContext constraintValidatorContext) {
        if(CollectionUtils.isEmpty(objects) || limitSize<objects.size()){
            return false;
        }
        return true;
    }
}

自此約束就被定義好了,被定義好的約束注解標注到對應的元素上就可以對參數進行約束

hibenate-validator – Java Bean Validation的實現

前面提到Java Bean Validation只是一個規(guī)范呼畸,而hibenate-validator則是對規(guī)范的具體實現

上面提到了如果定義一個約束痕支。接下來介紹如何使用hibenate-validator進行校驗

  1. 獲取Validator實例
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
  1. Validator接口包含3個方法可以用來對整個實體或者單個屬性進行校驗
    • Validator#validate() 對給定的標注了約束注解的屬性的bean進行校驗
Car car = new Car( null, true );
 
Set<ConstraintViolation<Car>> constraintViolations = validator.validate( car );
 
assertEquals( 1, constraintViolations.size() );
assertEquals( "must not be null", constraintViolations.iterator().next().getMessage() );
  • Validator#validateProperty() 對給定對象的單個屬性進行校驗
Car car = new Car( null, true );
 
Set<ConstraintViolation<Car>> constraintViolations = validator.validateProperty(
        car,
        "manufacturer"
);
 
assertEquals( 1, constraintViolations.size() );
assertEquals( "must not be null", constraintViolations.iterator().next().getMessage() );
  • Validator#validateValue() 通過使用validateValue()方法,您可以檢查給定類的單個屬性是否可以被成功驗證蛮原,如果該屬性具有指定的值
Set<ConstraintViolation<Car>> constraintViolations = validator.validateValue(
        Car.class,
        "manufacturer",
        null
);
 
assertEquals( 1, constraintViolations.size() );
assertEquals( "must not be null", constraintViolations.iterator().next().getMessage() );

Java Bean Validation 聲明了一些約束卧须,同樣hibernate-validator也創(chuàng)建了一些額外的約束。參照https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-builtin-constraints 了解所有支持的約束

使用spring 應該如何進行參數校驗

spring validator

將驗證視為業(yè)務邏輯有利有弊儒陨,spring設計了一個校驗的框架花嘶。validation包下主要有dataBinder和validator兩部分。

Validator是一個接口蹦漠,類通過實現Validator接口椭员,并實現 supports 方法和 validate 方法來完成一個校驗器的編寫。錯誤信息會放到Errors中笛园,

public class PersonValidator implements Validator {
 
    /**
     * This Validator validates only Person instances
     */
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }
 
    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}

然后借助DataBuinder的validate方法完成校驗

Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());
 
// bind to the target object
binder.bind(propertyValues);
 
// validate the target object
binder.validate();
 
// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();

這種方式和Java Bean Validation比 使用起來明顯很繁瑣

所以spring validation實現了對Java Bean Validation的適配隘击,完成了救贖

LocalValidatorFactoryBean 類既實現了javaBeanValidation 的 javax.validation.ValidatorFactory , javax.validation.Validator 接口喘沿,同樣也實現了spring 的org.springframework.validation.Validator闸度。所以可以看出LocalValidatorFactoryBean其實是一個適配或者說整合spring Validation和java Bean validation的校驗功能的類

如果classPath中存在Java Bean Validation,LocalValidatorFactoryBean 會被注冊成全局的validator蚜印。

public Validator mvcValidator() {
        Validator validator = getValidator();
        if (validator == null) {
            if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) {
                Class<?> clazz;
                try {
                    //這里的OptionalValidatorFactoryBean是LocalValidatorFactoryBean的子類
                    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;
    }

LocalValidatorFactoryBean的父類SpringValidatorAdapter中定義了

private javax.validation.Validator targetValidator;

真正的validate操作會委派給這個對象,最終進行的還是Java Bean Validation的校驗留量。

public void validate(Object target, Errors errors) {
        if (this.targetValidator != null) {
            processConstraintViolations(this.targetValidator.validate(target), errors);
        }
    }

所以 spring 雖然自己定義了一套參數校驗的規(guī)則窄赋,但是由于使用起來并不便利。最終還是對Java Bean Validation進行了適配楼熄。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末忆绰,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子可岂,更是在濱河造成了極大的恐慌错敢,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異稚茅,居然都是意外死亡纸淮,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門亚享,熙熙樓的掌柜王于貴愁眉苦臉地迎上來咽块,“玉大人,你說我怎么就攤上這事欺税〕藁Γ” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵晚凿,是天一觀的道長亭罪。 經常有香客問我,道長歼秽,這世上最難降的妖魔是什么应役? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮哲银,結果婚禮上扛吞,老公的妹妹穿的比我還像新娘。我一直安慰自己荆责,他們只是感情好滥比,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著做院,像睡著了一般盲泛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上键耕,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天寺滚,我揣著相機與錄音,去河邊找鬼屈雄。 笑死村视,一個胖子當著我的面吹牛,可吹牛的內容都是我干的酒奶。 我是一名探鬼主播蚁孔,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼惋嚎!你這毒婦竟也來了杠氢?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤另伍,失蹤者是張志新(化名)和其女友劉穎鼻百,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡温艇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年因悲,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片中贝。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡囤捻,死狀恐怖,靈堂內的尸體忽然破棺而出邻寿,到底是詐尸還是另有隱情蝎土,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布绣否,位于F島的核電站誊涯,受9級特大地震影響,放射性物質發(fā)生泄漏蒜撮。R本人自食惡果不足惜暴构,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望段磨。 院中可真熱鬧取逾,春花似錦、人聲如沸苹支。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽债蜜。三九已至晴埂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間寻定,已是汗流浹背儒洛。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留狼速,地道東北人琅锻。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像向胡,于是被迫代替她去往敵國和親浅浮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內容