最近在处理公司的档案系统时遇到了一个诡异的问题:异步归档功能突然出现了莫名其妙的失败,代码执行到一半就直接跳到 finally 块,而且日志里完全看不到任何异常信息。经过一番排查,发现是 JAXB 在 SPI 环境下的版本冲突问题。
这篇文章记录一下整个排查过程和最终的解决方案,希望能帮到遇到类似问题的同学。
问题现象
首先说说遇到的奇怪现象:
- 代码莫名中断:异步归档的业务代码执行到一半就直接跳到 finally 块,就像是被什么东西强制中断了一样
- 日志完全没有异常:最开始完全看不到任何错误日志,这让排查变得异常困难
- 最终定位到的真正错误:
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 包供宿主系统加载。问题就出在这里:
- 多套 JAXB 实现共存 :JDK 8 自带了
javax.xml.bind
包,而很多应用又会引入com.sun.xml.bind
的不同版本 - 类加载的不确定性:在 SPI 环境下,运行时到底加载哪个版本的 JAXB 实现,我们无法完全控制
- 版本不匹配导致的字段缺失 :不同版本的 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;
}
问题解决后的效果
经过上面的改造,所有问题都得到了解决:
- 异常不再"失踪" :外层改用
catch (Throwable t)
后,所有 Error 级别的异常都能被正常捕获和记录 - JAXB 版本冲突彻底解决:换用 Jackson XML 后,不再依赖 JDK 自带或宿主环境的 JAXB 实现
- 依赖可控 :Maven Shade 打包后,所有必要的 jar 都在自己的包里,不会再出现
NoClassDefFoundError
- 归档功能恢复正常:异步线程稳定运行,不再有莫名其妙的中断
总结
这次问题的排查过程让我深刻体会到了 SPI 环境下依赖管理的复杂性。总结几点经验:
关于异常处理
- 在关键的外层位置,适当使用
catch (Throwable)
来捕获 Error 级别的异常 - 但不要滥用,中间层还是应该按具体异常类型进行处理
- 异常日志要尽可能详细,方便后续排查
关于 SPI 项目的依赖管理
- SPI 项目的运行环境不可控,尽量把关键依赖打包进自己的 jar
- 避免使用 JDK 自带的可能有版本问题的组件(如 JAXB)
- Maven Shade 插件是个不错的工具,但要注意签名文件的处理
关于技术选型
- 选择第三方组件时,要考虑其在不同环境下的兼容性
- Jackson 的生态相对更独立,版本冲突问题相对较少
- 对于 XML 处理,如果没有特殊需求,直接用 Jackson 是个不错的选择
希望这次的踩坑经历能对遇到类似问题的朋友有所帮助。在 SPI 开发中,依赖管理确实是一个需要特别关注的点,多做一些预防性的工作,能避免很多后续的麻烦。