如何处理单个@RequestBody抛出的多个HttpMessageNotReadableException?
解决方案
实现原理
默认Jackson反序列化时遇到第一个错误就会终止流程并抛出HttpMessageNotReadableException,因此只能捕获到首个错误。要实现全量错误收集,需修改Jackson反序列化配置,关闭快速失败逻辑,同时自定义处理器收集反序列化全流程的所有字段错误,最后统一抛出包含所有错误的自定义异常。
具体实现步骤
1. 自定义反序列化错误收集处理器
继承Jackson的DeserializationProblemHandler,在各类反序列化错误回调中收集字段错误信息,返回true让反序列化流程继续执行:
public class CollectAllDeserializationErrorsHandler extends DeserializationProblemHandler { private final Map<String, String> fieldErrors = new HashMap<>(); // 处理枚举、日期等字符串转目标类型错误 @Override public JavaType handleWeirdStringValue(DeserializationContext ctxt, Class<?> targetType, String valueToConvert, String failureMsg) throws IOException { String currentField = ctxt.getParser().getParsingContext().getCurrentName(); if (targetType.isEnum()) { String allowedValues = Arrays.stream(targetType.getEnumConstants()) .map(Object::toString) .collect(Collectors.joining(",")); fieldErrors.put(currentField, String.format("非法枚举值[%s],允许值:%s", valueToConvert, allowedValues)); } else if (LocalDate.class.isAssignableFrom(targetType)) { fieldErrors.put(currentField, String.format("日期格式错误[%s],请使用yyyy-MM-dd格式", valueToConvert)); } else { fieldErrors.put(currentField, String.format("值格式错误:%s", failureMsg)); } // 跳过当前字段的赋值,继续处理后续字段 return super.handleWeirdStringValue(ctxt, targetType, valueToConvert, failureMsg); } // 处理未知字段错误 @Override public boolean handleUnknownProperty(DeserializationContext ctxt, JsonParser p, JsonDeserializer<?> deserializer, Object beanOrClass, String propertyName) throws IOException { fieldErrors.put(propertyName, "未知请求字段"); // 继续处理后续字段 return true; } public Map<String, String> getFieldErrors() { return fieldErrors; } public boolean hasErrors() { return !fieldErrors.isEmpty(); } }
2. 自定义请求反序列化异常
用于封装收集到的所有字段错误:
public class RequestDeserializationException extends RuntimeException { private final Map<String, String> fieldErrors; public RequestDeserializationException(Map<String, String> fieldErrors) { this.fieldErrors = fieldErrors; } public Map<String, String> getFieldErrors() { return fieldErrors; } }
3. 自定义Jackson消息转换器
替换Spring默认的MappingJackson2HttpMessageConverter,在反序列化时注入错误收集器,完成后如果有错误就抛出自定义异常:
public class CustomJacksonMessageConverter extends MappingJackson2HttpMessageConverter { @Override public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { // 拷贝全局ObjectMapper避免修改全局配置 ObjectMapper objectMapper = getObjectMapper().copy(); CollectAllDeserializationErrorsHandler errorHandler = new CollectAllDeserializationErrorsHandler(); objectMapper.addHandler(errorHandler); // 关闭反序列化快速失败配置 objectMapper.disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE); objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); Object result = objectMapper.readValue(inputMessage.getBody(), objectMapper.constructType(type)); // 反序列化完成后检查是否有收集到的错误 if (errorHandler.hasErrors()) { throw new RequestDeserializationException(errorHandler.getFieldErrors()); } return result; } }
4. 注册自定义消息转换器到Spring MVC
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { // 将自定义转换器放在最前面,优先生效 converters.add(0, new CustomJacksonMessageConverter()); } }
5. 新增自定义异常处理器
@ExceptionHandler(RequestDeserializationException.class) public ResponseEntity<Map<String, Object>> handleDeserializationErrors(RequestDeserializationException e) { Map<String, Object> resp = new HashMap<>(); resp.put("code", 400); resp.put("message", "请求参数格式错误"); resp.put("errors", e.getFieldErrors()); return ResponseEntity.badRequest().body(resp); }
效果验证
当传入的请求体同时包含非法枚举值和错误格式日期时,响应示例如下:
{ "code": 400, "message": "请求参数格式错误", "errors": { "speed": "非法枚举值[XXX],允许值:FAST,SLOW,NORMAL", "startDate": "日期格式错误[2020/01/01],请使用yyyy-MM-dd格式" } }
内容的提问来源于stack exchange,提问作者techDoubts23
相关产品推荐
相关产品推荐

