Spring WebFlux 如何捕获InvalidTypeIdException及定制Jackson校验规则
核心问题实现方案
一、在全局异常处理器中捕获包装后的InvalidTypeIdException
Spring WebFlux在请求体绑定流程中,会将Jackson抛出的底层反序列化异常统一包装为ServerWebInputException,不会将InvalidTypeIdException作为顶层异常抛出,无法直接在全局处理器中匹配捕获。
实现方式非常简单,只需要在异常处理链路中递归遍历异常根因链,定位目标异常即可:
- 实现通用的异常链遍历工具方法
private <T extends Throwable> T findCauseInChain(Throwable source, Class<T> targetType) { Throwable current = source; while (current != null) { if (targetType.isInstance(current)) { return targetType.cast(current); } current = current.getCause(); } return null; } - 在继承
AbstractErrorWebExceptionHandler的全局异常处理器中,重写错误响应渲染逻辑@Override protected Mono<Object> renderErrorResponse(ServerRequest request) { Throwable error = getError(request); // 从异常链中定位Jackson抛出的InvalidTypeIdException InvalidTypeIdException typeIdException = findCauseInChain(error, InvalidTypeIdException.class); if (typeIdException != null) { // 返回预设的标准化错误响应,完全替代临时的手动异常捕获逻辑 Map<String, Object> errorBody = Map.of( "code", 400, "message", "非法的type类型值,允许值为:" + typeIdException.getTypeResolver().getDescForKnownTypeIds() ); return Mono.just(ServerResponse.badRequest().bodyValue(errorBody)); } // 保留原有其他异常处理逻辑 return super.renderErrorResponse(request); }
通过这种方式可以完全移除当前临时使用的bodyToMono(String.class) + 手动JSON解析的代码,直接使用默认的request.bodyToMono(Parent.class)绑定逻辑即可。
二、在Jackson反序列化阶段实现等价的自定义校验,替代Bean Validation后置校验
当前自定义校验逻辑无法触发的核心原因是:Jackson的多态类型解析逻辑运行在反序列化流程的早期阶段,遇到非法type值会直接抛出异常,根本无法走到后续的Bean Validation字段校验流程。可以通过两种无侵入的方式,在Jackson层完成和@ValueOfEnum完全对齐的校验逻辑:
方案1:自定义DeserializationProblemHandler(低侵入)
不需要修改原有类上的Jackson注解配置,只需要注册问题处理器,在Jackson遇到未知类型ID时介入校验:
- 实现自定义问题处理器,嵌入枚举值校验逻辑
public class TypeValidatingProblemHandler extends DeserializationProblemHandler { @Override public JavaType handleUnknownTypeId(DeserializationContext ctxt, JavaType baseType, String subTypeId, TypeIdResolver idResolver, String failureMsg) throws IOException { // 完全复用原@ValueOfEnum的校验规则 Set<String> allowedEnumValues = Arrays.stream(MyEnum.values()) .map(Enum::name) .collect(Collectors.toSet()); if (!allowedEnumValues.contains(subTypeId)) { // 抛出带预设提示的校验异常,和Bean Validation的提示格式保持一致 throw new ConstraintViolationException("type字段必须是MyEnum枚举的合法值,当前传入:" + subTypeId, Collections.emptySet()); } // 若枚举值合法但无对应子类型,走默认错误处理 return super.handleUnknownTypeId(ctxt, baseType, subTypeId, idResolver, failureMsg); } } - 将处理器注册到Spring管理的ObjectMapper实例中
@Bean public Jackson2ObjectMapperBuilderCustomizer registerProblemHandler() { return builder -> builder.postConfigurer(objectMapper -> objectMapper.addHandler(new TypeValidatingProblemHandler()) ); }
方案2:自定义TypeIdResolver(全链路可控)
如果需要完全掌控多态类型解析的全流程,可以自定义类型ID解析器,将校验逻辑嵌入类型匹配的最前置环节:
- 实现自定义解析器
public class EnumValidatingTypeIdResolver extends TypeIdResolverBase { private Set<String> allowedEnumValues; private Map<String, JavaType> subtypeMapping; private TypeFactory typeFactory; @Override public void init(JavaType baseType) { this.typeFactory = baseType.getTypeFactory(); // 初始化允许的枚举值列表,和@ValueOfEnum规则对齐 this.allowedEnumValues = Arrays.stream(MyEnum.values()) .map(Enum::name) .collect(Collectors.toSet()); // 初始化子类型映射,替代原有@JsonSubTypes的配置 this.subtypeMapping = Map.of( "CUSTOM_TYPE", typeFactory.constructParametricType(Child.class, Integer.class) ); } @Override public String idFromValue(Object value) { return subtypeMapping.entrySet().stream() .filter(e -> e.getValue().equals(value.getClass())) .map(Map.Entry::getKey) .findFirst() .orElseThrow(); } @Override public String idFromValueAndType(Object value, Class<?> suggestedType) { return idFromValue(value); } @Override public JsonTypeInfo.Id getMechanism() { return JsonTypeInfo.Id.CUSTOM; } @Override public JavaType typeFromId(DatabindContext context, String id) throws IOException { // 前置执行枚举值校验 if (!allowedEnumValues.contains(id)) { throw new ConstraintViolationException("type字段必须是MyEnum枚举的合法值,当前传入:" + id, Collections.emptySet()); } // 匹配对应子类型 JavaType matchedType = subtypeMapping.get(id); if (matchedType == null) { throw new InvalidTypeIdException(context, id, "当前type值无对应可处理的子类型", Parent.class); } return matchedType; } } - 替换原有Parent类上的
@JsonTypeInfo注解,指定使用自定义解析器@JsonTypeInfo( use = JsonTypeInfo.Id.CUSTOM, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true, resolver = EnumValidatingTypeIdResolver.class ) abstract class Parent<T> { // 原有类字段、方法保持不变 }
两种方案都可以保证校验逻辑在反序列化阶段触发,抛出的异常会被全局异常处理器统一捕获处理,不需要再保留手动解析JSON的临时代码,错误提示也能和原有自定义校验的规则完全一致。
内容的提问来源于stack exchange,提问作者Serg
相关产品推荐
相关产品推荐

