在Java数据校验详解中详细介绍了Java数据校验相关的功能(简称Bean Validation,涵盖JSR-303、JSR-349、JSR-380),本文将在Bean Validation的基础上介绍Spring框架提供的数据校验功能。
Spring提供的数据校验功能分为2个部分,一个是Spring自定义的数据校验功能(以下称为Spring Validation),一个是符合Bean Validation规范的数据校验功能。
Spring Validation数据校验
Spring的自行开发的数据校验功能由3个部分组成:
- 校验器——Validator,他会运行校验代码。
- 校验对象,实际上就是一个JavaBean,Validator会对其进行校验。
- 校验结果——Errors,一次校验的结果都存放在Errors实例中。
这是Spring在Bean Validation规范制定之前就实现的数据校验功能,ValidationUtils的注释中@since标签是2003年5月6号,而JSR-303定稿时间已经是6年之后(2009年)的事了。
(文中仅为示例代码,可执行代码请到本人gitee库获取,本文代码在chkui.springcore.example.hybrid.springvalidation包中。)
Spring的数据校验功能就是实现检验器、校验对象、校验结果三个对象。先声明个一个校验对象(实体):
package chkui.springcore.example.hybrid.springvalidation.entity;
//车辆信息
public class Vehicle {
private String name;
private String type;
private String engine;
private String manufacturer;
private Calendar productionDate;
/**Getter Setter*/
}
然后针对这个实体声明一个校验器。校验器要实现org.springframework.validation.Validator接口:
package chkui.springcore.example.hybrid.springvalidation.validator;
public class VehicleValidator implements Validator {
private List<String> _TYPE = Arrays.asList(new String[] { "CAR", "SUV", "MPV" });
public boolean supports(Class<?> clazz) {
//将验证器和实体类进行绑定,如果这里返回false在验证过程中会抛出类型不匹配的异常
return Vehicle.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) { //验证数据
Vehicle vehicle = Vehicle.class.cast(target);
if (null == vehicle.getName()) {
//使用验证工具绑定结果
ValidationUtils.rejectIfEmpty(errors, "name", "name.empty", "车辆名称为空");
}
if (!_TYPE.contains(vehicle.getType())) {
//向Error添加验证错误信息
<2> errors.rejectValue("type", "type.error", "汽车类型必须是" + _TYPE);
}
//More validate ......
}
}
有了验证对象(JavaBean)和对应的验证器(Validator)就完成了一组验证功能。注意VehicleValidator::validate方法传递的errors参数,验证工具会将错误实例传递进来交给开发者去组装验证结果。
代码中的ValidationUtils就是数据校验工具,他提供了2个功能:
- 执行校验(接下来会马上介绍)。
- 提供错误信息绑定的功能,例如ValidationUtils.rejectIfEmpty这一行代码。会将对应的信息写入到Errors中。
有了验证对象和验证器就可以执行验证:
public class SpringValidationApp {
private static void springValidation(ApplicationContext ctx) {
VehicleValidator vehicleValidator = new VehicleValidator();//创建验证器
Vehicle vehicle = new Vehicle();//创建验证对象
<1> ValidationError error = new ValidationError("Vehicle");//创建错误信息
ValidationUtils.invokeValidator(vehicleValidator, vehicle, error);//执行验证
List<FieldError> list = error.getFieldErrors();
int count = 1;
//输出验证结果
for(FieldError res : list) {
print("Error Info ", count++ , ".");
print("Entity:", res.getObjectName());
print("Field:", res.getField());
print("Code:", res.getCode());
print("Message:", res.getDefaultMessage());
print("-");
}
}
}
执行完毕后,ValidationError中记录了所有校验错误信息。错误信息分为4个部分:
- 验证的对象的名称:在执行验证器的代码中<1>部分创建错误对象时指定。Vehicle就是验证对象的名称。
- 错误的域、错误code和错误信息:每一个错误都有对应的域、错误编码以及错误信息,在验证器<2>位置的代码就是指定错误信息。
以上错误信息可以通过error.getFieldErrors();来获取。
如果JavaBean有嵌套的结构,可以在校验器中调用其他的校验器来实现嵌套检验。先为Vehicle类增加一个Gearbox(变速箱)域:
package chkui.springcore.example.hybrid.springvalidation.entity;
//车辆信息
public class Vehicle {
private String name;
private String type;
private String engine;
private String manufacturer;
private Gearbox gearbox; //Gearbox是另外一个实例
private Calendar productionDate;
/**Getter Setter*/
}
//变速箱
public class Gearbox {
private String name;
private String manufacturer;
/**Getter Setter*/
}
在校验器VehicleValidator::validate中增加对Gearbox验证:
public class VehicleValidator implements Validator {
@Autowired
GearboxValidator gearboxValidator; //用于校验Gearbox的校验器
@Override
public void validate(Object target, Errors errors) {
Vehicle vehicle = Vehicle.class.cast(target);
//some code ......
}
if(null == vehicle.getGearbox()) {
errors.rejectValue("gearbox", "gearbox.error", "变速箱信息为空");
}else {
//指定子实体的名称
errors.pushNestedPath("gearbox");
//执行对Gearbox的校验
ValidationUtils.invokeValidator(gearboxValidator, vehicle.getGearbox(), errors);
}
}
}
Bean Validation数据校验
Spring现在推荐使用Bean Validation来进行数据校验,而且已经整合到Spring MVC框架中。
在Spring中使用Bean Validation和Java数据校验详解一文中介绍的内容差不多——也是注解和校验器组成一个约束,通过注解来控制校验的过程。
Spring核心部分没有提供Bean Validation相关的实现类,所以需要引入对应的实现框架。本文引入的是Hibernate Validator,他包括验证器和el,详情可以看源码根目录的build.gradle文件。
首先我们向IoC容器中添加全局校验器:
@Configuration
public class SpringValidationConfig {
@Bean("validator")
public Validator validator() {
return new LocalValidatorFactoryBean();
}
这一段添加Bean的代码非常简单,就是新建了一个LocalValidatorFactoryBean实例。LocalValidatorFactoryBean实现了javax.validation.Validator接口,并且会自动使用已经引入的Bean Validation框架。
然后向Vehicle增加Bean Validation相关的注解:
public class Vehicle {
@NotBlank
private String name;
@NotBlank
@VehicleType
private String type;
@NotBlank
private String engine;
@NotBlank
private String manufacturer;
<3> @Valid //@Valid的作用是对嵌套的解构进行校验
private Gearbox gearbox;
@Valid
private Tyre tyre;
@VehicleProductionDate
private Calendar productionDate;
/**Getter Setter*/
}
在上面的代码中,除了常规的@NotBlank等注解,还有@VehicleType这个自定义注解。在代码<3>的位置@Valid是告诉校验器还要对gearbox的实例进行校验,相当于前面介绍的嵌套校验功能。最后我们使用检验器来对Vehicle的实例进行校验:
public class SpringValidationApp {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringValidationConfig.class);
BeanValidation(ctx);//JSR规范验证
}
private static void BeanValidation(ApplicationContext ctx) {
Validator validator = ctx.getBean(Validator.class);//获取校验器
Vehicle vehicle = new Vehicle();//新建要校验的对象
validator.validate(vehicle).forEach(err -> { //执行校验
print("Field: ", err.getPropertyPath());
print("Error: ", err.getMessage());
});
}
}
关于Bean Validation的详细使用方法已经在 Java数据校验详解介绍。
兼容Bean Validation和Spring Validation
一些相对比较久远的项目可能会遇见在Spring Validation的基础上新增Bean Validation功能的情况。可以使用SpringValidatorAdapter适配器来解决这个问题:
public class SpringValidationApp {
private static void adapterValidation(ApplicationContext ctx) {
// 获取校验器
// LocalValidatorFactoryBean继承了SpringValidatorAdapter
// 所以这里就是获取LocalValidatorFactoryBean
SpringValidatorAdapter adapter = ctx.getBean(SpringValidatorAdapter.class);
Vehicle vehicle = new Vehicle();// 检验对象
ValidationError error = new ValidationError("Vehicle");
// Spring Validation
ValidationUtils.invokeValidator(adapter, vehicle, error);//执行校验
List<FieldError> list = error.getFieldErrors();//检验信息
// Bean Validation 校验
adapter.validate(vehicle).forEach(err -> { // 执行检验&输出校验结果
print("Field: ", err.getPropertyPath());
print("Error: ", err.getMessage());
});
}
}
上面的代码使用SpringValidatorAdapter分别执行了Bean Validation和Spring Validation。可以将SpringValidatorAdapter看作一个org.springframework.validation.Validator的实现类用ValidationUtils来执行校验,而验证的过程完全是按照Bean Validation的规范来执行的。
方法参数校验
除了校验一个实体类,Spring在Bean Validation的基础上使用后置处理器和AOP实现了方法参数的检验。例如下面的方法:
public interface PersonService {
public @NotBlank String execute(@NotBlank(message = "必须设置人员名称") String name,
@Min(value = 18, message = "年龄必须大于18") int age);
}
他表示返回数据不能为空字符串,传入的2个参数name不能为空字符串、age必须大于18。
要启用方法参数校验关键点是引入MethodValidationPostProcessor并在需要验证的Bean上增加一个@Validated注解。
先通过@Configuration引入后置处理器:
@Configuration
@ComponentScan("chkui.springcore.example.hybrid.springvalidation.service")
public class SpringValidationConfig {
@Bean("validator")
public Validator validator() {
return new LocalValidatorFactoryBean();
}
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor(Validator validator) {
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
postProcessor.setValidator(validator);
return postProcessor;
}
}
然后实现上面的PersonService接口并标记@Validated表示这个类中的方法要进行参数校验:
@Service
@Validated
public class PersonServiceImpl implements PersonService {
@Override
public String execute(String name, int age) {
return "I'm " + name + ". " + age + " years old.";
}
}
最后使用这个Service:
public class SpringValidationApp {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringValidationConfig.class);
methodValidation(ctx);//方法参数校验
}
private static void methodValidation(ApplicationContext ctx) {
//对方法进行参数校验
try {
PersonService personService = ctx.getBean(PersonService.class);
personService.execute(null, 1);//传递参数
} catch (ConstraintViolationException error) {
error.getConstraintViolations().forEach(err -> {//输出校验错误信息
print("Field: ", err.getPropertyPath());
print("Error: ", err.getMessage());
});
}
}
}
在运行的过程中,如果参数或返回数据不符合验证规则会抛出ConstraintViolationException异常,可以从中获取校验错误的信息。