1. 引言

Spring 的验证框架主要设计用于 JavaBeans,其中每个字段可以用验证注解约束。

本教程将探讨 **如何使用 Spring 的 Validator 接口验证 *Map<String, String>***。这种方法在处理无法直接映射到预定义 Java 对象的动态键值对时特别有用。

2. 问题解析 – Hibernate Validator 与 Map 的兼容性

在实现自定义验证器之前,很自然地会尝试直接在 Map 结构上使用标准约束注解(如 Hibernate Validator 和 Spring 的 @Valid/@Validated)。但这种方法往往达不到预期效果

看个例子:

Map<@Length(min = 10) String, @NotBlank String> givenMap = new HashMap<>();
givenMap.put("tooShort", "");

Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Map<String, String>>> violations = validator.validate(givenMap);
        
Assertions.assertThat(violations).isNotEmpty(); // 这里会失败

尽管使用了类型参数注解,但 violations 集合会是空的——不会检测到任何约束违规。

2.1. 为什么会失败?

Hibernate Validator(或 Bean Validation)基于 JavaBeans 约定运行,即通过 getter 验证对象属性。由于 Map 不将键和值暴露为属性,@Length@NotBlank 这样的约束注解无法直接应用,验证时会被忽略

简单说:对验证器而言,Map 是个黑盒子——除非明确告知如何检查内容,否则它无法解析内部结构。

2.2. 何时才有效?

当 Map 作为 JavaBean 的属性时,类型级约束注解可以生效:

public class WrappedMap {
    private Map<@Length(min = 10) String, @NotBlank String> map;

    // 构造方法、getter/setter...
}

这得益于 Hibernate Validator 对容器元素约束的支持。但对 Map 的键值验证仍然有限且不一致,可能需要显式启用值提取器,即使如此也不能保证完全支持。

要解决这个限制,我们可以通过实现 Spring 提供的 Validator 接口创建自定义验证器

3. 项目配置

实现解决方案前,需添加必要依赖。如果使用 Spring Boot,一个 starter 即可搞定:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>3.4.5</version>
</dependency>

若使用纯 Spring Framework,需手动添加以下依赖:

<!-- Spring Framework 核心 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.2.6</version>
</dependency>

<!-- 验证相关 -->
<dependency>
    <groupId>jakarta.validation</groupId>
    <artifactId>jakarta.validation-api</artifactId>
    <version>3.1.1</version>
</dependency>
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>8.0.2.Final</version>
</dependency>

这些依赖让我们能为 Map 结构实现自定义 Validator

4. 实现自定义验证器

配置好项目后,开始实现自定义验证器。我们将复制前述代码片段的验证规则,同时检查键和值:

@Service
public class MapValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
        return Map.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        Map<?, ?> rawMap = (Map<?, ?>) target;

        for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
            Object rawKey = entry.getKey();
            Object rawValue = entry.getValue();

            if (!(rawKey instanceof String key) || !(rawValue instanceof String value)) {
                errors.rejectValue("map[" + rawKey + "]", "map.entry.invalidType", "Map 必须只包含 String 类型的键和值");
                continue;
            }

            // 键验证
            if (key.length() < 10) {
                errors.rejectValue("map[" + key + "]", "key.tooShort", "键长度必须至少 10 个字符");
            }

            // 值验证
            if (!StringUtils.hasText(value)) {
                errors.rejectValue("map[" + key + "]", "value.blank", "值不能为空");
            }
        }
    }
}

该类实现了 Spring 的 Validator 接口,需实现两个方法:

  • supports(Class<?> clazz) – 判断此验证器是否支持给定类
  • validate(Object target, Errors errors) – 执行实际验证并报告约束违规

⚠️ 注意:我们显式检查键值类型以确保类型安全,避免运行时 ClassCastException

5. 调用验证器

Spring 验证框架与服务类无缝集成,允许创建可复用代码,在需要处注入并使用自定义验证器。

现在可在任何 Spring 管理的服务中注入并使用自定义验证器:

@Service
public class MapService {
    private final MapValidator mapValidator;

    @Autowired
    public MapService(MapValidator mapValidator) {
        this.mapValidator = mapValidator;
    }

    public void process(Map<String, String> inputMap) {
        // 将 Map 包装到绑定结构中用于验证
        MapBindingResult errors = new MapBindingResult(inputMap, "inputMap");

        // 执行验证
        mapValidator.validate(inputMap, errors);

        // 处理验证错误
        if (errors.hasErrors()) {
            throw new IllegalArgumentException("验证失败: " + errors.getAllErrors());
        }
        // 业务逻辑...
    }
}

此示例展示了如何通过构造函数注入 MapValidator,并在执行核心业务逻辑前调用它。将 Map 包装到 MapBindingResult 中,使 Spring 能一致地收集和结构化验证错误。

6. 结论

在 Spring 中验证 Map<String, String> 结构需要自定义方案,因为标准验证机制默认不会检查 Map 内容。Bean Validation 的支持有限且可能不符合预期。

通过实现 Validator 接口并集成到服务层,我们能完全控制每个键值对的验证方式,使应用更健壮灵活。此策略在处理动态输入(如配置、用户自定义表单或第三方 JSON 结构)时尤其有用。

本文所有代码示例及更多案例可在 GitHub 获取。