文档章节

在Spring框架中使用Validation

C
 ChainJ
发布于 2016/09/29 17:17
字数 2648
阅读 631
收藏 0
点赞 0
评论 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
深圳
Spring4新特性——集成Bean Validation 1.1(JSR-349)到SpringMVC

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

咖啡杯 ⋅ 2014/02/18 ⋅ 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 ⋅ 0

zhaoml529/SpringMVC-Activiti5.16-Shiro

SpringOA简介 基于SpringMVC+Shiro+Activiti 5.16 的简单OA,可以快速入门Activiti学习用。 此版本前台使用的是EasyUI 框架简介 框架以Spring Framework为核心、Spring MVC作为模型视图控制器...

zhaoml529 ⋅ 2017/05/19 ⋅ 0

第二章——Spring Boot启动器与依赖管理

强烈推荐选择支持依赖管理和可以发布到“Maven中央”仓库的构建系统。推荐使用Maven或Gradle。Spring Boot可以使用其他构建系统(例如,Ant),但支持得并不是很好。 1 依赖管理 Spring Boo...

Leech ⋅ 02/04 ⋅ 0

【补充】Hibernate validator使用和自定义validator及整合Spring MVC

Hibernate validator使用 导入validation-api-xxx.jar 以及 hibernate-validator-xxx.Final.jar 需要检查的java bean Entity.java 值校验的测试类 输出结果 这里有一个国际化的key值,国际化文...

jason_wu_2 ⋅ 2016/12/27 ⋅ 0

Spring 3 MVC and JSR303 @Valid example

In Spring 3, you can enable “mvc:annotation-driven” to support JSR303 bean validation via annotation, if any JSR 303 validator framework on the classpath. Note Hibernate Vali......

凯文加内特 ⋅ 2015/10/10 ⋅ 0

bean的属性校验

拓展阅读: Java如何实现判断一个对象的所有属性是否为空 springMVC介绍之spring validation Spring4新特性——集成Bean Validation 1.1(JSR-349)到SpringMVC(系列) 7. Validation, Data Bin...

pandudu ⋅ 2016/12/27 ⋅ 0

Spring MVC 到 Spring BOOT的简化之路

背景 从Servlet技术到Spring和Spring MVC,开发Web应用变得越来越简捷。但是Spring和Spring MVC的众多配置有时却让人望而却步,相信有过Spring MVC开发经验的朋友能深刻体会到这一痛苦。因为...

临江仙卜算子 ⋅ 05/09 ⋅ 0

项目中SpringMVC、Spring和Struts的区别讲解

导读:近期做到的项目中,用到的框架师SSM(SpringMVC+Spring+Mybatis),那么在这之前用过SSH,这里主要是区分一下SpringMVC和Struts,但是由于SpringMVC和Spring真的也挺容易迷糊的,所以,...

yiguang_820的博客 ⋅ 2017/12/11 ⋅ 0

Spring3.0.5jar包用法详解

Spring3.X以后jar包进行了重构,取消了原来2.X版本中的总的spring.jar包,而是把总包中的功能全部分开打包。正在向osgi靠拢。 各个jar包详解如下: 1. org.springframework.aop 包含在应用中...

sfilyh ⋅ 2012/03/30 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

tcp/ip详解-链路层

简介 设计链路层的目的: 为IP模块发送和接收IP数据报 为ARP模块发送ARP请求和接收ARP应答 为RARP模块发送RARP请求和接收RARP应答 TCP/IP支持多种链路层协议,如以太网、令牌环往、FDDI、RS-...

loda0128 ⋅ 59分钟前 ⋅ 0

spring.net aop代码例子

https://www.cnblogs.com/haogj/archive/2011/10/12/2207916.html

whoisliang ⋅ 今天 ⋅ 0

发送短信如何限制1小时内最多发送11条短信

发送短信如何限制1小时内最多发送11条短信 场景: 发送短信属于付费业务,有时为了防止短信攻击,需要限制发送短信的频率,例如在1个小时之内最多发送11条短信. 如何实现呢? 思路有两个 截至到当...

黄威 ⋅ 昨天 ⋅ 0

mysql5.7系列修改root默认密码

操作系统为centos7 64 1、修改 /etc/my.cnf,在 [mysqld] 小节下添加一行:skip-grant-tables=1 这一行配置让 mysqld 启动时不对密码进行验证 2、重启 mysqld 服务:systemctl restart mysql...

sskill ⋅ 昨天 ⋅ 0

Intellij IDEA神器常用技巧六-Debug详解

在调试代码的时候,你的项目得debug模式启动,也就是点那个绿色的甲虫启动服务器,然后,就可以在代码里面断点调试啦。下面不要在意,这个快捷键具体是啥,因为,这个keymap是可以自己配置的...

Mkeeper ⋅ 昨天 ⋅ 0

zip压缩工具、tar打包、打包并压缩

zip 支持压缩目录 1.在/tmp/目录下创建目录(study_zip)及文件 root@yolks1 study_zip]# !treetree 11└── 2 └── 3 └── test_zip.txt2 directories, 1 file 2.yum...

蛋黄Yolks ⋅ 昨天 ⋅ 0

聊聊HystrixThreadPool

序 本文主要研究一下HystrixThreadPool HystrixThreadPool hystrix-core-1.5.12-sources.jar!/com/netflix/hystrix/HystrixThreadPool.java /** * ThreadPool used to executed {@link Hys......

go4it ⋅ 昨天 ⋅ 0

容器之上传镜像到Docker hub

Docker hub在国内可以访问,首先要创建一个账号,这个后面会用到,我是用126邮箱注册的。 1. docker login List-1 Username不能使用你注册的邮箱,要用使用注册时用的username;要输入密码 ...

汉斯-冯-拉特 ⋅ 昨天 ⋅ 0

SpringBoot简单使用ehcache

1,SpringBoot版本 2.0.3.RELEASE ①,pom.xml <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.3.RELE......

暗中观察 ⋅ 昨天 ⋅ 0

Spring源码解析(八)——实例创建(下)

前言 来到实例创建的最后一节,前面已经将一个实例通过不同方式(工厂方法、构造器注入、默认构造器)给创建出来了,下面我们要对创建出来的实例进行一些“加工”处理。 源码解读 回顾下之前...

MarvelCode ⋅ 昨天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部