java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Validator 对象校验

Spring Validator从零掌握对象校验的详细过程

作者:小巫程序Demo日记

SpringValidator学习指南从零掌握对象校验,涵盖Validator接口、嵌套对象处理、错误代码解析等核心概念,帮助开发者实现数据校验的规范与高效,本文详细介绍Spring Validator从零掌握对象校验,感兴趣的朋友一起看看吧

Spring Validator 学习指南:从零掌握对象校验

一、Validator 接口的作用:你的数据“守门员”

想象你开发了一个用户注册功能,用户提交的数据可能有各种问题:名字没填、年龄写成了负数……这些错误如果直接保存到数据库,会导致后续流程出错。Validator 就像一位严格的守门员,在数据进入系统前,检查每个字段是否符合规则。

核心任务

二、Validator 接口的两大方法:如何工作?

1. supports(Class clazz):我能处理这个对象吗?

示例场景

2. validate(Object target, Errors errors):执行校验!

示例代码

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 中校验所有字段

缺点

正确做法:

拆分 Validator,组合使用!

步骤 1:为每个类创建独立的 Validator

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. 错误路径管理

pushNestedPathpopNestedPath
确保嵌套对象的错误字段带上前缀(如 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=街道地址不能为空

五、总结:为什么这样设计?

3.2. 将错误代码解析为错误信息:深入解析与实例演示

一、核心概念:错误代码的多层次解析

当你在 Spring 中调用 rejectValue 方法注册错误时(例如校验用户年龄不合法),Spring 不会只记录你指定的单一错误代码,而是自动生成一组层级化的错误代码。这种设计允许开发者通过不同层级的错误代码,灵活定义错误消息,实现“从具体到通用”的覆盖策略。

二、错误代码生成规则

假设在 PersonValidator 中触发以下校验逻辑:

errors.rejectValue("age", "too.darn.old");

生成的错误代码(按优先级从高到低):

三、消息资源文件的匹配策略

Spring 的 MessageSource 会按照错误代码的优先级顺序,在消息资源文件(如 messages.properties)中查找对应的消息。一旦找到匹配项,立即停止搜索

示例消息资源文件

# messages.properties
too.darn.old.age.int=年龄必须是整数且不超过 120 岁
too.darn.old.age=年龄不能超过 120 岁
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") 生成的代码变为:

六、常见问题与解决方案

问题 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 使用字段的简单类名(如 intString)。对于自定义类型(如 Address),代码中会使用 address(类名小写)。

七、总结:为何需要层级化错误代码?

学习建议

到此这篇关于Spring Validator 学习指南:从零掌握对象校验的文章就介绍到这了,更多相关Spring Validator 对象校验内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文