文档章节

在Spring框架中使用Validation

C
 ChainJ
发布于 2016/09/29 17:17
字数 2648
阅读 1030
收藏 0

        最近在Spring-Boot搭建的Spring MVC项目中做校验工作时遇到一些问题,想必一些朋友也有类似的疑惑,现和大家分享一下,同时也是对自己的工作与学习的一种敦促。

        关于Spring框架中的数据校验,常用的方式有两种,即JSR303标准的Bean Validation(通常底层采用Hibernate的Validator实现)和Spring框架的Validator接口。JSR303是基于一些注解而形成的规范,Hibernate  Validator的实现中增加了部分注解,用起来快捷方便;Spring Validator需要自己去实现校验的过程,通常调用ValidationUtils(org.springframework.validation)的一些静态方法去完成校验,虽然需要自己实现,但是非常灵活。关于这两者的使用,国内常见的网站、博客讲得都比较详细了,在这里我只简单提一提。在某些时候,我们想让自己的校验符合某些需求,既利用Spring Validator接口的灵活,同时又想用上JSR303规范的一些注解让实现变得方便快捷,如何去做呢?这是本文关注的重点。

        本文接下来会分别简单介绍一下Spring Validator接口和JSR303 BeanValidation标准的使用方法,然后在讲如何将二者结合。作者我在实现这件事的时候也在想,两种方式各有优劣,如果能取长补短就好了,在探索的时候,看到了这篇文章,原来自己的想法早有人实现了,这再次印证沃兹基·索德的一句话,“你自以为不错的idea,往往早被人玩坏了”。下面是链接,如果不想看两种方法的简介,可以跳到下文或者看看这篇文章。 http://blog.trifork.com/2009/08/04/bean-validation-integrating-jsr-303-with-spring/  这是Spring框架的开发者在09年写的一篇文章,从文中我们可以看出Spring框架在那时就想完善我今天想到的事了。

 

一、实现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)。让我们具体分析一下这两个方法:

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

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

        现在我们实现一个简单的例子。

实体类:

```

public class User {
    private String name;
    //getter and setter
}

```

校验类:

```

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 = "/users")
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,这个接口有BeanPropertyBindingResult、DirectFieldBindingResult、MapBindingResult以及BindingException这些常见实现类。在正常处理流程中,每一个@Valid的对应一个BindingResult以获取校验过程中的Errors内容。

 

二、使用JSR303 BeanValidation标准的注解进行校验。

        单纯使用注解是很方便快捷的,你只需要引入Hibernate Validator的jar包并在Spring容器中注册一个校验使用的Bean就可以了(这个就不再详述了,推荐一个链接 https://my.oschina.net/qjx1208/blog/200946 )。完成这些工作,在要校验的Bean上加上注解就好了,类似下面这个样子。

```

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

```

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

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

        void initialize(Annotation a)方法用于初始化校验开始前的数据,比如从注解获取一些分组信息等。

        boolean isValid(T value, ConstraintValidatorContext context)方法执行校验逻辑,返回true则代表校验通过,false代表校验失败。第一个参数value就是我们要校验的值的类型,参数ConstraintValidatorContext  context接口负责完成注册该实现类的bean以及调用等一系列操作。

        我们依然实现一个简单的例子。

自定义校验注解:

```

@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 = "/users")
public class UserController{

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

    }

}

```

        一个自定义的注解就可以调用了,根据定义注解时的@Target的值的不同,注解可使用的地方也不同。

 

        自此,使用Spring Validator接口和使用JSR303 BeanValidation标准的校验简介就此结束。下面我们将讲解在我做项目的过程中遇到的问题和自己的想法——将二者结合起来使用。

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

        清理一下我们的需求,我们需要自定义一个继承自Spring Validator的校验类去完成一些对Body内容的初步解析以及错误结果收集和处理;在该校验类中,调用JSR BeanValidation的注解(包括一些自定义注解)去完成实际的校验并将错误结果交给Spring Validator的Errors。实现这些功能,我们既要用到Spring Validator接口,又需要找到Hibernate Validator对JSR BeanValidation的实现以进行调用,我们需要实现以下接口: InitializingBean(主动初始化Bean中的一些数据)、ApplicationContextAware(设置当前Bean运行的上下文)、ConstraintValidatorFactory(获取自定义注解的校验类的Bean)和Spring Validator(实现校验类)。如果不需要调用自定义注解,则只需要实现InitializingBean和Spring Validator接口。

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

```

