文档章节

在Spring框架中使用Validation

C
 ChainJ
发布于 2016/09/29 17:17
字数 2648
阅读 904
收藏 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
0
32
使用Spring Validation 完成后端数据校验

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

Jitwxs
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 的更新亮点: 增加新的 注解用于简化测试批处理组件 增加新的 注解,用于...

达尔文
11/01
1K
0
spring mvc 采用 jsr303 bean validation 校验框架

这是一个规范,定义了一些元素来进行bean的数据校验,比如 你的model有一个 user.java ,里面有一个email,当用户注册时候要验证email是否合法。 一般做法是js前端校验,但是不安全,作为完整...

moz1q1
2014/10/31
0
0
Spring Validation实现原理分析

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

68号小喇叭
07/08
0
0

没有更多内容

加载失败,请刷新页面

加载更多

使用正则表达式实现网页爬虫的思路详解

网页爬虫:就是一个程序用于在互联网中获取指定规则的数据。这篇文章主要介绍了使用正则表达式实现网页爬虫的思路详解,需要的朋友可以参考下 网页爬虫:就是一个程序用于在互联网中获取指定规...

前端小攻略
21分钟前
0
0
vue中锚点的三种方法

第一种: router.js中添加 mode: 'history', srcollBehavior(to,from,savedPosition){ if(to.hash){ return {selector:to.hash } } } 组件: <template><div><ul class="li......

peakedness丶
23分钟前
0
0
记一次面试最常见的10个Redis"刁难"问题

导读:在程序员面试过程中Redis相关的知识是常被问到的话题。作为一名在互联网技术行业打击过成百上千名的资深技术面试官,本文作者总结了面试过程中经常问到的问题。十分值得一读。 Redis在...

小刀爱编程
今天
20
0
TiDB Lab 诞生记 | TiDB Hackathon 优秀项目分享

本文由红凤凰粉凤凰粉红凤凰队的成员主笔,他们的项目 TiDB Lab 在本届 TiDB Hackathon 2018 中获得了二等奖。TiDB Lab 为 TiDB 培训体系增加了一个可以动态观测 TiDB / TiKV / PD 细节的动画...

TiDB
今天
5
0
当区块链遇到零知识证明

本文由云+社区发表 当区块链遇到零知识证明 什么是零知识证明 零知识证明的官方定义是能够在不向验证者任何有用的信息的情况下,使验证者相信某个论断是正确的。这个定义有点抽象,下面笔者举...

腾讯云加社区
今天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部