JAXB 版本冲突踩坑记:SPI 项目中的 XML 处理方案升级

最近在处理公司的档案系统时遇到了一个诡异的问题:异步归档功能突然出现了莫名其妙的失败,代码执行到一半就直接跳到 finally 块,而且日志里完全看不到任何异常信息。经过一番排查,发现是 JAXB 在 SPI 环境下的版本冲突问题。

这篇文章记录一下整个排查过程和最终的解决方案,希望能帮到遇到类似问题的同学。

问题现象

首先说说遇到的奇怪现象:

  1. 代码莫名中断:异步归档的业务代码执行到一半就直接跳到 finally 块,就像是被什么东西强制中断了一样
  2. 日志完全没有异常:最开始完全看不到任何错误日志,这让排查变得异常困难
  3. 最终定位到的真正错误
csharp 复制代码
java.lang.NoSuchFieldError: REFLECTION
  at com.sun.xml.bind.v2.model.impl.RuntimeModelBuilder.<init>(...)
  ...
  at javax.xml.bind.JAXBContext.newInstance(...)

后来改用 Jackson 时又碰到了:

bash 复制代码
java.lang.NoClassDefFoundError: com/fasterxml/jackson/dataformat/xml/XmlMapper

第一个坑:Error 级别异常被"吞掉"了

为什么日志里看不到异常?原来我们的代码里只用了 try { ... } catch (Exception e) { ... } finally { ... },但是这次抛出的 NoSuchFieldError 属于 Error 级别,不是 Exception 的子类。

这就导致了一个很坑的现象:

  • 异常没有被 catch 住,所以没有日志输出
  • 代码直接跳到 finally 块,表面上看起来像是"正常结束"
  • 实际上异常继续向上抛,直到被最外层的容器捕获

解决方法 :在关键的外层位置改用 catch (Throwable t),这样就能捕获包括 Error 在内的所有异常了。

java 复制代码
try {
    // 业务代码
} catch (Throwable t) {
    log.error("归档处理异常", t);
} finally {
    // 清理代码
}

不过要注意,catch (Throwable) 只建议在最外层使用,中间层还是应该按具体情况来捕获特定的异常类型。

第二个坑:JAXB 版本冲突的深层原因

NoSuchFieldError: REFLECTION 这个错误看起来很玄学,但实际上是典型的类加载冲突问题。

我们的项目是以 SPI 插件的形式运行的,会被打成独立的 jar 包供宿主系统加载。问题就出在这里:

  1. 多套 JAXB 实现共存 :JDK 8 自带了 javax.xml.bind 包,而很多应用又会引入 com.sun.xml.bind 的不同版本
  2. 类加载的不确定性:在 SPI 环境下,运行时到底加载哪个版本的 JAXB 实现,我们无法完全控制
  3. 版本不匹配导致的字段缺失 :不同版本的 JAXB 实现中,某些类的字段定义可能不一样,运行时访问不存在的字段就会抛出 NoSuchFieldError

这种问题在 SPI 场景下特别常见,因为插件的运行环境完全依赖于宿主系统,依赖冲突几乎不可避免。

解决方案:拥抱 Jackson XML

既然 JAXB 在 SPI 环境下这么不稳定,那就换一个更可控的方案。经过调研,决定改用 Jackson 来处理 XML:

为什么选择 Jackson?

  • 不依赖 JDK 内置的 XML 处理组件
  • 版本管理更清晰,不会和系统自带的包冲突
  • 可以通过 Maven Shade 插件将依赖直接打包到我们的 jar 中
  • 性能和功能都不错,社区活跃度高

当然,切换过程中也遇到了新问题:NoClassDefFoundError: XmlMapper。这个错误更直接,就是运行时找不到 Jackson 的 XML 组件。解决办法也很明确:把相关依赖全部打包进我们的 SPI jar 中。

Maven 配置

首先添加 Jackson XML 相关的依赖:

xml 复制代码
<dependencies>
    <!-- Jackson XML 核心依赖 -->
    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
        <version>2.15.2</version>
    </dependency>
    
    <!-- 如果需要使用 JAXB 注解兼容 -->
    <dependency>
        <groupId>com.fasterxml.jackson.module</groupId>
        <artifactId>jackson-module-jaxb-annotations</artifactId>
        <version>2.15.2</version>
    </dependency>
</dependencies>

接下来是关键配置,使用 Maven Shade 插件将依赖打包进最终的 jar:

