如何优雅的进行数据校验

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>[] 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[] 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[] 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[] payload() default {};
 
    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        FrenchZipCode[] value();
    }
}

约束校验实现

约束校验实现类必须是ConstraintValidator接口的实现

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

范型A表示这个是实现类被哪个约束注解使用,也就是Constraint注解的validatedBy设置的值(Constraint)

真正校验的逻辑是在isvalid方法中实现的。参考下面的例子

public class CollectionSizeLimitValidator implements ConstraintValidator> {
 
    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

自此约束就被定义好了,被定义好的约束注解标注到对应的元素上就可以对参数进行约束

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> 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> constraintViolations = validator.validateProperty(
        car,
        "manufacturer"
);
 
assertEquals( 1, constraintViolations.size() );
assertEquals( "must not be null", constraintViolations.iterator().next().getMessage() );
  • Validator#validateValue() 通过使用validateValue()方法,您可以检查给定类的单个属性是否可以被成功验证,如果该属性具有指定的值
Set> 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进行了适配。

你可能感兴趣的:(如何优雅的进行数据校验)