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,才会继续调用 Validator 的 validate 方法。
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 ,这个接口有 BeanPropertyBindingResult、DirectFieldBindingResult 、MapBindingResult 以及 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@RequestBody User user
这样的方式去反序列化 user 了,因为它现在是 Body.payload 中某个key对应的value。
Spring Validator 接口很灵活,我们完全可以照自己的想法去实现一个对于 Body 的校验类,然后在该类里面再对 user 进行校验。事实上,对于Body.payload中不同的value,可能有着不同的校验类,我们可以在Body的校验类中分别调用它们,这并不难;但如果能够使用 JSR303 BeanValidation 标准的注解,我们将极大节省开发时间。
梳理一下我们的需求:
- 自定义一个继承自 Spring Validator 的校验类去完成一些对Body内容的初步解析以及错误结果收集和处理
- 在校验类中,调用 BeanValidation 的注解(包括一些自定义注解)去完成实际的校验
- 将错误结果交给 Spring Validator 的 Errors ,实现错误提示
要实现这些功能,我们既要用到 Spring Validator 接口,又需要找到 Hibernate Validator 对 JSR 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中。要写入这些错误信息,需要 Errors 的 getObjectName()
方法的值相同,这个值是从类名中来的,所以 BeanPropertyBindingResult 中这个值需要和上层 Errors 保持一致。该值对校验过程没有大的影响,只是在错误信息中会不准确。比如会提示Body中的name属性不为空,而实际上Body类根本没有name属性,name属性是Body.payload.user的属性
最后是对错误信息的处理,Spring MVC 提供 @ExceptionHandler
注解来标识错误信息处理的方法,可以在Controller中使用它。
后记
本文提供类结合 Spring Validator 和 JSR BeanValidation 以高效实现自定义校验类的解决方案。 在这个想法产生之初,我曾疑惑既然 JSR303 中已有校验标准,并且有开源项目实现了校验流程,为何 Spring 的开发者要再定义一套校验流程?重复造轮子低效又无意义。 在实现这个工程的过程中,我发现了 Sping 开发者的这篇文章,本文阐述了自定义校验流程的目的,同时也为我实现这个解决方案提供了思路。Spring 的开发者在09年就想解决这个问题,但到了今日还没有完成集成,可能是因为这个场景过于少见了吧。