1. 介紹
Spring 的驗證框架主要設計用於與 JavaBeans 配合使用,其中每個字段都可以使用驗證約束進行標註。
在本教程中,我們將探索如何使用 Spring 的 <em>Validator</em> 接口驗證一個 <em>Map<String, String></em>。這種方法特別適用於處理動態鍵值對,這些鍵值對不直接映射到預定義的 Java 對象。
2. 理解問題——Hibernate Validator 和 Maps
在實施自定義驗證器之前,自然會嘗試直接在 Maps 結構中使用 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(); // this will fall儘管有標註的類型參數,但違規集將為空——不會檢測到任何約束違規。
2.1. 為什麼會失敗?
Hibernate Validator,或者 Bean Validation 總體上基於 JavaBeans 約定,這意味着它會驗證通過 getter 訪問的對象的屬性。由於映射(maps)不暴露鍵和值作為屬性,因此 約束註解(如@Length 或 @NotBlank)在驗證過程中會被忽略,因為它們不適用。
換句話説,從驗證器的角度來看,映射就像一個黑盒——它不知道如何內省其內容,除非明確地告訴它如何。
2.2. 何時有效?
類型級別約束註解可以在 JavaBean 中,當 map 作為屬性存在時生效,例如:
public class WrappedMap {
private Map<@Length(min = 10) String, @NotBlank String> map;
// constructor, getters, setters...
}這得益於 Hibernate Validator 對容器元素約束的支持。然而,鍵和值的驗證仍然有限且不一致,尤其是在處理映射時。您可能需要顯式啓用值提取器,即使這樣也不能保證完全支持。
為了解決這一限制,您可以通過實現 Spring 提供的 Validator 接口來創建自定義驗證器。
3. 項目配置
在實施我們的解決方案之前,我們需要使用必要的依賴項配置項目。 如果我們使用的是 Spring Boot,則所有內容都包含在一個 單啓動器中:
<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>這些依賴項使我們能夠實現和使用自定義的 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 must contain only String keys and values");
continue;
}
// Key validation
if (key.length() < 10) {
errors.rejectValue("map[" + key + "]", "key.tooShort", "Key must be at least 10 characters long");
}
// Value validation
if (!StringUtils.hasText(value)) {
errors.rejectValue("map[" + key + "]", "value.blank", "Value must not be 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) {
// Wrap the map in a binding structure for validation
MapBindingResult errors = new MapBindingResult(inputMap, "inputMap");
// Run validation
mapValidator.validate(inputMap, errors);
// Handle validation errors
if (errors.hasErrors()) {
throw new IllegalArgumentException("Validation failed: " + errors.getAllErrors());
}
// Business logic goes here...
}
}此示例展示瞭如何使用構造注入注入 MapValidator 並在使用核心業務邏輯之前調用它。 將地圖包裝在 MapBindingResult 中允許 Spring 收集和結構化驗證錯誤。
6. 結論
驗證 Spring 中 Map<String, String> 結構的有效性需要採用自定義方法,因為標準驗證機制默認不具備內省 map 內容的功能。 Bean 驗證的 支持有限,可能無法按預期工作。
通過實現 Validator 接口並將其集成到您的服務層中,我們完全掌控了每個鍵值對的驗證方式,從而使我們的應用程序更加健壯和靈活。 這種策略尤其適用於處理動態輸入,例如配置、用户自定義表單或第三方 JSON 結構。