Enum抛自定义异常未进入@ExceptionHandler方法的解决方案
问题相关代码
枚举类定义,在@JsonCreator标注的静态方法中做参数校验,不通过时抛出自定义CustomException:
public enum Metadata{ AC, CD; @JsonCreator public static Metadata get(String value) throws CustomException { if(!isValid(value)){ throw new CustomException(); } // 省略合法值转换逻辑 return Metadata.valueOf(value); } // 省略isValid校验方法实现 private static boolean isValid(String value) { // 自定义校验逻辑 return true; } }
全局异常处理器中定义的自定义异常处理方法:
@ExceptionHandler(CustomException.class) public ResponseEntity<Response> handleJsonError(CustomException e){ // 省略自定义响应构造逻辑 return response; }
实际现象
枚举校验失败抛出CustomException时,请求不会进入上述CustomException对应的处理方法,反而进入HttpMessageNotReadableException的处理分支。调试查看异常的cause属性,可见异常被逐层包裹:最外层为HttpMessageNotReadableException,中间层是Jackson的ValueInstantiationException,最内层才是业务抛出的CustomException。
问题原因
Spring MVC解析@RequestBody请求参数时,会统一捕获Jackson反序列化阶段抛出的所有异常,包装为HttpMessageNotReadableException后再进入全局异常匹配流程。@JsonCreator抛出的异常属于Jackson反序列化异常,因此会被两层包装,外层异常类型不匹配CustomException的处理器规则,自然无法进入对应处理逻辑。
解决方法
方案1:在HttpMessageNotReadableException处理器中递归解析根异常(推荐)
该方案改动最小,不需要额外修改Jackson配置,只需要在已有的HttpMessageNotReadableException处理逻辑中,递归遍历异常cause链找到最内层根异常,如果是CustomException就直接复用对应的处理逻辑即可:
@ExceptionHandler(HttpMessageNotReadableException.class) public ResponseEntity<Response> handleHttpMessageNotReadable(HttpMessageNotReadableException e) { Throwable rootCause = getRootCause(e); if (rootCause instanceof CustomException) { return handleJsonError((CustomException) rootCause); } // 原有HttpMessageNotReadableException的处理逻辑 return defaultBadRequestResponse; } // 递归获取异常链最内层的根异常 private Throwable getRootCause(Throwable throwable) { Throwable cause = throwable.getCause(); if (cause == null) { return throwable; } return getRootCause(cause); }
方案2:自定义Jackson反序列化处理器,从源头避免异常被包装
如果不想在异常处理器中做异常链遍历,可以自定义Jackson配置,注册反序列化错误处理器,在枚举解析失败时直接抛出CustomException,跳过Jackson和Spring的默认异常包装逻辑:
@Configuration public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder -> builder.postConfigurer(objectMapper -> { objectMapper.addHandler(new DeserializationProblemHandler() { @Override public Object handleWeirdStringValue(DeserializationContext ctxt, Class<?> targetType, String valueToConvert, String failureMsg) throws IOException { if (targetType.equals(Metadata.class)) { throw new CustomException(); } return super.handleWeirdStringValue(ctxt, targetType, valueToConvert, failureMsg); } }); }); } }
使用该方案需要注意,枚举校验逻辑需要和@JsonCreator中的逻辑保持一致,避免出现校验规则不统一的问题。
内容的提问来源于stack exchange,提问作者danceAyush

