使用 Spring Validator 验证 Map

1、简介

Spring 的 Validation 框架主要设计用于 JavaBean,其中每个字段都可以通过注解来定义验证约束。

本文将带你了解如何使用 Spring 的 Validator 接口验证 Map<String,String>

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(); // 测试失败

尽管在 Map 泛型上添加了约束注解,但校验结果 violations 仍是空的,即未检测到违反约束的情况。

2.1、为什么会校验失败?

Hibernate Validator 或一般的 Bean Validation 是根据 JavaBean 惯例运行的,这意味着它会验证通过 getter 访问的对象属性。由于 Map 不会将键和值作为属性公开,因此 @Length@NotBlank 这样的约束注解并不直接适用,在验证过程中会被忽略

换句话说,从 Validator 的角度来看,Map 就是一个黑盒,除非明确告诉它如何内省(introspect)其内容,否则它不知道如何内省。

2.2、Map 校验生效的场景

MapJavaBean 中的一个属性时,约束注解就会起作用:

public class WrappedMap {

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

    // 忽略构造函数、getter、Setter
}

这要归功于 Hibernate Validator 对容器元素约束的支持。不过,对键和值的验证仍然有限,而且不一致,尤其是对 Map 而言。可能需要明确启用值提取器(Value Extractor),即便如此,也不能保证完全支持。

要解决这一限制,可以通过实现 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,则需要手动加入 Jakarta Validation API 及其 Hibernate 实现:

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

<!-- Validation related -->
<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、实现自定义 Validator

依赖就绪后,就可以开始实现自定义 Validator 了:

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

    @Override
    public void validate(Object target, Errors errors) {
        // 强制转换为 Map
        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 must contain only String keys and values");
                continue;
            }

            // Key 校验
            if (key.length() < 10) {
                errors.rejectValue("map[" + key + "]", "key.tooShort", "Key must be at least 10 characters long");
            }

            // Value 校验
            if (!StringUtils.hasText(value)) {
                errors.rejectValue("map[" + key + "]", "value.blank", "Value must not be blank");
            }
        }
    }
}

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

  • supports(Class clazz) - 确定该 Validator 能否处理给定的类。
  • validate(Object target, Errors errors) - 执行实际验证并报告任何违反约束的情况

注意,这里明确检查了键和值的类型,以确保类型安全,避免在运行时出现 ClassCastException

5、调用 Validator

可以在任何需要的地方注入、使用我们的自定义 Validator

@Service
public class MapService {
    private final MapValidator mapValidator;

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

    public void process(Map<String, String> inputMap) {
        // 将 Map 封装在一个 Binding 结构中进行验证。
        MapBindingResult errors = new MapBindingResult(inputMap, "inputMap");

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

        // 处理校验失败的情况
        if (errors.hasErrors()) {
            throw new IllegalArgumentException("Validation failed: " + errors.getAllErrors());
        }
        // 业务逻辑。。。。
    }
}

上例展示了如何使用构造函数注入 MapValidator,并在执行核心业务逻辑之前调用它。将 Map 封装在 MapBindingResult 中可让 Spring 以一致的方式收集和处理验证错误。

6、总结

在 Spring 中验证 Map<String, String> 结构需要自定义方法,因为标准验证机制默认情况下不会内省(introspectMap 内容。

通过自定义 Validator 接口实现,我们就能完全控制每个键值对的验证方式,从而使应用更健壮、更灵活。在处理配置、用户自定义表单或第三方 JSON 结构等动态输入时,这种策略尤其有用。


Ref:https://www.baeldung.com/spring-validator-map