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

 更新时间:2025年02月25日 16:06:35   作者:小巫程序Demo日记  
SpringValidator学习指南从零掌握对象校验,涵盖Validator接口、嵌套对象处理、错误代码解析等核心概念,帮助开发者实现数据校验的规范与高效,本文详细介绍Spring Validator从零掌握对象校验,感兴趣的朋友一起看看吧

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

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=街道地址不能为空

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

  • 代码复用AddressValidator 可被其他需要校验地址的类(如 OrderCompany)直接使用。
  • 单一职责:每个 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 使用字段的简单类名(如 intString)。对于自定义类型(如 Address),代码中会使用 address(类名小写)。

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

  • 灵活覆盖:允许针对特定字段或类型定制消息,同时提供通用兜底。
  • 国际化友好:不同语言可定义不同层级的消息,无需修改代码。
  • 代码解耦:校验逻辑与具体错误消息分离,提高可维护性。

学习建议

  • 通过调试观察 errors.getCodes() 的输出,深入理解代码生成规则。
  • 在项目中优先使用字段级错误代码(如 too.darn.old.age),提高错误消息的精准度。

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

相关文章

  • Java基础必学TreeSet集合

    Java基础必学TreeSet集合

    这篇文章主要介绍了Java必学基础TreeSet集合,TreeSet集合实现了SortedSet接口, 可以对集合中元素进行自然排序, 要求集合中的元素必须是可比较的。下文详细介绍需要的朋友可以参考一下
    2022-04-04
  • MyEclipse安装JS代码提示的教程(Spket插件)

    MyEclipse安装JS代码提示的教程(Spket插件)

    本篇文章主要介绍了MyEclipse安装JS代码提示的教程(Spket插件),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-12-12
  • maven打包zip包含bin下启动脚本的完整代码

    maven打包zip包含bin下启动脚本的完整代码

    这篇文章主要介绍了maven打包zip包含bin下启动脚本,本文给大家讲解的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-10-10
  • SpringBoot中使用Guava实现单机令牌桶限流的示例

    SpringBoot中使用Guava实现单机令牌桶限流的示例

    本文主要介绍了SpringBoot中使用Guava实现单机令牌桶限流的示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • 教你如何把Eclipse创建的Web项目(非Maven)导入Idea

    教你如何把Eclipse创建的Web项目(非Maven)导入Idea

    这篇文章主要介绍了教你如何把Eclipse创建的Web项目(非Maven)导入Idea,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-04-04
  • 关于jvm内存如何查看

    关于jvm内存如何查看

    本文介绍了如何在Java虚拟机(JVM)中进行内存管理,包括查看JVM内存情况的常用方法和工具,这些方法包括使用JDK自带的工具如jps、jstat、jmap和jconsole,以及一些第三方监控工具如Prometheus、Grafana和ElasticStack等,通过这些方法
    2024-09-09
  • 小米推送Java代码

    小米推送Java代码

    今天小编就为大家分享一篇关于小米推送Java代码,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-01-01
  • Java框架设计灵魂之反射的示例详解

    Java框架设计灵魂之反射的示例详解

    反射就是把Java类中的各个成员映射成一个个的Java对象。本文将通过示例为大家详细讲解Java框架设计的灵魂:反射,感兴趣的可以了解一下
    2022-06-06
  • Spring.Net IOC依赖注入原理流程解析

    Spring.Net IOC依赖注入原理流程解析

    这篇文章主要介绍了Spring.Net IOC依赖注入原理流程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-07-07
  • 浅谈Java并发中ReentrantLock锁应该怎么用

    浅谈Java并发中ReentrantLock锁应该怎么用

    本文主要介绍了ava并发中ReentrantLock锁的具体使用,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-11-11

最新评论