Maven System Scope 依赖在 Spring Boot 打包后类找不到的问题排查与解决

一、问题现象

在本地开发环境(IDEA)中,ImagePlatformUtil 类的 addImage 方法可以正常编译和运行,代码如下:

复制代码
BaseBusiObject<BaseDocObject> o = new BaseBusiObject<BaseDocObject>();

但将项目打包部署到测试环境服务器后,启动服务正常,访问接口时报错:

复制代码
java.lang.ClassNotFoundException: cn.jsbchina.ecny.xxx.BaseDocObject

BaseDocObject 类来自本地的第三方 jar 包(ecm-http-service_ocr_jdk17.jar),通过 Maven system scope 方式引入。


二、依赖关系梳理

项目依赖结构如下:

复制代码
ecny-front-wallet(Spring Boot 启动模块)
    └── common-busi
            └── common-interflow
                    └── common-public
                            └── ecm-http-service_ocr_jdk17.jar(system scope)

jar 包的引入方式:

复制代码
<!-- common-public/pom.xml -->
<dependency>
    <groupId>ecm-http-service</groupId>
    <artifactId>ecm-http-service.jar</artifactId>
    <version>1.0</version>
    <scope>system</scope>
    <systemPath>${pom.basedir}/src/main/resources/lib/ecm-http-service_ocr_jdk17.jar</systemPath>
</dependency>

三、尝试过的失败方案

方案 1:手动塞 jar 到 fat jar 中

做法 :从服务器下载 ecny-front-wallet.jar,解压后在 BOOT-INF/lib 目录下放入 ecm-http-service_ocr_jdk17.jar,重新打包上传服务器。

结果 :启动正常,但访问接口依然报 ClassNotFoundException

原因分析 :Spring Boot fat jar 使用特殊的 LaunchedURLClassLoader 加载嵌套 jar,手动用压缩工具塞进去的 jar 可能因为 zip 文件的 central directory 索引、压缩标记等细节差异,导致类加载器无法正确识别。


方案 2:只配置 includeSystemScope 不显式声明依赖

做法 :只在 ecny-front-wallet/pom.xmlspring-boot-maven-plugin 中添加 <includeSystemScope>true</includeSystemScope>

结果 :打出的 jar 中 BOOT-INF/lib 下依然没有 ecm-http-service_ocr_jdk17.jar

原因分析<includeSystemScope> 只作用于当前模块 pom 中显式声明的 system scope 依赖。由于 ecny-front-wallet 的 pom 中没有声明这个依赖,所以配置了也没有效果。


四、技术原因分析

1. system scope 依赖不传递

Maven 的 system scope 有一个重要特性:不参与依赖传递

common-busi 依赖 common-interflowcommon-interflow 又依赖 common-public 时,common-public 中声明的 system scope 依赖不会自动传递common-busi

这意味着:

  • common-public 模块编译时能访问到 BaseDocObject

  • common-busi 模块编译时无法 访问到 BaseDocObject(除非也显式声明)

  • 运行时,JVM 的类加载器也找不到这个类

2. jar-in-jar 无法被类加载器加载

Spring Boot fat jar 的目录结构如下:

复制代码
ecny-front-wallet.jar
├── BOOT-INF/
│   ├── classes/
│   └── lib/
│       ├── common-public-0.0.1-SNAPSHOT.jar
│       │   └── lib/
│       │       └── ecm-http-service_ocr_jdk17.jar   ← 类加载器无法读取
│       └── other-dependencies.jar

common-public.jar 内部的 ecm-http-service_ocr_jdk17.jar 只是作为普通资源文件 被嵌入,JVM 的标准类加载器不会递归扫描 jar 包内部的嵌套 jar。

3. 本地 IDEA 能运行的原因

IDEA 对 system scope 的处理比较特殊,它在运行时会将所有模块的依赖(包括 system scope)以独立 jar 的形式加入 classpath,而不是经过 Maven 的依赖传递机制。所以本地看起来正常,但打包后就出问题了。


