使用 Spring 的 Validator 接口进行校验
Spring 提供了一个 Validator 接口,你可以使用它来校验对象。Validator 接口通过使用 Errors 对象来工作,这样在校验过程中,校验器可以将校验失败的情况报告给 Errors 对象。
考虑以下一个小的数据对象示例:
public class Person {
private String name;
private int age;
// 通常的 getter 和 setter...
}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:
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");
}
}
}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,如下例所示:
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();
}
}
}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 对象,这很繁琐:
// 旧写法
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) { ... }Spring 6.1+ 推荐新写法:
// 新写法
validator.validateObject(person).failOnError(IllegalArgumentException::new);这一改进极大地提升了编程式校验的开发体验,使其更符合流式 API 的风格。