xml 复制代码
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.4.1</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <createDependencyReducedPom>true</createDependencyReducedPom>
                        <transformers>
                            <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
                        </transformers>
                        <filters>
                            <filter>
                                <artifact>*:*</artifact>
                                <excludes>
                                    <!-- 排除签名文件,避免冲突 -->
                                    <exclude>META-INF/*.SF</exclude>
                                    <exclude>META-INF/*.DSA</exclude>
                                    <exclude>META-INF/*.RSA</exclude>
                                </excludes>
                            </filter>
                        </filters>
                        <artifactSet>
                            <includes>
                                <!-- 只打包 Jackson 相关的依赖 -->
                                <include>com.fasterxml.jackson.core:*</include>
                                <include>com.fasterxml.jackson.dataformat:jackson-dataformat-xml</include>
                                <include>com.fasterxml.jackson.module:jackson-module-jaxb-annotations</include>
                                <include>com.fasterxml.woodstox:woodstox-core</include>
                                <include>org.codehaus.woodstox:stax2-api</include>
                            </includes>
                        </artifactSet>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

这样配置的好处是,所有 Jackson 相关的依赖都会被打包到最终的 jar 中,不再依赖宿主环境提供。

代码改造

从 JAXB 迁移到 Jackson XML 的代码改动其实不大,主要是注解的替换:

java 复制代码
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;

@JacksonXmlRootElement(localName = "archive")
public class ArchiveBasic {
    @JacksonXmlProperty(localName = "title")
    private String title;

    @JacksonXmlProperty(localName = "creator")
    private String creator;

    // getter/setter 略...
}

public class XmlGenerateUtil {
    private static final XmlMapper XML_MAPPER = new XmlMapper();
    
    public static String toXml(Object obj) throws Exception {
        return XML_MAPPER.writeValueAsString(obj);
    }
    
    public static <T> T fromXml(String xml, Class<T> clazz) throws Exception {
        return XML_MAPPER.readValue(xml, clazz);
    }
}

对于集合类型的处理,可以使用 @JacksonXmlElementWrapper@JacksonXmlProperty 组合:

java 复制代码
public class ArchivePackage {
    @JacksonXmlElementWrapper(localName = "files")
    @JacksonXmlProperty(localName = "file")
    private List<String> fileList;
    
    // 或者不需要包装元素
    @JacksonXmlElementWrapper(useWrapping = false)
    @JacksonXmlProperty(localName = "item")
    private List<String> items;
}

问题解决后的效果

经过上面的改造,所有问题都得到了解决:

  1. 异常不再"失踪" :外层改用 catch (Throwable t) 后,所有 Error 级别的异常都能被正常捕获和记录
  2. JAXB 版本冲突彻底解决:换用 Jackson XML 后,不再依赖 JDK 自带或宿主环境的 JAXB 实现
  3. 依赖可控 :Maven Shade 打包后,所有必要的 jar 都在自己的包里,不会再出现 NoClassDefFoundError
  4. 归档功能恢复正常:异步线程稳定运行,不再有莫名其妙的中断

总结

这次问题的排查过程让我深刻体会到了 SPI 环境下依赖管理的复杂性。总结几点经验:

关于异常处理

  • 在关键的外层位置,适当使用 catch (Throwable) 来捕获 Error 级别的异常
  • 但不要滥用,中间层还是应该按具体异常类型进行处理
  • 异常日志要尽可能详细,方便后续排查

关于 SPI 项目的依赖管理

  • SPI 项目的运行环境不可控,尽量把关键依赖打包进自己的 jar
  • 避免使用 JDK 自带的可能有版本问题的组件(如 JAXB)
  • Maven Shade 插件是个不错的工具,但要注意签名文件的处理

关于技术选型

  • 选择第三方组件时,要考虑其在不同环境下的兼容性
  • Jackson 的生态相对更独立,版本冲突问题相对较少
  • 对于 XML 处理,如果没有特殊需求,直接用 Jackson 是个不错的选择

希望这次的踩坑经历能对遇到类似问题的朋友有所帮助。在 SPI 开发中,依赖管理确实是一个需要特别关注的点,多做一些预防性的工作,能避免很多后续的麻烦。

复制代码
相关推荐
NightDW2 小时前
amqp-client源码解析1:数据格式
java·后端·rabbitmq
程序员清风3 小时前
美团二面:KAFKA能保证顺序读顺序写吗?
java·后端·面试
ytadpole17 小时前
揭秘xxl-job:从高可用到调度一致性
java·后端
玉衡子18 小时前
六、深入理解JVM执行引擎
java·jvm
每天进步一点_JL18 小时前
JVM 内存调优:到底在调什么?怎么调?
java·jvm·后端
yinke小琪18 小时前
说说Java 中 Object 类的常用的几个方法?详细的讲解一下
java·后端·面试
间彧21 小时前
Spring Boot项目中如何实现Redis分布式锁
java
掘金安东尼21 小时前
AI 应用落地谈起 ,免费试用 Amazon Bedrock 的最佳时机
java·架构
杨杨杨大侠21 小时前
案例03-附件E-部署运维
java·docker·github