直播镜头免安装绿色版
7.24G · 2025-10-27
最近在处理公司的档案系统时遇到了一个诡异的问题:异步归档功能突然出现了莫名其妙的失败,代码执行到一半就直接跳到 finally 块,而且日志里完全看不到任何异常信息。经过一番排查,发现是 JAXB 在 SPI 环境下的版本冲突问题。
这篇文章记录一下整个排查过程和最终的解决方案,希望能帮到遇到类似问题的同学。
首先说说遇到的奇怪现象:
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
为什么日志里看不到异常?原来我们的代码里只用了 try { ... } catch (Exception e) { ... } finally { ... },但是这次抛出的 NoSuchFieldError 属于 Error 级别,不是 Exception 的子类。
这就导致了一个很坑的现象:
解决方法:在关键的外层位置改用 catch (Throwable t),这样就能捕获包括 Error 在内的所有异常了。
try {
// 业务代码
} catch (Throwable t) {
log.error("归档处理异常", t);
} finally {
// 清理代码
}
不过要注意,catch (Throwable) 只建议在最外层使用,中间层还是应该按具体情况来捕获特定的异常类型。
NoSuchFieldError: REFLECTION 这个错误看起来很玄学,但实际上是典型的类加载冲突问题。
我们的项目是以 SPI 插件的形式运行的,会被打成独立的 jar 包供宿主系统加载。问题就出在这里:
javax.xml.bind 包,而很多应用又会引入 com.sun.xml.bind 的不同版本NoSuchFieldError这种问题在 SPI 场景下特别常见,因为插件的运行环境完全依赖于宿主系统,依赖冲突几乎不可避免。
既然 JAXB 在 SPI 环境下这么不稳定,那就换一个更可控的方案。经过调研,决定改用 Jackson 来处理 XML:
为什么选择 Jackson?
当然,切换过程中也遇到了新问题:NoClassDefFoundError: XmlMapper。这个错误更直接,就是运行时找不到 Jackson 的 XML 组件。解决办法也很明确:把相关依赖全部打包进我们的 SPI jar 中。
首先添加 Jackson 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:
<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 的代码改动其实不大,主要是注解的替换:
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 组合:
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 级别的异常都能被正常捕获和记录NoClassDefFoundError这次问题的排查过程让我深刻体会到了 SPI 环境下依赖管理的复杂性。总结几点经验:
catch (Throwable) 来捕获 Error 级别的异常希望这次的踩坑经历能对遇到类似问题的朋友有所帮助。在 SPI 开发中,依赖管理确实是一个需要特别关注的点,多做一些预防性的工作,能避免很多后续的麻烦。