一、问题现象
在本地开发环境(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.xml 的 spring-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-interflow,common-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-plugin 的 includeSystemScope 参数。
步骤 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 私服 -
改用普通依赖 :安装到仓库后,改为普通
compilescope 依赖,可以正常传递
安装到本地仓库的命令:
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 顶层 |
核心知识点:
-
Maven
systemscope 依赖不传递 -
Spring Boot fat jar 的类加载机制不支持 jar-in-jar
-
<includeSystemScope>需要配合显式声明使用 -
最佳实践是避免
systemscope,使用私库管理第三方 jar