Spring 集成 JSR BeanValidation


Spring 框架中的数据校验,常用的方式有两种,即 JSR303 标准的 Bean Validation (通常底层采用 Hibernate Validator 实现)和Spring框架的 Validator 接口。

JSR303: 基于Java标准注解而形成的规范

Hibernate Validator: 实现 JSR303, 并增加了部分注解,功能更强,使用方便;

  • 实现 JSR303 标准
  • 功能增强,增加了自定义注解
  • 开箱即用,无需额外编码

Spring Validator:

  • 需要自行实现校验过程
  • 提供功能丰富的校验工具类 org.springframework.validation.ValidationUtils
  • 实现灵活,可扩展性强

关于这两者的使用,大部分能搜索到的博客讲得都比较详细了,在这里我只简单提一提。

Spring Validator

接口说明

实现 Spring 框架的 Validator 接口:

Validator 接口( org.springframework.validation.Validator )是Spring(Spring 3.0+)框架中声明的接口,它通过 @Valid (javax.validation.Valid JSR标准) 或 @Validated(value = {SomeInterface.class}) ( org.springframework.validation.annotation.Validated Spring标准)注解调用已有的实现类。

Validator接口有两个方法,boolean support(Class<?> clazz) ,和 void validate(Object target, Errors errors) 。让我们具体分析一下这两个方法:

support 方法,返回该实现类是否支持被校验的对象,也就是是否支持clazz的值。调用自实现的 Validator 实例时,这个方法返回true,才会继续调用 Validatorvalidate 方法。

validate ,对被 @Valid@Validated 注解标记的对象进行校验,遇到的异常将通过 Errors 接口返回。关于 Errors 在下文再详细介绍。

示例

实体类:

public class User {
    private String name;
}

校验类:

public class UserValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return User.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "user name can't be empty!");
        User user = (User) target;
        if(user.getName().equals("admin")){
            errors.rejectValue("name", "name can't be 'admin'!");
        }
    }

调用注解进行校验:

@RestController
@RequestMapping(value = "/user")
public class UserController{
    @InitBinder
    public initBinder(WebDataBinder binder){
        binder.setValidator(new UserValidator());
    }

    @RequestMapping(value = "", method = RequestMethod.POST)
    public ModelAndView register(@Validated @RequestBody User user,  BindingResult result)  {

    }
}

这样就可以在 Controller 中调用了。 上文提及的 Errors 接口,它的一个子接口是 BindingResult ,这个接口有 BeanPropertyBindingResultDirectFieldBindingResultMapBindingResult 以及 BindingException 这些常见实现类。在正常处理流程中,每一个 @Valid 的对应一个 BindingResult 以获取校验过程中的 Errors 内容。

Hibernate BeanValidation

使用说明

单纯使用注解是很方便快捷的,只需要引入Hibernate Validator的jar包并在Spring容器中注册一个校验使用的Bean就可以了。 完成这些工作,在要校验的Bean上加上注解就好了,类似下面这个样子。

public class User {
    @NotNull(groups = { UserRegister.class })
    private String name;
}

groups 属性用于支持分组校验和一些顺序校验。

自定义校验

JSR303 BeanValidation 也支持自定义注解和对应的校验逻辑,这个功能主要通过定义注解和实现 ConstraintValidator<A extends Annotation, T> 接口里的 void initialize(Annotation a) 方法和 boolean isValid( T value, ConstraintValidatorContext context) 方法。

initialize 方法用于初始化校验开始前的数据,比如从注解获取一些分组信息等。 isValid 方法用户执行校验逻辑,返回true则代表校验通过,false代表校验失败。

自定义示例

自定义校验注解:

@Target({ FIELD, PARAMETER }) // 注解可于字段和参数
@Retention(RUNTIME)
@Constraint(validatedBy = MyValidator.class) // 指定自定义校验器
public @interface MyValidation {
    String message() default "error message!"; // 用于保存错误信息
    Class<?>[] groups() default {}; // 当需要分组时,可保存分组信息
    Class<? extends Payload>[] payload() default {};
}

实现校验类:

public class MyValidator implements ConstraintValidator<MyValidation, User> {

    @Override
    public void initialize(MyValidation constraintAnnotation) {
        constraintAnnotation.message();
    }

    @Override
    public boolean isValid(User value, ConstraintValidatorContext context) {
        if (StringUtils.isEmpty(value.getName()) || "admin".equals(value.getName())) {
            return false;
        }

        return true;
    }
}

调用校验注解:

@RestController
@RequestMapping(value = "/user")
public class UserController{

    @RequestMapping(value = "", method = RequestMethod.POST)
    public ModelAndView register(@MyValidation @RequestBody User user,  BindingResult result)  {

    }
}

完成以上步骤,一个自定义的注解就可以调用了。

结合 Spring Validator + BeanValidation

自此,使用Spring Validator接口和使用JSR303 BeanValidation标准的校验简介就此结束。 在某些时候,我们想让自己的校验符合某些需求,既利用Spring Validator接口的灵活,同时又想用上JSR303规范的一些注解让实现变得方便快捷,如何去做呢?这是本文关注的重点。 下面我们将讲解在我做项目的过程中遇到的问题和自己的想法:将二者结合起来使用。

背景

存在这样一个情景,出于某种需要,我们把所有传入 Controller 的数据用一个类 Body 封装起来,所有数据只存在 Body 的一个属性 Map payload 中。序列化 Body 不困难,但如何验证每个 Controller 获取的不同数据呢?我们不能再使用 @RequestBody User user 这样的方式去反序列化 user 了,因为它现在是 Body.payload 中某个key对应的value。 Spring Validator 接口很灵活,我们完全可以照自己的想法去实现一个对于 Body 的校验类,然后在该类里面再对 user 进行校验。事实上,对于Body.payload中不同的value,可能有着不同的校验类,我们可以在Body的校验类中分别调用它们,这并不难;但如果能够使用 JSR303 BeanValidation 标准的注解,我们将极大节省开发时间。