五、最终解决方案

核心思路

在最终打包的 Spring Boot 启动模块(ecny-front-wallet)中显式声明 system scope 依赖,并配置 spring-boot-maven-pluginincludeSystemScope 参数。

步骤 1:复制 jar 到目标模块

ecm-http-service_ocr_jdk17.jar 复制到 ecny-front-wallet 模块:

复制代码
e-cny-modules/ecny-front-wallet/src/main/resources/lib/ecm-http-service_ocr_jdk17.jar

步骤 2:显式声明 system scope 依赖

修改 ecny-front-wallet/pom.xml,在 dependencies> 中添加:

复制代码
<dependency>
    <groupId>ecm-http-service</groupId>
    <artifactId>ecm-http-service.jar</artifactId>
    <version>1.0</version>
    <scope>system</scope>
    <systemPath>${pom.basedir}/src/main/resources/lib/ecm-http-service_ocr_jdk17.jar</systemPath>
</dependency>

步骤 3:配置 spring-boot-maven-plugin

修改 ecny-front-wallet/pom.xml 中的 spring-boot-maven-plugin

复制代码
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <version>${spring-boot.version}</version>
    <configuration>
        <includeSystemScope>true</includeSystemScope>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>repackage</goal>
            </goals>
        </execution>
    </executions>
</plugin>

步骤 4:重新打包部署

复制代码
mvn clean package -pl e-cny-modules/ecny-front-wallet -am

部署后,BOOT-INF/lib 下将直接包含 ecm-http-service_ocr_jdk17.jar,Spring Boot 的类加载器能够正确加载。


六、最佳实践建议

1. 尽量避免使用 system scope

system scope 是 Maven 中不推荐使用的依赖方式(已被标记为 deprecated)。如果有条件,建议:

  • 上传到公司私库 :使用 mvn install:install-file 安装到本地仓库,或上传到 Nexus 私服

  • 改用普通依赖 :安装到仓库后,改为普通 compile scope 依赖,可以正常传递

安装到本地仓库的命令:

复制代码
mvn install:install-file \
    -Dfile=ecm-http-service_ocr_jdk17.jar \
    -DgroupId=ecm-http-service \
    -DartifactId=ecm-http-service \
    -Dversion=1.0 \
    -Dpackaging=jar

安装后改为普通依赖:

复制代码
<dependency>
    <groupId>ecm-http-service</groupId>
    <artifactId>ecm-http-service</artifactId>
    <version>1.0</version>
</dependency>

2. system scope 的使用场景

如果必须使用 system scope(无法推私库),请确保:

  • 最终打包的模块中显式声明依赖

  • 配置 spring-boot-maven-plugin<includeSystemScope>true</includeSystemScope>

  • jar 文件放在模块的 src/main/resources/lib 目录下

3. 多模块项目中的依赖管理

对于多模块项目,第三方 jar 的依赖应该:

  • 集中管理 :放在一个公共模块(如 common-public

  • 显式声明:在使用到的每个模块中显式声明

  • 路径一致 :确保 systemPath 指向的路径在打包环境中存在


七、总结

问题 原因 解决方案
本地正常,服务器报错 system scope 不传递 在最终模块显式声明依赖
手动塞 jar 到 fat jar 无效 Spring Boot 类加载器不加载嵌套 jar Maven 打包时正确引入
只配置 includeSystemScope 无效 当前模块未声明依赖 同时显式声明 system 依赖
jar-in-jar 无法加载 JVM 类加载器不递归扫描 确保 jar 在 BOOT-INF/lib 顶层

核心知识点

  1. Maven system scope 依赖不传递

  2. Spring Boot fat jar 的类加载机制不支持 jar-in-jar

  3. <includeSystemScope> 需要配合显式声明使用

  4. 最佳实践是避免 system scope,使用私库管理第三方 jar