java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot异常响应被序列化为XML

SpringBoot异常响应被序列化为XML的排查与修复指南

作者:流烟默

本文详细记录了一次SpringBoot微服务中正常接口返回JSON、异常响应却被序列化为XML的排查过程,根因在于异常处理器持有独立的转换器列表,未经过WebMvcConfigurer的扩展,文章提供了基于BeanPostProcessor的稳定修复方案,并总结了排查误区和通用性教训

记录一次耗时较长的排查:正常接口返回 JSON,异常响应却被序列化为 XML。最终定位到是 ExceptionHandlerExceptionResolver 持有独立的转换器列表,并给出稳定修复方案。

一、问题现象

在某 Spring Boot 3.x / Spring Framework 6.x 微服务中,出现了诡异的响应格式不一致问题:

场景响应格式
正常业务请求(如 POST /api/user/update)JSON {"code":0,...}
异常响应(如 POST /api/cache/refresh 触发 405)XML <CommonResult><code>300</code>...

二、根因分析

1. XML 转换器的来源

项目中某个中间件(如 ShardingSphere-JDBC)间接引入了 jackson-dataformat-xml,Spring Boot 检测到该依赖后,自动注册了 MappingJackson2XmlHttpMessageConverter。

在 Spring Framework 6.x 的 WebMvcConfigurationSupport#addDefaultHttpMessageConverters 中,XML 转换器的注册顺序早于 JSON:

[..., StringHttpMessageConverter, ResourceHttpMessageConverter,
 MappingJackson2XmlHttpMessageConverter,   ← XML
 MappingJackson2HttpMessageConverter,      ← JSON
 ...]

当请求头为 Accept: */* 或 application/xml 时,内容协商机制优先选中 XML → 统一响应体 CommonResult 被序列化成 <CommonResult>...</CommonResult>。

2. 为什么"正常 JSON、异常 XML"

这是本次排查最耗时的点。关键发现是:正常路径与异常路径持有两份独立的转换器列表。

路径转换器列表宿主是否经过 WebMvcConfigurer#extendMessageConverters
正常响应RequestMappingHandlerAdapter✅ 会
异常响应ExceptionHandlerExceptionResolver❌ 不会

WebMvcConfigurer#extendMessageConverters 只影响 RequestMappingHandlerAdapter 那一份;而 ExceptionHandlerExceptionResolver 由 Boot 单独构建(默认包装在 HandlerExceptionResolverComposite 里),持有自己的一份转换器列表,不经过 extendMessageConverters 回调。

3. 为什么@ExceptionHandler方法拿不到清理结果

排查中还验证了两件事:

三、最终修复方案

核心配置类

@Slf4j
@Configuration
public class WebMvcConverterConfig implements WebMvcConfigurer {

    // 1) 清理正常路径(RequestMappingHandlerAdapter)的 XML 转换器
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        int removed = removeXmlConverters(converters);
        log.info("extendMessageConverters 移除 XML 转换器: {} 个", removed);
    }

    // 2) 清理异常路径(ExceptionHandlerExceptionResolver)的 XML 转换器
    @Bean
    public static BeanPostProcessor exceptionResolverXmlConverterCleaner() {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) {
                // resolver 可能是独立 bean,也可能被包在 Composite 里(Boot 3.x 默认),两种都处理
                if (bean instanceof ExceptionHandlerExceptionResolver resolver) {
                    clean(resolver);
                } else if (bean instanceof HandlerExceptionResolverComposite composite) {
                    for (HandlerExceptionResolver r : composite.getExceptionResolvers()) {
                        if (r instanceof ExceptionHandlerExceptionResolver ehr) {
                            clean(ehr);
                        }
                    }
                }
                return bean;
            }

            private void clean(ExceptionHandlerExceptionResolver resolver) {
                // removeIf 就地修改同一 List 实例,resolver 已构建的返回值处理器同样生效
                int removed = removeXmlConverters(resolver.getMessageConverters());
                log.info("ExceptionHandlerExceptionResolver[{}] 移除 XML 转换器: {} 个",
                        resolver.hashCode(), removed);
            }
        };
    }

    private static int removeXmlConverters(List<HttpMessageConverter<?>> converters) {
        int before = converters.size();
        converters.removeIf(c -> c instanceof MappingJackson2XmlHttpMessageConverter);
        return before - converters.size();
    }
}

关键技术点

要点说明
static @BeanBPP 需早于宿主配置类实例化,static 让它在普通 Bean 之前完成注册
HandlerExceptionResolverComposite 兼容Boot 3.x 里异常解析器被包在 Composite 中,必须遍历其 getExceptionResolvers()
removeIf 就地修改BPP 在 Resolver 初始化之后执行,但其返回值处理器持有的是同一 List 引用,removeIf 同步生效
getMessageConverters()ExceptionHandlerExceptionResolver 有 public getter,无需反射

为什么不直接排除依赖?

jackson-dataformat-xml 被中间件间接引入,强行排除有 NoClassDefFoundError 风险(中间件运行时可能读取 XML/YAML 配置)。BPP 方案零依赖风险,更为稳妥。

四、验证

启动日志确认

extendMessageConverters 移除 XML 转换器: 2 个
ExceptionHandlerExceptionResolver[xxxxx] 移除 XML 转换器: 2 个   ← 关键:证明异常路径也被清理

请求验证

# 正常请求 → JSON
curl -X POST 'http://localhost:8080/api/user/update' \
  -H 'Accept: */*' -H 'Content-Type: application/json' \
  -d '{"id":"1","name":"test"}'
# 异常请求(POST 打 GET-only 接口)→ JSON
curl -X POST 'http://localhost:8080/api/cache/refresh' \
  -H 'Accept: */*'

两条请求均返回 JSON,问题闭环。

五、排查历程中的关键误区

阶段错误判断修正
1认为是 @ExceptionHandler 未声明 produces全局 20+ Handler 逐个加不现实,且不符合根因
2认为是包路径未被扫描通过容器内 ls + 启动日志确认类已被加载
3认为是中间件依赖没排干净排干净了但 BPP 逻辑不对,问题依旧
4BPP 用 instanceof ExceptionHandlerExceptionResolver 匹配实际 Bean 是 HandlerExceptionResolverComposite,从未命中
5想在 @RestControllerAdvice 类上加 produces@ExceptionHandler 不支持 produces 属性

排查启示:

六、通用性总结

  1. XML 序列化能力:只要 Classpath 上有 jackson-dataformat-xml(或 JAXB 实现),Spring Boot 就会注册 XML 转换器。若接口契约只允许 JSON,最彻底的做法是排除依赖;若不能排除,需覆盖所有转换器列表的持有者(RequestMappingHandlerAdapter + ExceptionHandlerExceptionResolver)。
  2. extendMessageConverters 作用域有限:仅覆盖 RequestMappingHandlerAdapter,不覆盖异常解析器。
  3. 异常解析器的注册结构:Boot 3.x 默认把 ExceptionHandlerExceptionResolver 放进 HandlerExceptionResolverComposite 中,需要逐层访问。
  4. 就地修改 List 引用:BPP 在组件初始化后清理其持有的 List,因 List 引用为共享对象,removeIf 即可同步生效,无需重新注入。

以上就是SpringBoot异常响应被序列化为XML的排查与修复指南的详细内容,更多关于SpringBoot异常响应被序列化为XML的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文