梳理一下我们的需求:

  • 自定义一个继承自 Spring Validator 的校验类去完成一些对Body内容的初步解析以及错误结果收集和处理
  • 在校验类中,调用 BeanValidation 的注解(包括一些自定义注解)去完成实际的校验
  • 将错误结果交给 Spring ValidatorErrors ,实现错误提示

要实现这些功能,我们既要用到 Spring Validator 接口,又需要找到 Hibernate ValidatorJSR BeanValidation 的实现类以进行调用。 我们的思路如下:

  • 实现 InitializingBean 以初始化Bean中的一些数据
  • 实现 ApplicationContextAware 以设置当前Bean运行的上下文
  • 实现 ConstraintValidatorFactory 以获取自定义注解的校验类的Bean
  • 实现 Validator 以实现自定义校验类

如果不需要调用自定义注解,则只需要实现InitializingBean和Spring Validator接口。

实现示例

Spring Validator 接口有个子接口 SmartValidator ,跟踪源码可以发现,它只是在 Validator 的基础上增加了分组校验的功能。因为分组校验很常用,所以这里我们采用 SmartValidator

@Component
public class BodyValidation implements InitializingBean, ApplicationContextAware, ConstraintValidatorFactory, org.springframework.validation.SmartValidator {

    private Validator validator;  // javax.validation.Validator
    private ApplicationContext applicationContext;

    @Override
    public boolean supports(Class<?> clazz) {
        return true;  // 本校验适用于几乎所有类型的Bean
    }

    @Override
    public void validate(Object target, Errors errors) {
        validate(target, errors, Default.class);  // javax.validation.groups.Default
    }

    @Override
    public void validate(Object target, Errors errors, Object... validationHints) {
        if (validator == null) {
            return;
        }
        Class<?>[] classes = new Class[validationHints.length];
        for (int i = 0; i < validationHints.length; i++) {
            classes[i] = (Class<?>) validationHints[i];
        }

        Body body = (Body) target;
        for (String key : body.getMapping().keySet()) {
            Set<ConstraintViolation<Object>> constraintViolations = validator.validate(body.get(key), classes);
            Errors error = new BeanPropertyBindingResult(body.get(key), errors.getObjectName());
            for (ConstraintViolation<Object> violation : constraintViolations) {
                String propertyPath = violation.getPropertyPath().toString();
                String message = violation.getMessage();
                error.rejectValue(propertyPath, "Field Error: " + key + "." + propertyPath, message);
            }
            if (error.hasErrors()) {
                errors.addAllErrors(error);  // 为了使errors能拿到错误信息,error和errors的objectName必须一致
            }
        }
    }

    /**
     * 获取目标类型的校验类实例
     */
    @Override
    public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
        Map<?, T> beans = applicationContext.getBeansOfType(key);
        if (beans.isEmpty()) {
            try {
                return key.newInstance();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

        if (beans.size() > 1) {
            throw new RuntimeException("Beans must be Singleton!");
        }

        return beans.values().iterator().next();
    }

    /**
     * 销毁校验类实例
     */
    @Override
    public void releaseInstance(ConstraintValidator<?, ?> instance) {
        System.out.println("The bean of BodyValidation is no longer used!");
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        ValidatorFactory validatorFactory =  Validation.byDefaultProvider().configure(). constraintValidatorFactory(this).buildValidatorFactory();
        validator = validatorFactory.usingContext().getValidator();
    }
}

补充说明

前面的实现中再次用到了 Spring Validator 接口中用到的 Errors 接口,它帮助处理错误信息的收集和处理。 有一点需要注意的是,Errors 的实现类根据类名来进行写入错误信息,同时会根据绑定的数据进行反射以验证字段类型。直接向 Errors 里面写错误会得到类型不匹配的异常(如某个对象没有xxx field等),所以对于不同的类型需要绑定不同的 Errors。 在我们的实现中,需要先确定数据类型才能确定Errors。我们采用了它的一个子接口 BindingResult 的一个实现 BeanPropertyBindingResult 来保存错误信息,然后写入上一层的Errors中。要写入这些错误信息,需要 ErrorsgetObjectName() 方法的值相同,这个值是从类名中来的,所以 BeanPropertyBindingResult 中这个值需要和上层 Errors 保持一致。该值对校验过程没有大的影响,只是在错误信息中会不准确。比如会提示Body中的name属性不为空,而实际上Body类根本没有name属性,name属性是Body.payload.user的属性

最后是对错误信息的处理,Spring MVC 提供 @ExceptionHandler 注解来标识错误信息处理的方法,可以在Controller中使用它。

后记

本文提供类结合 Spring ValidatorJSR BeanValidation 以高效实现自定义校验类的解决方案。 在这个想法产生之初,我曾疑惑既然 JSR303 中已有校验标准,并且有开源项目实现了校验流程,为何 Spring 的开发者要再定义一套校验流程?重复造轮子低效又无意义。 在实现这个工程的过程中,我发现了 Sping 开发者的这篇文章,本文阐述了自定义校验流程的目的,同时也为我实现这个解决方案提供了思路。Spring 的开发者在09年就想解决这个问题,但到了今日还没有完成集成,可能是因为这个场景过于少见了吧。

bean-validation-integrating-jsr-303-with-spring

© Cheng all right reserved,powered by GitbookModified At: 2022-03-31 17:43:32

results matching ""

    No results matching ""