Java数据校验详解中详细介绍了Java数据校验相关的功能(简称Bean Validation,涵盖JSR-303、JSR-349、JSR-380),本文将在Bean Validation的基础上介绍Spring框架提供的数据校验功能。

Spring提供的数据校验功能分为2个部分,一个是Spring自定义的数据校验功能(以下称为Spring Validation),一个是符合Bean Validation规范的数据校验功能。



Spring Validation数据校验

Spring的自行开发的数据校验功能由3个部分组成:

  1. 校验器——Validator,他会运行校验代码。
  2. 校验对象,实际上就是一个JavaBean,Validator会对其进行校验。
  3. 校验结果——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个功能:

  1. 执行校验(接下来会马上介绍)。
  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 ValidationJava数据校验详解一文中介绍的内容差不多——也是注解和校验器组成一个约束,通过注解来控制校验的过程。

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 ValidationSpring 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异常,可以从中获取校验错误的信息。