最近在处理公司的档案系统时遇到了一个诡异的问题:异步归档功能突然出现了莫名其妙的失败,代码执行到一半就直接跳到 finally 块,而且日志里完全看不到任何异常信息。经过一番排查,发现是 JAXB 在 SPI 环境下的版本冲突问题。
这篇文章记录一下整个排查过程和最终的解决方案,希望能帮到遇到类似问题的同学。
问题现象
首先说说遇到的奇怪现象:
- 代码莫名中断:异步归档的业务代码执行到一半就直接跳到 finally 块,就像是被什么东西强制中断了一样
- 日志完全没有异常:最开始完全看不到任何错误日志,这让排查变得异常困难
- 最终定位到的真正错误:
java.lang.NoSuchFieldError: REFLECTION
at com.sun.xml.bind.v2.model.impl.RuntimeModelBuilder.<init>(...)
...
at javax.xml.bind.JAXBContext.newInstance(...)
后来改用 Jackson 时又碰到了:
java.lang.NoClassDefFoundError: com/fasterxml/jackson/dataformat/xml/XmlMapper
第一个坑:Error 级别异常被"吞掉"了
为什么日志里看不到异常?原来我们的代码里只用了 try { ... } catch (Exception e) { ... } finally { ... },但是这次抛出的 NoSuchFieldError 属于 Error 级别,不是 Exception 的子类。
这就导致了一个很坑的现象:
- 异常没有被 catch 住,所以没有日志输出
- 代码直接跳到 finally 块,表面上看起来像是"正常结束"
- 实际上异常继续向上抛,直到被最外层的容器捕获
解决方法:在关键的外层位置改用 catch (Throwable t),这样就能捕获包括 Error 在内的所有异常了。
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 相关的依赖:
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.dataformatgroupId>
<artifactId>jackson-dataformat-xmlartifactId>
<version>2.15.2version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.modulegroupId>
<artifactId>jackson-module-jaxb-annotationsartifactId>
<version>2.15.2version>
dependency>
dependencies>
接下来是关键配置,使用 Maven Shade 插件将依赖打包进最终的 jar:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-shade-pluginartifactId>
<version>3.4.1version>
<executions>
<execution>
<phase>packagephase>
<goals>
<goal>shadegoal>
goals>
<configuration>
<createDependencyReducedPom>truecreateDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
transformers>
<filters>
<filter>
<artifact>*:*artifact>
<excludes>
<exclude>META-INF/*.SFexclude>
<exclude>META-INF/*.DSAexclude>
<exclude>META-INF/*.RSAexclude>
excludes>
filter>
filters>
<artifactSet>
<includes>
<include>com.fasterxml.jackson.core:*include>
<include>com.fasterxml.jackson.dataformat:jackson-dataformat-xmlinclude>
<include>com.fasterxml.jackson.module:jackson-module-jaxb-annotationsinclude>
<include>com.fasterxml.woodstox:woodstox-coreinclude>
<include>org.codehaus.woodstox:stax2-apiinclude>
includes>
artifactSet>
configuration>
execution>
executions>
plugin>
plugins>
build>
这样配置的好处是,所有 Jackson 相关的依赖都会被打包到最终的 jar 中,不再依赖宿主环境提供。
代码改造
从 JAXB 迁移到 Jackson XML 的代码改动其实不大,主要是注解的替换:
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 fromXml(String xml, Class clazz) throws Exception {
return XML_MAPPER.readValue(xml, clazz);
}
}
对于集合类型的处理,可以使用 @JacksonXmlElementWrapper 和 @JacksonXmlProperty 组合:
public class ArchivePackage {
@JacksonXmlElementWrapper(localName = "files")
@JacksonXmlProperty(localName = "file")
private List fileList;
// 或者不需要包装元素
@JacksonXmlElementWrapper(useWrapping = false)
@JacksonXmlProperty(localName = "item")
private List 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 开发中,依赖管理确实是一个需要特别关注的点,多做一些预防性的工作,能避免很多后续的麻烦。