如何优雅的进行数据校验

Java Bean Validation

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

Java Bean Validation 是什么?

Java Bean Validation 是一个规范,为了给开发人员提供一个对象级的约束声明和验证工具,以及约束元数据存储库和查询api。最早定义在JSR303,经过版本的迭代,从JSR303到JSR349,再到最新的JSR380,也就是现在说的 Bean Validation 2.0。

验证数据是贯穿应用程序的,包括任意一层。通常如果在每层单独进行校验不仅耗时,还会是代码变得冗余。为了避免这种情况,Bean Validation 允许开发人员将验证逻辑直接捆绑到域模型中,将验证逻辑和域模型的代码写在一起。

通常是通过注解的方式进行约束,也可以支持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进行部分状态校验。比如比如被标注的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只是一个规范,而hibenate-validator则是对规范的具体实现

上面提到了如果定义一个约束。接下来介绍如何使用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也创建了一些额外的约束。参照https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#section-builtin-constraints 了解所有支持的约束

使用spring 应该如何进行参数校验

spring validator

将验证视为业务逻辑有利有弊,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 虽然自己定义了一套参数校验的规则,但是由于使用起来并不便利。最终还是对Java Bean Validation进行了适配。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容