@Component  // 在Spring中注册该Bean,在其他地方调用
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接口,我们使用Spring Validator接口时,它帮助处理错误信息的收集和处理。有一点需要注意的是,Errors的实现类根据类名来进行写入一些错误信息,但Errors接口的实现类会根据绑定的数据进行反射,所以直接向Errors里面写错误会得到类型不匹配的异常(如某个对象没有xxx field等),所以对于不同的类型需要绑定不同的Errors。而在我们的实现中,要先确定数据类型才能确定Errors,所以我们采用了它的一个子接口BindingResult的一个实现BeanPropertyBindingResult来保存错误信息,然后写入上一层的Errors中。要写入这些错误信息,需要Errors的getObjectName()方法的值相同,这个值是从类名中来的,所以BeanPropertyBindingResult中这个值需要设置为上层Errors中的这个值。该值对校验过程没有大的影响,只是在错误信息中会有一些不友好的体现(比如会提示Body中的name属性不为空,而实际上Body类根本没有name属性,name属性是Body.payload中的某个value的属性)。

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

 

        最近几天的积累大都写下来了,这个过程中还有一些对Spring MVC框架对请求的解析、分发以及反序列化处理的理解,待这部分知识理解足够成熟后,再写一篇博客。

        在朋友的建议下写技术博客,以敦促自己学习和进步。写博客确实有助于对知识点的再梳理,这是第一篇,希望还有很多篇。

 

© 著作权归作者所有

C
粉丝 0
博文 4
码字总数 6834
作品 0
深圳
私信 提问
让Spring Controller 的方法基本数据类型参数支持Bean Validation

让Spring Controller 的方法基本数据类型参数支持Bean Validation Spring中的Bean Validation 我们知道Spring MVC层是默认可以支持Bean Validation的,尝试使用了一下感觉很不方便,只支持对...

ForEleven
2014/04/18
9K
31
使用Spring Validation 完成后端数据校验

转载并修改于:使用spring validation完成数据后端校验 前言 Web开发中JS校验可以涵盖大部分的校验职责,如用户名唯一性,生日格式,邮箱格式校验等等常用的校验。但是为了避免用户绕过浏览器...

Jitwxs
2018/10/11
0
0
Spring Batch 4.1 GA 发布,用于编写批处理应用的框架

Spring Batch 4.1 GA 正式发布了,可以在 Spring Boot 2.1 中使用 Spring Batch 4.1 GA 版本。 Spring Batch 4.1 GA 的更新亮点: 增加新的 注解用于简化测试批处理组件 增加新的 注解,用于...

达尔文
2018/11/01
1K
0
Spring Validation实现原理分析

最近要做动态数据的提交处理,即需要分析提交数据字段定义信息后才能明确对应的具体字段类型,进而做数据类型转换和字段有效性校验,然后做业务处理后提交数据库,自己开发一套校验逻辑的话周...

68号小喇叭
2018/07/08
0
0
Spring4新特性——集成Bean Validation 1.1(JSR-349)到SpringMVC

Spring4新特性——泛型限定式依赖注入 Spring4新特性——核心容器的其他改进 Spring4新特性——Web开发的增强 Spring4新特性——集成Bean Validation 1.1(JSR-349)到SpringMVC Spring4新特性...

咖啡杯
2014/02/18
284
0

没有更多内容

加载失败,请刷新页面

加载更多

关于运维,该怎么决定它的方向,这个似工作又似兴趣的存在

我之前主要从事网络、桌面、机房管理等相关工作,这些工作使我迷惘,这应该是大多数运维人都经历过的过程; 18年国庆,我从国内前三的消费金融公司裸辞,下海创业,就是想要摆脱这样的困境。...

网络小虾米
28分钟前
5
0
Java Timer的用法

Timer timer = new Timer(); timer.schedule(new TimerTask() { public void run() { System.out.println("11232"); } }, 200000 , 1000); public void schedule(TimerTask task, long delay......

林词
32分钟前
5
0
使用js动态加载外部js文件以及动态创建script脚本

动态脚本指的是在页面加载时不存在,但将来的某一时刻通过修改该DOM动态添加的脚本。和操作HTML元素一样,创建动态脚本也有两种方式:插入外部文件和直接插入JavaScript代码。 动态加载外的外...

Bing309
39分钟前
3
0
从零开始入门 K8s | Kubernetes 网络概念及策略控制

作者 | 阿里巴巴高级技术专家 叶磊 一、Kubernetes 基本网络模型 本文来介绍一下 Kubernetes 对网络模型的一些想法。大家知道 Kubernetes 对于网络具体实现方案,没有什么限制,也没有给出特...

阿里巴巴云原生
44分钟前
3
0
天气获取

本文转载于:专业的前端网站➨天气获取 $.get("http://wthrcdn.etouch.cn/WeatherApi", { citykey: cityCode }, function (d) { //创建文档对象 var parser = new ......

前端老手
44分钟前
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部