
本文深入剖析普通jar与fat jar在编译期与运行时的依赖解析差异,明确回答“是否需手动引入传递依赖”“类路径如何构成”等关键问题,并提供可落地的maven配置方案与实践警示。
本文深入剖析普通jar与fat jar在编译期与运行时的依赖解析差异,明确回答“是否需手动引入传递依赖”“类路径如何构成”等关键问题,并提供可落地的maven配置方案与实践警示。
在Java项目构建与部署中,“本地能跑、线上报错”是高频痛点,其根源往往直指一个被低估的概念:JAR包的依赖可见性边界。理解普通JAR(Normal JAR)与Fat JAR(也称Uber-JAR、Shaded JAR)的本质差异,是破局的第一步。
一、普通JAR:依赖不随包走,需显式声明
普通JAR仅包含项目自身编译后的字节码(.class)和资源文件,完全不嵌入任何第三方依赖。以库 A(依赖 X, Y, Z)为例:
- ✅ 编译期(Compile Classpath):当服务
B声明<dependency><groupid>com.example</groupid><artifactid>A</artifactid></dependency>时,Maven仅将A.jar加入编译类路径;X/Y/Z的API不可见,B中若直接调用X的类(如com.google.common.collect.Lists),编译即失败。 - ✅ 运行期(Runtime Classpath):Maven默认不会自动拉取
A的传递依赖。即使A的pom.xml声明了X/Y/Z,B的最终运行类路径中只有A.jar——X/Y/Z缺失,启动时必抛NoClassDefFoundError或ClassNotFoundException。
? 关键结论:普通JAR作为依赖引入时,必须由使用者(
B)显式声明所有所需依赖。Maven的传递依赖机制仅在B的pom.xml中完整声明A及其依赖链时才生效(例如通过<scope>compile</scope>的标准依赖),而非靠A.jar自身携带。
二、Fat JAR:依赖内聚,但引入新约束
Fat JAR通过插件(如 maven-shade-plugin 或 spring-boot-maven-plugin)将 A 及其全部依赖(X/Y/Z)的 .class 文件解压并扁平化合并至同一JAR中。
- ✅ 编译期:
B引入A-fat.jar后,其编译类路径理论上可见A、X、Y、Z的所有公开类(因字节码已存在于该JAR内)。但强烈不建议B直接调用X/Y/Z的内部API——这破坏了模块边界,导致B与A的实现细节强耦合。 - ✅ 运行期:
A-fat.jar是自包含的,B的运行类路径只需包含此单个JAR即可启动A的功能,无需额外添加X/Y/Z。
⚠️ 重要警示:
-
依赖冲突风险:若
B自身也直接依赖X(版本为2.0),而A-fat.jar内嵌的是X-1.5,则运行时可能出现NoSuchMethodError(因B调用X-2.0新增方法,但加载的是X-1.5的类)。 -
无法热更新:
X/Y/Z的修复需重新构建整个Fat JAR,无法单独替换依赖JAR。 - 体积膨胀:一个Spring Boot Fat JAR常达80MB+,显著拖慢CI/CD与容器镜像分发。
三、实战配置示例(Maven)
方案1:生成Fat JAR(推荐用于独立服务)
<build><plugins><!-- Spring Boot项目(最简) --><plugin><groupid>org.springframework.boot</groupid><artifactid>spring-boot-maven-plugin</artifactid><version>3.3.0</version><executions><execution><goals><goal>repackage</goal><!-- 关键:生成可执行Fat JAR --></goals></execution></executions></plugin><!-- 普通Java项目(使用shade插件) --><plugin><groupid>org.apache.maven.plugins</groupid><artifactid>maven-shade-plugin</artifactid><version>3.5.0</version><executions><execution><phase>package</phase><goals><goal>shade</goal></goals><configuration><transformers><transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"><mainclass>com.example.AMain</mainclass><!-- 指定入口类 --></transformer></transformers><filters><filter><artifact>*:*</artifact><excludes><exclude>META-INF/*.SF</exclude><exclude>META-INF/*.DSA</exclude><exclude>META-INF/*.RSA</exclude></excludes></filter></filters></configuration></execution></executions></plugin></plugins></build>
方案2:分离依赖(推荐用于库项目)
<build><plugins><!-- 生成普通JAR(默认行为) --><plugin><groupid>org.apache.maven.plugins</groupid><artifactid>maven-jar-plugin</artifactid><version>3.4.0</version><configuration><archive><manifest><addclasspath>true</addclasspath><classpathprefix>lib/</classpathprefix><mainclass>com.example.AMain</mainclass></manifest></archive></configuration></plugin><!-- 将依赖复制到lib目录 --><plugin><groupid>org.apache.maven.plugins</groupid><artifactid>maven-dependency-plugin</artifactid><version>3.6.0</version><executions><execution><id>copy-dependencies</id><phase>prepare-package</phase><goals><goal>copy-dependencies</goal></goals><configuration><outputdirectory>${project.build.directory}/lib</outputdirectory><overwritereleases>false</overwritereleases><overwritesnapshots>false</overwritesnapshots><overwriteifnewer>true</overwriteifnewer></configuration></execution></executions></plugin></plugins></build>
执行 mvn package 后生成:
-
A-1.0.jar(含MANIFEST中Class-Path: lib/X.jar lib/Y.jar ...) -
target/lib/目录下存放所有依赖JAR
→ 部署时需同时上传JAR包与lib目录,但支持按需更新单个依赖。
四、终极选择指南
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 对外发布SDK/工具库 | 普通JAR + 完整POM | 保证使用者可控依赖版本,避免冲突 |
| 独立可执行服务(如Spring Boot应用) | Fat JAR | “开箱即用”,简化运维,适合容器化部署 |
| 数据调度平台任务(如DolphinScheduler) | Fat JAR | 平台通常只接受单文件上传,且隔离性强 |
| 大型微服务集群 | 分离依赖(JAR + lib) | 平衡部署效率与依赖治理,支持灰度升级 |
? 黄金法则:Fat JAR解决的是“分发便利性”问题,普通JAR解决的是“依赖可控性”问题。没有银弹,只有权衡。 在CI/CD流水线中,建议对同一代码基线生成两种产物(
-jar和-fat.jar),按场景选用。
最后提醒:无论采用哪种方式,务必通过 mvn dependency:tree -Dverbose 定期审查依赖树,用 jdeps --list-deps your-app.jar 验证Fat JAR的类引用完整性——真正的工程稳健性,始于对依赖关系的透明掌控。











