使用 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 校验生效的场景
当 Map
是 JavaBean
中的一个属性时,约束注解就会起作用:
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>
结构需要自定义方法,因为标准验证机制默认情况下不会内省(introspect) Map
内容。
通过自定义 Validator
接口实现,我们就能完全控制每个键值对的验证方式,从而使应用更健壮、更灵活。在处理配置、用户自定义表单或第三方 JSON 结构等动态输入时,这种策略尤其有用。
Ref:https://www.baeldung.com/spring-validator-map