
1. 项目概述为什么我们需要一个“全量”的Maven包在Java后端开发中Maven几乎是项目构建和依赖管理的代名词。我们每天都在用maven clean package来生成一个可部署的JAR包。但很多开发者尤其是刚接触项目部署和交付的同学都踩过这样一个坑在本地跑得好好的程序一放到服务器上就报ClassNotFoundException或者NoClassDefFoundError。你检查了target目录下的JAR包发现它确实生成了但大小可能只有几十KB和你本地仓库里动辄几十MB的依赖库形成了鲜明对比。这就是典型的“瘦JAR”问题——Maven默认打包生成的JAR只包含你项目自己编译的类文件不包含任何它依赖的第三方库。那么什么场景下我们需要一个包含所有依赖的“胖JAR”呢想象一下这几个画面你需要把一个数据处理工具交给运维同事让他定期在服务器上跑你开发了一个小型的命令行工具希望用户下载后直接java -jar就能运行而不用关心复杂的CLASSPATH设置或者你在做一个POC演示需要在客户现场一台干净的环境里快速启动你的服务。在这些场景下一个自带所有依赖、开箱即用的JAR包就是最优雅的解决方案。这个需求的核心就是让Maven在打包时不仅包含项目自身的代码还要把所有直接和间接依赖的第三方库比如Spring Boot、Apache Commons、数据库驱动等统统“打包”进去形成一个独立、可执行的单体JAR文件。2. 核心方案选型三大主流插件深度对比要实现“打包所有依赖”这个目标Maven社区提供了多种插件但主流且成熟的方案主要有三个maven-assembly-pluginmaven-shade-plugin和spring-boot-maven-plugin。选择哪一个取决于你的项目类型和最终想要的效果绝不是随便选一个就行。2.1 maven-assembly-plugin灵活但略显原始的“打包工”maven-assembly-plugin可以理解为最基础、最灵活的打包工具。它的核心思想是定义一个“装配描述符”告诉插件如何把各种文件你的代码、依赖JAR、配置文件等组装到一起。对于生成包含依赖的JAR它通常有两种做法一是生成一个“超级JAR”即把所有依赖的.class文件解压出来和你项目的.class文件混合在一起再重新压缩成一个新的JAR二是生成一个包含lib文件夹和启动脚本的压缩包如ZIP或TARlib文件夹里存放所有依赖的JAR。它的优势在于极其灵活你可以定制任何你想要的输出结构。但它的缺点也很明显如果生成超级JAR在处理同名资源文件比如META-INF/MANIFEST.MF或spring.handlers时默认行为是覆盖这很容易导致依赖库的元数据丢失进而引发运行时错误。虽然可以通过配置containerDescriptorHandlers来合并这些文件但配置复杂度陡增。注意对于普通的、没有复杂资源冲突的应用程序或者你明确需要输出一个包含lib目录的发布包时assembly插件是一个选择。但对于现代Java应用尤其是使用了大量框架Spring, MyBatis等的应用它通常不是首选。2.2 maven-shade-plugin功能强大的“重构与合并专家”maven-shade-plugin是Apache Maven官方推荐的、用于创建“可执行超级JAR”的插件。它的名字“shade”阴影很形象它的核心功能不仅仅是打包更重要的是“重命名”即 shading。它可以把依赖的包路径进行重命名从而解决依赖冲突这个令人头疼的问题。例如你的项目同时依赖了com.google.guava:guava:20.0和另一个库而这个库又间接依赖了com.google.guava:guava:15.0。默认情况下Maven会选择一个版本通常是就近原则这可能导致运行时行为不符合预期。shade插件可以让你将某个依赖比如较新的Guava重命名为myapp.shaded.com.google.guava这样两个版本的类就可以在同一个JAR中共存互不干扰。它的核心价值在于解决依赖冲突和创建真正独立的、无冲突的超级JAR。配置上它比assembly更专注于JAR文件处理提供了丰富的Transformer来智能地合并服务描述文件如META-INF/services下的SPI文件这是很多框架如JDBC驱动加载、SLF4J绑定正常运行所必需的。2.3 spring-boot-maven-plugin面向Spring Boot的“一站式解决方案”如果你是Spring Boot项目那么恭喜你你拥有最优雅的解决方案。spring-boot-maven-plugin是Spring Boot生态的原生插件它在底层也使用了类似shade的技术但做了大量面向Spring Boot的优化和封装。它打包生成的通常被称为 “Fat JAR” 或 “Executable JAR”。这个JAR的内部结构非常独特它不是一个简单的、所有.class文件混在一起的压缩包。相反它采用了一种“JAR within JAR”的嵌套结构。当你解压一个Spring Boot Fat JAR时你会发现BOOT-INF/classes/里面是你项目编译后的.class文件。BOOT-INF/lib/里面是所有依赖的第三方JAR文件它们保持原样没有被解压。org/springframework/boot/loader/这里面是Spring Boot自定义的类加载器LaunchedURLClassLoader。这种结构的精妙之处在于它通过自定义类加载器能够直接从JAR文件内部的BOOT-INF/lib/目录加载嵌套的JAR包。这既避免了解压所有依赖可能带来的资源冲突因为每个依赖JAR的META-INF都是独立的又实现了单一可执行文件的目标。同时这个插件默认就处理好了MANIFEST.MF指定了主类和Spring Boot的类加载器真正做到java -jar your-app.jar一键启动。如何选择传统Java SE应用或需要处理严重依赖冲突首选maven-shade-plugin。Spring Boot应用毫无疑问使用spring-boot-maven-plugin这是最佳实践。需要高度定制化输出结构如分发包考虑maven-assembly-plugin。下面这个表格可以帮你快速决策特性maven-assembly-pluginmaven-shade-pluginspring-boot-maven-plugin核心能力灵活组装任意文件结构创建超级JAR解决依赖冲突为Spring Boot应用创建可执行Fat JAR输出物超级JAR或分发包ZIP/TAR超级JAR (Uber JAR)可执行Fat JAR嵌套JAR结构资源文件处理默认覆盖需手动配置合并提供多种Transformer智能合并自动优化处理支持嵌套JAR资源加载依赖冲突解决不支持支持通过重命名包间接支持依赖隔离在嵌套JAR中易用性配置复杂中等简单对Spring Boot项目适用场景定制化分发包通用Java应用需解决依赖冲突Spring Boot应用3. 实战配置详解手把手打造你的Fat JAR理论说再多不如一行配置。我们分别针对shade插件和spring-boot插件给出最常用、最稳定的配置方案。3.1 使用maven-shade-plugin打造通用超级JAR假设你有一个非Spring Boot的普通Java项目主类是com.yourcompany.MainApp。首先在项目的pom.xml文件的buildplugins部分添加如下配置plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.5.1/version !-- 建议使用最新稳定版 -- executions execution phasepackage/phase !-- 绑定到package生命周期阶段 -- goals goalshade/goal !-- 执行shade目标 -- /goals configuration !-- 关键创建可执行JAR的清单文件 -- transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer !-- 指定你的主类即包含main方法的类 -- mainClasscom.yourcompany.MainApp/mainClass /transformer !-- 重要合并服务发现文件如JDBC驱动、日志绑定 -- transformer implementationorg.apache.maven.plugins.shade.resource.ServicesResourceTransformer/ !-- 重要合并Spring的处理器文件如果你的项目用了Spring -- transformer implementationorg.apache.maven.plugins.shade.resource.AppendingTransformer resourceMETA-INF/spring.handlers/resource /transformer transformer implementationorg.apache.maven.plugins.shade.resource.AppendingTransformer resourceMETA-INF/spring.schemas/resource /transformer /transformers !-- 可选过滤掉签名文件避免安全警告 -- filters filter artifact*:*/artifact excludes excludeMETA-INF/*.SF/exclude excludeMETA-INF/*.DSA/exclude excludeMETA-INF/*.RSA/exclude /excludes /filter /filters !-- 最终生成的JAR文件后缀可选默认是原来的名字 -- finalName${project.artifactId}-${project.version}-shaded/finalName /configuration /execution /executions /plugin配置解读与实操心得phasepackage/phase这行配置意味着当你执行mvn clean package时shade插件会自动执行。它会替代默认的maven-jar-plugin直接生成一个已经“shaded”好的超级JAR。你原来那个不包含依赖的“瘦JAR”就不会被生成了。ServicesResourceTransformer这个Transformer至关重要。很多Java库使用META-INF/services/目录下的文件进行SPI服务提供者接口注册比如Java的java.sql.Driver实现MySQL驱动、SLF4J的日志绑定器。如果没有这个Transformer这些服务文件会被覆盖导致驱动加载失败、日志不输出等问题。加上它插件会合并所有依赖中的同名服务文件。Spring相关Transformer如果你的项目使用了Spring框架非Boot那么spring.handlers和spring.schemas这两个文件用于解析Spring XML配置中的自定义命名空间。必须使用AppendingTransformer来合并它们否则Spring可能无法解析你的XML配置。过滤签名文件很多依赖JAR如老版本的Bouncy Castle带有数字签名。当这些JAR被解压并重新打包到一个新的JAR中时签名会失效并在运行时产生一堆警告信息。通过过滤掉.SF,.DSA,.RSA文件可以消除这些烦人的警告。配置完成后运行mvn clean package。在target目录下你会找到类似your-app-1.0-shaded.jar的文件这个文件体积会很大因为它包含了所有依赖。你可以用java -jar target/your-app-1.0-shaded.jar来运行它。3.2 使用spring-boot-maven-pluginSpring Boot项目专属对于Spring Boot项目配置简单到令人发指。通常Spring Boot通过spring-boot-starter-parent父POM或spring-boot-dependenciesBOM已经帮你预置了配置。基础配置在你的pom.xml中确保有如下插件声明。如果你是用spring-boot-starter-parent它已经内置了。build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId !-- version通常由父POM或BOM管理无需显式指定 -- /plugin /plugins /build执行打包直接运行mvn clean package。打包完成后在target目录下你会找到两个JAR文件your-app-1.0.jar这个就是普通的“瘦JAR”只包含你编译的代码。your-app-1.0.jar.original这是瘦JAR的备份。your-app-1.0-exec.jar或直接就是your-app-1.0.jar覆盖了原文件这里有个关键点实操中的关键细节Spring Boot插件默认的打包目标repackage会替换掉默认maven-jar-plugin生成的那个原始JAR。所以你最终在target目录下看到的那个最大的、同名的JAR例如your-app-1.0.jar就是已经包含了所有依赖的Fat JAR。而那个原始的、不包含依赖的JAR会被重命名为.original后缀。你可以直接运行java -jar target/your-app-1.0.jar。高级配置示例有时你需要定制主类虽然Spring Boot通常能自动找到、或者需要包含一些额外的依赖比如本地jar包。plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration !-- 显式指定主类如果自动检测失败的话 -- mainClasscom.yourcompany.YourApplication/mainClass !-- 配置类加载器一般不用动 -- executabletrue/executable !-- 让生成的JAR在Unix/Linux上可直接执行./app.jar -- !-- 包含或排除特定的依赖 -- includes include groupIdcom.special/groupId artifactIdspecial-lib/artifactId /include /includes /configuration executions execution goals goalrepackage/goal !-- 明确指定repackage目标 -- /goals /execution /executions /plugin4. 深度避坑指南与效能优化即使配置正确在实际操作中依然会遇到各种“坑”。下面是我从多次项目交付中总结出的常见问题及解决方案。4.1 依赖冲突与NoSuchMethodError这是使用shade插件时最常遇到的问题。现象是程序在本地开发环境运行正常但打出的Fat JAR在运行时抛出NoSuchMethodError或NoClassDefFoundError。根本原因你的项目依赖了同一个库的两个不同版本。Maven在构建时根据依赖调解规则如最短路径优先选择了一个版本比如较旧的版本打包进了Fat JAR。但你的代码在编译时是针对新版本的API编译的运行时却链接到了旧版本的类导致方法找不到。排查与解决使用mvn dependency:tree命令这是你的第一把利器。在项目根目录下运行此命令它会打印出完整的依赖树。仔细查找冲突的依赖看是哪个传递依赖引入了不兼容的版本。mvn dependency:tree -Dverbose dependency.txt查看输出的dependency.txt文件搜索冲突的groupId和artifactId。在POM中统一管理版本找到冲突后在你的dependencyManagement或直接在最顶层的依赖声明中显式地指定你想要的版本。Maven会优先使用显式声明的版本。dependency groupIdcom.google.guava/groupId artifactIdguava/artifactId version32.1.3-jre/version !-- 显式指定最新稳定版 -- /dependency使用Shade插件的重命名功能终极武器如果冲突无法通过统一版本解决比如两个依赖强制要求不同版本且无法升级可以使用Shade插件的重命名功能将其中一个隔离。configuration relocations relocation patterncom.google.guava/pattern !-- 要重命名的原包名 -- shadedPatternmyapp.shaded.com.google.guava/shadedPattern !-- 重命名后的包名 -- excludes excludecom.google.guava.BETA/exclude !-- 可选排除某些类 -- /excludes /relocation /relocations /configuration注意重命名后你自己的代码中所有引用到这个包的地方也必须使用新的包名。这通常意味着你需要修改源码因此这是最后的手段。4.2 资源文件丢失或冲突症状程序启动后日志配置文件不生效、Spring XML配置文件解析报错、或某些图片/模板文件加载不到。原因与方案对于shade插件确保你按照3.1节的配置包含了ServicesResourceTransformer和针对Spring的AppendingTransformer。这是处理大多数元资源文件冲突的标准做法。对于静态资源如application.yml,logback.xml, 图片这些文件通常位于src/main/resources目录下。默认情况下Maven会将其复制到JAR的根目录或对应子目录。Shade或Spring Boot插件在打包时你项目中的资源文件优先级最高会覆盖依赖中同名的资源文件。如果你需要合并而非覆盖配置会非常复杂通常建议避免资源文件同名。可以将你的配置文件放在独特的路径下。4.3 Fat JAR体积过大与启动缓慢当你引入大量依赖后Fat JAR可能达到100MB甚至更大这会影响上传部署的速度并且JVM在启动时需要扫描整个JAR可能会略微影响启动时间。优化策略依赖瘦身定期运行mvn dependency:analyze。这个命令会分析哪些依赖是“已使用但未声明”需要你添加到POM和“已声明但未使用”。果断移除那些未使用的依赖。很多Starter会引入一堆你可能用不到的库。排除传递依赖如果你明确知道某个依赖的传递依赖是不需要的可以将其排除。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-logging/artifactId !-- 例如如果你打算用Log4j2 -- /exclusion /exclusions /dependency使用Spring Boot的Thin Launcher高级Spring Boot提供了一个“瘦身”解决方案它生成一个很小的“瘦JAR”这个JAR不包含依赖但在运行时从本地Maven仓库或远程仓库下载依赖。这适合容器化环境镜像可以做得非常小。但这引入了运行时对仓库的依赖增加了复杂度。分层构建Docker镜像优化在Docker化部署时可以利用Docker镜像的分层机制。将依赖JAR变动少和应用程序JAR变动频繁分到不同的层这样每次代码更新后只需要构建和推送应用程序层可以极大加快CI/CD流程。Spring Boot插件支持此功能通过配置layers实现。4.4 如何验证Fat JAR的内容打包完成后如何确认所有依赖都打进去了查看文件大小最直观的方法Fat JAR应该比普通的JAR大很多。使用jar命令查看# 列出JAR包内容 jar tf target/your-app-1.0-shaded.jar | head -20 # 对于Spring Boot Fat JAR查看lib目录 jar tf target/your-app-1.0.jar | grep BOOT-INF/lib解压查看直接将.jar文件后缀改为.zip然后用解压软件打开。你应该能看到大量第三方库的包名目录对于超级JAR或BOOT-INF/lib/文件夹里满满的JAR文件对于Spring Boot JAR。5. 进阶场景与个性化定制5.1 包含非Maven仓库的本地JAR包有时项目需要依赖一个公司内部或第三方提供的、没有上传到Maven中央仓库的JAR文件。步骤将JAR文件安装到本地Maven仓库mvn install:install-file -Dfilepath/to/your.jar -DgroupIdcom.company -DartifactIdinternal-lib -Dversion1.0 -Dpackagingjar然后在pom.xml中像普通依赖一样引用它dependency groupIdcom.company/groupId artifactIdinternal-lib/artifactId version1.0/version /dependency之后无论是shade还是spring-boot插件都会自动将其包含进Fat JAR。5.2 为Fat JAR添加启动脚本Windows/Linux对于非Spring Boot项目使用shade插件生成的JAR虽然可以java -jar运行但有时我们希望像原生应用一样双击或在命令行中直接输入app就能启动。可以使用maven-assembly-plugin生成包含脚本的发布包或者使用更专业的maven-appassembler-plugin。这里以生成一个简单的跨平台启动脚本为例需借助maven-antrun-plugin或直接在资源目录放置脚本这里不展开复杂配置。更常见的做法是在部署时由运维人员编写一个统一的启动脚本如start.sh或start.bat来执行java -jar命令并配置好JVM参数、日志路径等这比打包进JAR更灵活。5.3 与Docker集成的最佳实践现代部署离不开Docker。为Fat JAR构建Docker镜像时有几点优化建议使用多阶段构建第一阶段用Maven镜像打包第二阶段用更小的JRE镜像运行可以显著减小最终镜像体积。# 第一阶段构建 FROM maven:3.8-openjdk-11 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 第二阶段运行 FROM openjdk:11-jre-slim WORKDIR /app COPY --frombuilder /app/target/your-app-*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]利用Spring Boot的镜像分层在Dockerfile中可以结合Spring Boot插件的分层功能将依赖层、资源层、应用层分开拷贝充分利用Docker缓存。始终指定非root用户运行在Dockerfile中创建非root用户并切换是基本的安全准则。最后我想分享一个最深刻的体会“包含所有依赖”的打包方式虽然带来了部署的便利但也将依赖管理的复杂性从构建时转移到了打包结果这个“黑盒”中。你必须对项目的依赖树有清晰的了解定期审查和清理否则Fat JAR很容易变成一个臃肿的“依赖地狱”快照。在追求便利的同时保持依赖的整洁是每个负责任的开发者应该坚持的习惯。每次执行mvn clean package后不妨花一分钟看看dependency:tree的输出也许就能提前避免一个深夜的线上故障。