Java 8 fat-JAR 打包后无法读取 JPEG2000 图像
结论
问题通常出在 fat-JAR 的合并过程:ImageIO 的 SPI 注册文件被覆盖或丢失了。
META-INF/services/javax.imageio.spi.ImageReaderSpi
jai-imageio-jpeg2000 通过 Java SPI 机制注册 JPEG2000 reader。在 IntelliJ 中运行时,各个依赖 JAR 独立存在于 classpath,JVM 可以读取每个 JAR 里的 SPI 文件。maven-assembly-plugin 把依赖展开并合并到同一个 JAR 后,同名资源可能互相覆盖。此时,即使 reader 的 .class 文件仍在,ImageIO 也发现不了它,PDFBox 随后会抛出 MissingImageReaderException。
.m2 下的目录是按 Maven 坐标组织的仓库存储结构,不会原样写入 fat-JAR。它和 fat-JAR 中的路径不同是正常的。
使用 Maven Shade Plugin 合并 SPI 文件
可以用 maven-shade-plugin 替换当前的 maven-assembly-plugin,并配置 ServicesResourceTransformer。该 transformer 会合并各依赖中的 META-INF/services/* 文件,避免同名文件只留下一个。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer implementation=
"org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation=
"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>CoverSheetCreatorFX</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
如果主类位于某个 package 中,mainClass 需要填写完整类名,例如:
<mainClass>com.example.CoverSheetCreatorFX</mainClass>
重新构建并运行:
mvn clean package
java -jar target/your-application.jar
项目中应固定插件版本。上面的版本可以作为 Java 8 项目的配置示例,实际使用前还要结合 Maven 版本和构建 JDK 完整测试一次。
继续使用 Maven Assembly Plugin
如果暂时不更换插件,可以在现有 assembly 配置中加入 metaInf-services container descriptor handler:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>CoverSheetCreatorFX</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<containerDescriptorHandlers>
<containerDescriptorHandler>
<handlerName>metaInf-services</handlerName>
</containerDescriptorHandler>
</containerDescriptorHandlers>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
这项配置会让 assembly plugin 合并 META-INF/services 的内容。它是否可用还与项目采用的插件版本有关,因此构建完成后要检查生成物,不能只看构建是否成功。
检查 SPI 文件是否被覆盖
先查看依赖 JAR 中的服务注册文件:
unzip -p "$HOME/.m2/repository/com/github/jai-imageio/jai-imageio-jpeg2000/1.3.0/jai-imageio-jpeg2000-1.3.0.jar" \
META-INF/services/javax.imageio.spi.ImageReaderSpi
再查看 fat-JAR 中的同一个文件:
unzip -p target/your-application.jar \
META-INF/services/javax.imageio.spi.ImageReaderSpi
也可以先检查该文件是否存在:
jar tf target/your-application.jar |
grep 'META-INF/services/javax.imageio.spi.ImageReaderSpi'
如果 fat-JAR 中没有这个文件,或者文件内容只包含其他 ImageIO 依赖的注册信息,没有 JPEG2000 provider,就可以确定问题出在 SPI 文件的打包过程。
还要检查 JPEG2000 reader 的 class 是否已经打进 JAR:
jar tf target/your-application.jar |
grep -i 'jpeg2000\|j2k\|ImageReaderSpi'
找到 class 只能证明代码已经打包,无法证明 SPI 注册完整。ImageIO 能否发现 provider,还取决于注册文件和运行时的服务扫描。
在运行时检查 JPEG2000 reader
可以在程序启动阶段临时加入以下诊断代码:
import javax.imageio.ImageIO;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
import java.util.Iterator;
public class ImageIODiagnostics {
public static void printReaders() {
ImageIO.scanForPlugins();
IIORegistry registry = IIORegistry.getDefaultInstance();
Iterator<ImageReaderSpi> providers =
registry.getServiceProviders(ImageReaderSpi.class, true);
while (providers.hasNext()) {
ImageReaderSpi provider = providers.next();
System.out.println(
provider.getClass().getName()
+ " -> "
+ String.join(", ", provider.getFormatNames())
);
}
}
}
分别在 IntelliJ 和 fat-JAR 环境中运行这段代码,然后对比输出的 provider。
ImageIO.scanForPlugins() 会重新扫描 classpath 上的 SPI 服务,却无法恢复打包时已经丢失的 META-INF/services 文件。它适合排查初始化或 classloader 问题,无法取代正确的打包配置。
IntelliJ 正常而 fat-JAR 失败的原因
IntelliJ 通常通过类似下面的 classpath 运行程序:
target/classes
jai-imageio-core-1.4.0.jar
jai-imageio-jpeg2000-1.3.0.jar
jbig2-imageio-3.0.0.jar
pdfbox-....jar
...
这些依赖仍是独立的 JAR,各自都能保留自己的注册文件:
META-INF/services/javax.imageio.spi.ImageReaderSpi
传统 fat-JAR 会先解压这些 JAR,再把文件写入同一个归档。多个依赖中存在相同路径时,如果构建插件没有合并资源,后写入的文件就会覆盖先写入的文件。最终,class 仍然存在,服务注册信息却不完整。
.m2 中可能有这样的仓库目录:
com/github/jai-imageio/jai-imageio-jpeg2000/1.3.0/
它表示:
groupId / artifactId / version
这不是 Java package。依赖展开后,class 会按照自身声明的 package 存放,不需要保留 Maven 仓库的外层目录。
其他排查项
合并 SPI 文件后如果问题仍未解决,可以继续检查以下内容。
1. 确认实际依赖版本
检查是否有版本冲突、依赖排除,或者多个 PDFBox 版本同时进入 classpath:
mvn dependency:tree
也可以缩小输出范围:
mvn dependency:tree \
-Dincludes=com.github.jai-imageio,org.apache.pdfbox
重点检查:
jai-imageio-jpeg2000是否确实进入 runtime classpath;- 是否被某个
<exclusion>排除; - 是否存在多个 PDFBox 或 jai-imageio 版本;
- 打包插件处理的是否为 Maven 实际解析出的依赖。
2. 检查是否启用了类裁剪
使用 Shade Plugin 的 <minimizeJar>true</minimizeJar> 或其他压缩、混淆工具时,SPI 加载的类可能被误判为“未使用”并删除。JPEG2000 reader 通过配置文件动态加载,裁剪工具不一定能从静态调用关系中识别它。
排查期间不要启用 minimizeJar。如果必须缩减体积,需要为 SPI provider 添加保留规则。
3. 核对运行 fat-JAR 的 Java 环境
IntelliJ 使用的 JDK 可能与命令行 java 不同,可以分别检查:
java -version
mvn -version
还要核对 IntelliJ 的 Project SDK、Maven Runner JDK,以及最终用户运行 JAR 时使用的 Java 版本。不同 Java 8 发行版对 JavaFX 的打包方式可能不同,但这通常不会直接造成 JPEG2000 reader 消失。
4. 检查 classloader 和初始化时机
如果应用使用了自定义 classloader、插件框架或特殊的 JavaFX 启动方式,ImageIO 首次初始化时可能拿到了不包含 provider 的线程上下文 classloader。可以在应用启动早期执行:
Thread.currentThread().setContextClassLoader(
ImageIODiagnostics.class.getClassLoader()
);
ImageIO.scanForPlugins();
这段代码会改变线程上下文 classloader,不适合作为未经验证的通用修复。只有确认 SPI 文件已经正确合并、provider class 也确实存在,但注册仍然失败时,才需要从这个方向排查。
5. 区分 JPEG2000 与 JBIG2
jbig2-imageio 负责 JBIG2 图像,不处理 JPEG2000。它也可能带有自己的 ImageIO SPI 文件,并覆盖 JPEG2000 provider 的服务文件。项目中有多个 ImageIO 扩展时,fat-JAR 应合并 META-INF/services,不能任意保留其中一个依赖的版本。
处理顺序
先让 assembly 配置支持 metaInf-services 合并,或者改用配置了 ServicesResourceTransformer 的 Shade Plugin。重新打包后,检查 fat-JAR 中的 META-INF/services/javax.imageio.spi.ImageReaderSpi,确认其中包含 JPEG2000 provider。随后运行诊断代码,确认 ImageIO 已经完成注册。
如果服务文件和 provider class 都完整,问题却仍然存在,再检查依赖冲突、JDK 差异、类裁剪和 classloader。调整 fat-JAR 内的 class 路径通常无法解决这个异常。
备注:内容仅供参考。
