Spring Validator从零掌握对象校验的详细过程
作者:小巫程序Demo日记
Spring Validator 学习指南:从零掌握对象校验
一、Validator 接口的作用:你的数据“守门员”
想象你开发了一个用户注册功能,用户提交的数据可能有各种问题:名字没填、年龄写成了负数……这些错误如果直接保存到数据库,会导致后续流程出错。Validator 就像一位严格的守门员,在数据进入系统前,检查每个字段是否符合规则。
核心任务:
- 检查对象属性是否合法(如非空、数值范围)。
- 收集错误信息,方便后续提示用户。
二、Validator 接口的两大方法:如何工作?
1. supports(Class clazz)
:我能处理这个对象吗?
- 作用:判断当前 Validator 是否支持校验某个类的对象。
- 关键选择:
- 精确匹配:
return Person.class.equals(clazz);
→ 只校验Person
类。 - 灵活匹配:
return Person.class.isAssignableFrom(clazz);
→ 支持Person
及其子类。
- 精确匹配:
示例场景:
- 如果你有一个
Student extends Person
,使用equals
时,Student
对象不会被校验;使用isAssignableFrom
则会校验。
2. validate(Object target, Errors errors)
:执行校验!
- 作用:编写具体的校验规则,发现错误时记录到
Errors
对象。 - 常用工具:
ValidationUtils
简化非空检查。
示例代码:
public void validate(Object target, Errors errors) { // 检查 name 是否为空 ValidationUtils.rejectIfEmpty(errors, "name", "name.empty"); Person person = (Person) target; // 检查年龄是否合法 if (person.getAge() < 0) { errors.rejectValue("age", "negative.age", "年龄不能为负数!"); } }
三、处理嵌套对象:如何避免重复代码?
假设你有一个 Customer
类,包含 Address
对象:
public class Customer { private String firstName; private String surname; private Address address; // 嵌套对象 }
问题:直接在一个 Validator 中校验所有字段
缺点:
- 若其他类(如
Order
)也包含Address
,需重复编写地址校验代码。 - 维护困难:修改地址规则时,需改动多处代码。
正确做法:
拆分 Validator,组合使用!
步骤 1:为每个类创建独立的 Validator
- AddressValidator(校验地址):
public class AddressValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return Address.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { ValidationUtils.rejectIfEmpty(errors, "street", "street.empty"); ValidationUtils.rejectIfEmpty(errors, "city", "city.empty"); } }
CustomerValidator(校验客户,并复用 AddressValidator):
public class CustomerValidator implements Validator { private final Validator addressValidator; // 通过构造函数注入 AddressValidator public CustomerValidator(Validator addressValidator) { this.addressValidator = addressValidator; } @Override public boolean supports(Class<?> clazz) { return Customer.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { // 1. 校验客户的直属字段(firstName, surname) ValidationUtils.rejectIfEmpty(errors, "firstName", "firstName.empty"); ValidationUtils.rejectIfEmpty(errors, "surname", "surname.empty"); Customer customer = (Customer) target; Address address = customer.getAddress(); // 2. 校验嵌套的 Address 对象 if (address == null) { errors.rejectValue("address", "address.null"); return; } // 3. 关键:切换错误路径到 "address",防止字段名冲突 errors.pushNestedPath("address"); try { ValidationUtils.invokeValidator(addressValidator, address, errors); } finally { errors.popNestedPath(); // 恢复原始路径 } } }
步骤 2:实际使用
// 创建 Validator AddressValidator addressValidator = new AddressValidator(); CustomerValidator customerValidator = new CustomerValidator(addressValidator); // 准备测试数据 Customer customer = new Customer(); customer.setFirstName(""); // 空名字 customer.setAddress(new Address()); // 空地址 // 执行校验 Errors errors = new BeanPropertyBindingResult(customer, "customer"); customerValidator.validate(customer, errors); // 输出错误 if (errors.hasErrors()) { errors.getAllErrors().forEach(error -> { System.out.println("字段:" + error.getObjectName() + "." + error.getCode()); }); } // 输出结果: // 字段:customer.firstName.empty // 字段:customer.address.street.empty
四、关键技巧与常见问题
1. 错误路径管理
pushNestedPath
和 popNestedPath
:
确保嵌套对象的错误字段带上前缀(如 address.street
),避免与主对象的字段名冲突。
2. 防御性编程
在组合 Validator 时,检查注入的 Validator 是否支持目标类型:
public CustomerValidator(Validator addressValidator) { if (!addressValidator.supports(Address.class)) { throw new IllegalArgumentException("必须支持 Address 类型!"); } this.addressValidator = addressValidator; }
3. 国际化支持
错误代码(如 firstName.empty
)可对应语言资源文件(如 messages_zh.properties
),实现多语言提示:
# messages_zh.properties firstName.empty=名字不能为空 address.street.empty=街道地址不能为空
五、总结:为什么这样设计?
- 代码复用:
AddressValidator
可被其他需要校验地址的类(如Order
、Company
)直接使用。 - 单一职责:每个 Validator 只负责一个类的校验,逻辑清晰,易于维护。
- 灵活扩展:新增嵌套对象(如
PaymentInfo
)时,只需创建新的 Validator 并注入,无需修改已有代码。
3.2. 将错误代码解析为错误信息:深入解析与实例演示
一、核心概念:错误代码的多层次解析
当你在 Spring 中调用 rejectValue
方法注册错误时(例如校验用户年龄不合法),Spring 不会只记录你指定的单一错误代码,而是自动生成一组层级化的错误代码。这种设计允许开发者通过不同层级的错误代码,灵活定义错误消息,实现“从具体到通用”的覆盖策略。
二、错误代码生成规则
假设在 PersonValidator
中触发以下校验逻辑:
errors.rejectValue("age", "too.darn.old");
生成的错误代码(按优先级从高到低):
too.darn.old.age.int
→ 字段名 + 错误代码 + 字段类型too.darn.old.age
→ 字段名 + 错误代码too.darn.old
→ 原始错误代码
三、消息资源文件的匹配策略
Spring 的 MessageSource
会按照错误代码的优先级顺序,在消息资源文件(如 messages.properties
)中查找对应的消息。一旦找到匹配项,立即停止搜索。
示例消息资源文件:
# messages.properties too.darn.old.age.int=年龄必须是整数且不超过 120 岁 too.darn.old.age=年龄不能超过 120 岁 too.darn.old=输入的值不合理
匹配过程:
- 优先查找
too.darn.old.age.int
→ 若存在则使用。 - 若未找到,查找
too.darn.old.age
→ 若存在则使用。 - 最后查找
too.darn.old
→ 作为兜底消息。
四、实战演示:从代码到错误消息
步骤 1:创建实体类与校验器
// Person.java public class Person { private String name; private int age; // getters/setters } // PersonValidator.java public class PersonValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return Person.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { Person person = (Person) target; if (person.getAge() > 120) { errors.rejectValue("age", "too.darn.old"); } } }
步骤 2:配置消息资源文件
在 src/main/resources/messages.properties
中定义:
# 具体到字段类型 too.darn.old.age.int=年龄必须是整数且不能超过 120 岁 # 具体到字段 too.darn.old.age=年龄不能超过 120 岁 # 通用错误 too.darn.old=输入的值无效
步骤 3:编写测试代码
@SpringBootTest public class ValidationTest { @Autowired private MessageSource messageSource; @Test public void testAgeValidation() { Person person = new Person(); person.setAge(150); // 触发错误 Errors errors = new BeanPropertyBindingResult(person, "person"); PersonValidator validator = new PersonValidator(); validator.validate(person, errors); // 提取错误消息 errors.getFieldErrors("age").forEach(error -> { String message = messageSource.getMessage(error.getCode(), null, Locale.getDefault()); System.out.println("错误消息:" + message); }); } }
输出结果:
错误消息:年龄必须是整数且不能超过 120 岁
解析:
因为 too.darn.old.age.int
在消息文件中存在,优先使用该消息。若删除这行,则会匹配 too.darn.old.age
,以此类推。
五、自定义错误代码生成策略
默认的 DefaultMessageCodesResolver
生成的代码格式为:错误代码 + 字段名 + 字段类型
。
若需修改规则,可自定义 MessageCodesResolver
。
示例:简化错误代码
@Configuration public class ValidationConfig { @Bean public MessageCodesResolver messageCodesResolver() { DefaultMessageCodesResolver resolver = new DefaultMessageCodesResolver(); resolver.setMessageCodeFormatter(DefaultMessageCodesResolver.Format.POSTFIX_ERROR_CODE); return resolver; } }
效果:
调用 rejectValue("age", "too.darn.old")
生成的代码变为:
age.too.darn.old
too.darn.old
六、常见问题与解决方案
问题 1:如何查看实际生成的错误代码?
在测试代码中打印错误对象:
errors.getFieldErrors("age").forEach(error -> { System.out.println("错误代码列表:" + Arrays.toString(error.getCodes())); });
输出:
错误代码列表:[too.darn.old.age.int, too.darn.old.age, too.darn.old]
问题 2:字段类型在代码中如何表示?
Spring 使用字段的简单类名(如 int
、String
)。对于自定义类型(如 Address
),代码中会使用 address
(类名小写)。
七、总结:为何需要层级化错误代码?
- 灵活覆盖:允许针对特定字段或类型定制消息,同时提供通用兜底。
- 国际化友好:不同语言可定义不同层级的消息,无需修改代码。
- 代码解耦:校验逻辑与具体错误消息分离,提高可维护性。
学习建议:
- 通过调试观察
errors.getCodes()
的输出,深入理解代码生成规则。 - 在项目中优先使用字段级错误代码(如
too.darn.old.age
),提高错误消息的精准度。
到此这篇关于Spring Validator 学习指南:从零掌握对象校验的文章就介绍到这了,更多相关Spring Validator 对象校验内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!