Spring 4.3.14.RELEASE中如何在自定义验证器中实现级联验证?
如何在自定义Map验证器中实现级联验证(像@Valid一样验证内部对象)
我在Spring 4.3.14.RELEASE项目的MVC层中,需要验证Map<String, List<InnerObj>>类型的对象。我已经编写了自定义验证器和对应的注解,但不知道怎么让验证规则像@Valid注解一样,级联验证内部的List集合及InnerObj对象。
我的自定义验证器代码:
public class MapValidator implements ConstraintValidator<ValidMap, Map<String, List<InnerObj>>> { @Override public boolean isValid(final Map<String, List<InnerObj>> map, final ConstraintValidatorContext context) { if (map == null || map.size() == 0) { return false; } // 遍历所有对象并执行验证 return true; } }
对应的注解:
@Target({METHOD, FIELD, ANNOTATION_TYPE}) @Retention(RUNTIME) @Documented @Constraint(validatedBy = MapValidator.class) public @interface ValidMap { String message() default "valid.map"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
解决方案
我来帮你搞定这个级联验证的需求。要让自定义Map验证器像@Valid一样递归验证内部的InnerObj,核心是在验证器里手动触发JSR-303的验证逻辑,并把内部对象的错误信息整合到当前的验证上下文里,这样这些错误会被Spring MVC的BindingResult正常收集。具体步骤如下:
1. 在自定义验证器中注入Validator实例
首先,我们需要在MapValidator里拿到JSR-303的Validator对象,Spring会自动帮我们注入这个实例:
import javax.validation.Validator; import javax.validation.ConstraintViolation; import org.springframework.beans.factory.annotation.Autowired; import java.util.Set; public class MapValidator implements ConstraintValidator<ValidMap, Map<String, List<InnerObj>>> { @Autowired private Validator validator; @Override public void initialize(ValidMap constraintAnnotation) { // 这里可以放初始化逻辑,比如读取注解参数,不需要的话留空就行 } @Override public boolean isValid(final Map<String, List<InnerObj>> map, final ConstraintValidatorContext context) { if (map == null || map.size() == 0) { return false; } boolean isMapValid = true; // 先关闭默认的错误提示,避免重复输出默认消息 context.disableDefaultConstraintViolation(); // 遍历Map里的每一组键值对 for (Map.Entry<String, List<InnerObj>> entry : map.entrySet()) { String mapKey = entry.getKey(); List<InnerObj> innerObjList = entry.getValue(); // 先校验List本身是否为空 if (innerObjList == null) { context.buildConstraintViolationWithTemplate("列表[key: " + mapKey + "]不能为空") .addPropertyNode(mapKey) .addConstraintViolation(); isMapValid = false; continue; } // 遍历List里的每个InnerObj,执行级联验证 for (int objIndex = 0; objIndex < innerObjList.size(); objIndex++) { InnerObj innerObj = innerObjList.get(objIndex); // 触发InnerObj上的所有JSR-303验证规则 Set<ConstraintViolation<InnerObj>> objViolations = validator.validate(innerObj); if (!objViolations.isEmpty()) { isMapValid = false; // 把每个InnerObj的错误信息添加到上下文,同时构建清晰的错误路径 for (ConstraintViolation<InnerObj> violation : objViolations) { context.buildConstraintViolationWithTemplate(violation.getMessage()) // 错误路径格式:map字段名[mapKey][objIndex].innerObj字段名 .addPropertyNode(mapKey) .addPropertyNode(String.valueOf(objIndex)) .addPropertyNode(violation.getPropertyPath().toString()) .addConstraintViolation(); } } } } return isMapValid; } }
2. 确保InnerObj上的验证注解生效
你的InnerObj类需要标注标准的JSR-303验证注解,这样上面的validator.validate()才能检测到错误,比如:
import javax.validation.constraints.NotNull; import javax.validation.constraints.Min; public class InnerObj { @NotNull(message = "名称不能为空") private String name; @Min(value = 18, message = "年龄不能小于18岁") private Integer age; // 别忘了添加getter和setter方法 public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } }
3. 在目标Map字段上使用自定义注解
最后,在你需要验证的Map字段上标注@ValidMap即可,不需要额外加@Valid,因为我们已经在自定义验证器里实现了级联逻辑:
public class MyRequestDto { @ValidMap private Map<String, List<InnerObj>> dataMap; // getter和setter public Map<String, List<InnerObj>> getDataMap() { return dataMap; } public void setDataMap(Map<String, List<InnerObj>> dataMap) { this.dataMap = dataMap; } }
一些注意事项
- 如果你的项目没有用Spring的依赖注入,也可以通过
Validation.buildDefaultValidatorFactory().getValidator()手动获取Validator实例,但在Spring环境下注入是更优雅的方式。 - 构建错误路径很重要,这样前端能准确知道是Map里哪个key下的第几个InnerObj的哪个字段出了问题。
- 关闭默认约束违反消息是为了避免出现重复的错误提示,确保只有我们自定义的错误信息被输出。
内容的提问来源于stack exchange,提问作者Vasiliy Tungusov
相关产品推荐
相关产品推荐

