Skip to content

使用 Spring 的 Validator 接口进行校验

Spring 提供了一个 Validator 接口,你可以使用它来校验对象。Validator 接口通过使用 Errors 对象来工作,这样在校验过程中,校验器可以将校验失败的情况报告给 Errors 对象。

考虑以下一个小的数据对象示例:

java
public class Person {

	private String name;
	private int age;

	// 通常的 getter 和 setter...
}
kotlin
class Person(val name: String, val age: Int)

接下来的示例通过实现 org.springframework.validation.Validator 接口的以下两个方法,为 Person 类提供校验行为:

  • supports(Class):此 Validator 是否可以校验所提供 Class 的实例?
  • validate(Object, org.springframework.validation.Errors):校验给定的对象,如果有校验错误,则将其注册到给定的 Errors 对象中。

实现 Validator 非常直接,尤其是当你了解 Spring 框架提供的 ValidationUtils 工具类时。以下示例为 Person 实例实现了 Validator

java
public class PersonValidator implements Validator {

	/**
	 * 此 Validator 仅校验 Person 实例
	 */
	public boolean supports(Class clazz) {
		return Person.class.equals(clazz);
	}

	public void validate(Object obj, Errors e) {
		ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
		Person p = (Person) obj;
		if (p.getAge() < 0) {
			e.rejectValue("age", "negativevalue");
		} else if (p.getAge() > 110) {
			e.rejectValue("age", "too.darn.old");
		}
	}
}
kotlin
class PersonValidator : Validator {

	/**
	 * 此 Validator 仅校验 Person 实例
	 */
	override fun supports(clazz: Class<*>): Boolean {
		return Person::class.java == clazz
	}

	override fun validate(obj: Any, e: Errors) {
		ValidationUtils.rejectIfEmpty(e, "name", "name.empty")
		val p = obj as Person
		if (p.age < 0) {
			e.rejectValue("age", "negativevalue")
		} else if (p.age > 110) {
			e.rejectValue("age", "too.darn.old")
		}
	}
}

ValidationUtils 类上的静态 rejectIfEmpty(..) 方法用于在 name 属性为 null 或空字符串时拒绝该属性。查看 ValidationUtils 的 JavaDoc,了解除了前面显示的示例之外它还提供了哪些功能。

虽然完全可以实现一个单一的 Validator 类来校验复杂对象中的每个嵌套对象,但更好的做法是将每个嵌套类对象的校验逻辑封装在其自己的 Validator 实现中。一个“复杂”对象的简单示例是 Customer,它由两个字符串属性(名字和姓氏)和一个复杂的 Address 对象组成。Address 对象可以独立于 Customer 对象使用,因此实现了一个独立的 AddressValidator。如果你希望你的 CustomerValidator 重用 AddressValidator 类中包含的逻辑而不采用复制粘贴,你可以在 CustomerValidator 中依赖注入或实例化一个 AddressValidator,如下例所示:

java
public class CustomerValidator implements Validator {

	private final Validator addressValidator;

	public CustomerValidator(Validator addressValidator) {
		if (addressValidator == null) {
			throw new IllegalArgumentException("提供的 [Validator] 是必需的,且不能为空。");
		}
		if (!addressValidator.supports(Address.class)) {
			throw new IllegalArgumentException("提供的 [Validator] 必须支持 [Address] 实例的校验。");
		}
		this.addressValidator = addressValidator;
	}

	/**
	 * 此 Validator 校验 Customer 实例,以及 Customer 的任何子类
	 */
	public boolean supports(Class clazz) {
		return Customer.class.isAssignableFrom(clazz);
	}

	public void validate(Object target, Errors errors) {
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
		Customer customer = (Customer) target;
		try {
			errors.pushNestedPath("address");
			ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
		} finally {
			errors.popNestedPath();
		}
	}
}
kotlin
class CustomerValidator(private val addressValidator: Validator) : Validator {

	init {
		if (addressValidator == null) {
			throw IllegalArgumentException("提供的 [Validator] 是必需的,且不能为空。")
		}
		if (!addressValidator.supports(Address::class.java)) {
			throw IllegalArgumentException("提供的 [Validator] 必须支持 [Address] 实例的校验。")
		}
	}

	/**
	 * 此 Validator 校验 Customer 实例,以及 Customer 的任何子类
	 */
	override fun supports(clazz: Class<*>): Boolean {
		return Customer::class.java.isAssignableFrom(clazz)
	}

	override fun validate(target: Any, errors: Errors) {
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required")
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required")
		val customer = target as Customer
		try {
			errors.pushNestedPath("address")
			ValidationUtils.invokeValidator(this.addressValidator, customer.address, errors)
		} finally {
			errors.popNestedPath()
		}
	}
}

校验错误会被报告给传递给校验器的 Errors 对象。在 Spring Web MVC 的情况下,你可以使用 <spring:bind/> 标签来检查错误消息,但你也可以自己检查 Errors 对象。有关它提供的方法的更多信息可以在 JavaDoc 中找到。

校验器也可以被局部调用以立即校验给定对象,而不涉及绑定过程。从 6.1 开始,通过新的 Validator.validateObject(Object) 方法简化了这一过程,该方法现在默认可用,返回一个简单的 Errors 表示,可以进行检查:通常调用 hasErrors() 或新的 failOnError 方法,将错误摘要消息转换为异常(例如,validator.validateObject(myObject).failOnError(IllegalArgumentException::new))。


补充教学

1. 为什么在有了 Bean Validation (JSR-303) 后仍需要 Spring Validator?

虽然 @NotNull 等注解非常方便,但 Spring 原生 Validator 在以下场景中更具优势:

  • 复杂的跨字段校验:例如“如果 A 字段为 X,则 B 字段不能为空”。
  • 业务逻辑依赖:校验逻辑需要查询数据库或调用外部服务(在实现类中可以轻松注入 Service/Repository)。
  • 动态规则:校验规则根据运行时状态发生变化。
  • 非 POJO 校验:校验不方便添加注解的对象。

2. Errors 接口与 BindingResult

在 Spring MVC 控制器中,你经常会看到 BindingResult 参数。其实 BindingResult 接口扩展了 Errors 接口。 当你调用 validator.validate(target, errors) 时,报错信息会存入 BindingResult,你可以通过 errors.hasErrors() 检查,并使用 errors.getAllErrors() 获取详细信息。

3. pushNestedPath 和 popNestedPath 的妙用

正如示例中所示,处理嵌套对象(如 Customer 里的 Address)时,使用 errors.pushNestedPath("address") 可以方便地将错误路径设置为 address.city 而不仅仅是 city。 这对于在 Web 页面上回显错误消息非常关键,因为它能帮助前端代码准确地将错误定位到对应的输入框。

4. Spring 6.1 新特性:validateObject

过去,如果你想在代码中手动触发校验,需要自己创建一个 BeanPropertyBindingResult 对象,这很繁琐:

java
// 旧写法
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) { ... }

Spring 6.1+ 推荐新写法:

java
// 新写法
validator.validateObject(person).failOnError(IllegalArgumentException::new);

这一改进极大地提升了编程式校验的开发体验,使其更符合流式 API 的风格。

Based on Spring Framework.