Maven全量包打包指南:maven-shade-plugin与assembly-plugin实战
1. 项目概述:为什么我们需要一个“全量”的Maven包?
在Java后端开发中,Maven几乎是项目构建和依赖管理的代名词。我们每天都在用mvn clean package来生成那个熟悉的target/xxx.jar文件。但不知道你有没有遇到过这样的场景:当你兴冲冲地把这个jar包扔到测试服务器上,用java -jar命令启动时,控制台却报出一连串的ClassNotFoundException或NoClassDefFoundError。你一拍脑袋,才想起来这个jar包里只包含了你自己的编译类文件,项目所依赖的那些几十上百个第三方库,一个都没被打进去。
这就是标准的“瘦jar”问题。在微服务、容器化部署大行其道的今天,我们常常需要将一个应用及其所有依赖打包成一个独立的、可执行的“胖jar”(Fat Jar)或“超级jar”(Uber Jar)。这样,无论是在本地测试、CI/CD流水线,还是在Docker容器中,我们只需要传递这一个文件,就能确保运行环境的一致性,彻底告别“在我机器上是好的”这种经典难题。今天要聊的,就是如何用Maven,一劳永逸地解决打包时包含所有依赖(包括第三方依赖)的问题。这不仅仅是加个插件那么简单,里面有不少配置的“坑”和选择的“道”。
2. 核心思路与插件选型:不止一种“全量”法
当你决定要打一个全量包时,面前其实有好几条路。不同的插件,背后的哲学和产出的结果截然不同。选择哪一个,取决于你的最终目的:是要一个可执行的独立应用,还是一个方便被其他项目引用的“全量”库?
2.1 主流插件对比:maven-assembly-pluginvsmaven-shade-pluginvsspring-boot-maven-plugin
市面上主要有三个插件能帮你完成这个任务,它们各有侧重。
maven-assembly-plugin(装配插件)这是一个非常古老且通用的打包插件。它的核心思想是“装配”——你可以通过一个自定义的assembly.xml描述符,告诉Maven如何把项目的输出、依赖、资源文件甚至其他任意文件,“组装”成一个特定的发布包格式。这个格式可以是tar.gz、zip,当然也包括我们需要的包含依赖的jar。
- 优点:极其灵活,功能强大。除了打包依赖,你还能把启动脚本、配置文件、文档等一并打包,生成一个完整的发布包。
- 缺点:对于生成可执行
jar包来说,配置稍显繁琐。它只是简单地将所有依赖的jar包解压后,和你项目的类文件一起,扁平化地放入同一个目录结构下。这可能会带来依赖冲突(同名类文件被覆盖)的问题,且默认不处理MANIFEST.MF中的Main-Class和Class-Path,需要额外配置。 - 适用场景:需要生成包含完整目录结构的发布包(如
bin、lib、conf),或者打包格式不是简单jar的情况。
maven-shade-plugin(阴影插件)这个插件的名字很形象,它的核心能力是“重命名”(即制造阴影)。它会把所有依赖的jar包解压,然后将所有.class文件、资源文件重新打包进一个jar中。它的杀手锏在于可以处理依赖冲突——通过重命名冲突类所在的包,来避免类覆盖。
- 优点:生成的是标准的、单一的、可执行的
jar文件。自动处理MANIFEST.MF,可以指定主类。强大的重定位(Relocation)功能是解决依赖冲突的利器。 - 缺点:配置比
assembly插件更复杂一些,特别是当需要重定位时。由于所有类都被打平到同一个jar里,在调试时堆栈信息中的类名可能因重定位而变得陌生。 - 适用场景:需要生成一个干净、单一的可执行
jar包,并且项目依赖复杂,可能存在jar包冲突(比如不同库用了不同版本的Guava或ASM)。
spring-boot-maven-plugin如果你是Spring Boot项目,那么这就是你的“官方指定”解决方案。它在底层集成了maven-shade-plugin的能力,并针对Spring Boot应用做了大量优化和默认配置。
- 优点:开箱即用,零配置或极少配置即可生成可执行的、包含所有依赖的
jar。完美支持Spring Boot的特性,如加载BOOT-INF/classes和BOOT-INF/lib下的资源。还能生成用于分解依赖和加速容器启动的“分层jar”。 - 缺点:仅适用于Spring Boot项目。生成的
jar包结构是特殊的(BOOT-INF/),不能被当作普通库依赖直接使用。 - 适用场景:所有基于Spring Boot的应用程序打包。
注意:对于非Spring Boot的普通Java项目,如果你的目标是生成可执行
jar,maven-shade-plugin通常是更通用和专业的选择。maven-assembly-plugin则更适合需要复杂定制打包结构的场景。下文我们将以maven-shade-plugin为重点进行详细拆解,因为它最能体现“包含所有依赖”这一核心需求下的各种技术细节。
2.2 理解“包含依赖”的层次:Class Path vs Uber Jar
在深入配置前,必须厘清一个概念:我们说的“包含依赖”,到底指的是什么?
- 在Class Path中引用:传统的
MANIFEST.MF中通过Class-Path属性列出所有依赖jar的路径。这要求运行环境里,这些jar包必须存在于指定路径。这并没有真正“包含”。 - 解压后合并(Uber Jar):将所有依赖
jar包解压,将其中的.class文件和资源文件,与项目自身的文件合并,重新打包到一个新的jar包中。这才是真正的“全量包”。maven-shade-plugin和spring-boot-maven-plugin采用的就是这种方式。
我们的目标显然是第二种。
3. 使用maven-shade-plugin打造可执行全量包
让我们从一个最基础的、非Spring Boot的Java项目开始,一步步配置出一个完美的全量jar包。
3.1 基础POM配置与参数解析
首先,在项目的pom.xml文件中,添加maven-shade-plugin的配置。
<project> <!-- ... 其他配置 ... --> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.1</version> <!-- 请使用最新稳定版 --> <executions> <execution> <phase>package</phase> <!-- 绑定到package生命周期阶段 --> <goals> <goal>shade</goal> <!-- 执行shade目标 --> </goals> <configuration> <!-- 核心配置区域 --> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>关键配置项详解:
createDependencyReducedPom- 作用:是否创建一个简化版的
pom.xml。当你的jar包被其他项目依赖时,这个简化pom会移除那些已经被“阴影化”(打包进来)的依赖,避免传递依赖冲突。 - 值:
true/false,默认为true。 - 实操心得:如果你打出来的
jar包是用于最终发布运行,而不是作为库给别的项目用,建议设置为false,可以避免生成多余的dependency-reduced-pom.xml文件,让项目结构更干净。
- 作用:是否创建一个简化版的
filters- 作用:过滤资源文件。有些依赖的
META-INF目录下可能存在签名文件(如.SF、.DSA、.RSA),如果多个jar包都有签名,合并时会冲突导致验证失败。 - 常用配置:
<configuration> <filters> <filter> <artifact>*:*</artifact> <!-- 匹配所有构件 --> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> - 踩坑记录:如果不排除这些签名文件,运行时报
java.lang.SecurityException: Invalid signature file digest错误的概率极高。这几乎是使用shade插件必配的一项。
- 作用:过滤资源文件。有些依赖的
3.2 配置可执行JAR与主类
要让打出来的jar包能用java -jar直接运行,必须指定主类并配置MANIFEST.MF。
<configuration> <!-- ... 其他配置 ... --> <transformers> <!-- 设置Manifest主类 --> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.yourcompany.yourapp.Main</mainClass> <!-- 替换为你的主类全限定名 --> </transformer> </transformers> </configuration>ManifestResourceTransformer:这个转换器专门用于处理META-INF/MANIFEST.MF文件。这里我们只指定了Main-Class,它还会自动帮你合并其他必要的属性。
3.3 处理资源文件与依赖冲突(重定位)
这是maven-shade-plugin最强大的部分。
资源文件合并问题:多个依赖jar包可能包含同名的资源文件,例如META-INF/services/javax.xml.parsers.SAXParserFactory(服务加载文件)。默认情况下,后面的会覆盖前面的,这可能导致某些 SPI 机制失效。
解决方案:使用ServicesResourceTransformer。
<transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.yourcompany.yourapp.Main</mainClass> </transformer> <!-- 合并Service Loader配置文件 --> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> </configuration>依赖冲突(包名冲突)问题:这是更棘手的问题。比如,你的项目同时依赖了库A和库B,它们都使用了com.google.common.collect包下的类,但版本不同。简单合并会导致其中一个版本的类被覆盖,运行时行为不可预测。
终极武器:重定位(Relocation)重定位的原理是,在打包时修改某些类(来自特定依赖)的包名,为它们创建一个新的、唯一的命名空间,从而避免冲突。
<configuration> <!-- ... 其他配置 ... --> <relocations> <relocation> <pattern>com.google.common</pattern> <!-- 原始包名 --> <shadedPattern>com.yourcompany.shaded.google.common</shadedPattern> <!-- 重定位后的包名 --> <excludes> <exclude>com.google.common.test.**</exclude> <!-- 可选:排除测试包 --> </excludes> </relocation> <!-- 可以配置多个重定位规则 --> <relocation> <pattern>org.apache.commons.io</pattern> <shadedPattern>com.yourcompany.shaded.apache.commons.io</shadedPattern> </relocation> </relocations> </configuration>pattern:需要重定位的原始包名前缀。shadedPattern:重定位后的新包名前缀。excludes:可以排除某些不需要重定位的子包。- 实操心得:重定位不是免费的。所有使用了被重定位类的代码,包括你自己的代码(如果你直接引用了
Guava),其 import 语句也需要相应修改。通常,我们只重定位那些“传递性依赖”,即你项目代码不直接引用,但多个底层库都依赖且版本冲突的组件。确定是否需要重定位,以及重定位哪些包,需要分析项目的依赖树(mvn dependency:tree)。
3.4 一个完整的、生产可用的配置示例
结合以上所有要点,一个健壮的配置如下:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.1</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> <!-- 有时也需要排除LICENSE文件以避免重复 --> <exclude>META-INF/LICENSE</exclude> <exclude>META-INF/LICENSE.txt</exclude> </excludes> </filter> </filters> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.myapp.Application</mainClass> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> <!-- 合并Apache许可证等NOTICE文件 --> <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer"/> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.handlers</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.schemas</resource> </transformer> </transformers> <!-- 按需配置重定位 --> <!-- <relocations>...</relocations> --> </configuration> </execution> </executions> </plugin>配置完成后,运行mvn clean package。在target目录下,你会找到两个jar文件:一个是原始的your-artifact-version.jar(瘦jar),另一个是your-artifact-version-shaded.jar或your-artifact-version.jar(胖jar,原始瘦jar会被替换,取决于配置)。这个胖jar就是包含了所有依赖的可执行文件。
4. 使用maven-assembly-plugin的替代方案
如果你需要更多的打包控制,比如生成一个zip包,里面包含bin(启动脚本)、lib(依赖jar包)、conf(配置文件)这样的标准目录结构,maven-assembly-plugin是更好的选择。
4.1 定义Assembly描述符
首先,在src/main/assembly目录下创建一个package.xml文件(目录和文件名可自定义)。
<!-- src/main/assembly/package.xml --> <assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0 http://maven.apache.org/xsd/assembly-2.2.0.xsd"> <id>full</id> <!-- 构建后缀,如 -full.zip --> <formats> <format>zip</format> <!-- 打包格式,也可以是 dir, tar.gz, jar 等 --> </formats> <includeBaseDirectory>false</includeBaseDirectory> <!-- 是否在包内包含项目根目录名 --> <dependencySets> <dependencySet> <outputDirectory>/lib</outputDirectory> <!-- 依赖包输出到 /lib 目录 --> <scope>runtime</scope> <!-- 包含运行时依赖 --> <unpack>false</unpack> <!-- 不解压,直接拷贝jar文件 --> </dependencySet> </dependencySets> <fileSets> <fileSet> <directory>${project.basedir}/src/main/resources</directory> <outputDirectory>/conf</outputDirectory> <!-- 配置文件放到 /conf --> <includes> <include>*.yml</include> <include>*.properties</include> <include>*.xml</include> </includes> </fileSet> <fileSet> <directory>${project.build.directory}</directory> <outputDirectory>/</outputDirectory> <!-- 项目主jar包放到根目录 --> <includes> <include>${project.build.finalName}.jar</include> </includes> </fileSet> <fileSet> <directory>${project.basedir}/scripts</directory> <outputDirectory>/bin</outputDirectory> <!-- 启动脚本放到 /bin --> <fileMode>0755</fileMode> <!-- 赋予脚本可执行权限 --> <includes> <include>startup.sh</include> <include>shutdown.sh</include> </includes> </fileSet> </fileSets> </assembly>4.2 配置POM并绑定执行
然后在pom.xml中配置插件,并指向这个描述符。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.7.1</version> <configuration> <descriptors> <descriptor>src/main/assembly/package.xml</descriptor> </descriptors> <!-- 如果你希望主jar包也是可执行的,需要额外配置manifest --> <archive> <manifest> <mainClass>com.example.myapp.Application</mainClass> </manifest> </archive> </configuration> <executions> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </executions> </plugin>运行mvn clean package后,在target目录下会生成一个your-artifact-version-full.zip文件。解压后,你会看到清晰的目录结构,这种格式对于需要手动部署或对运行环境有严格目录要求的场景非常友好。
5. Spring Boot项目的“开箱即用”方案
对于Spring Boot项目,一切变得非常简单。你只需要引入spring-boot-maven-plugin,它默认就绑定了repackage目标到package阶段。
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <!-- version通常由spring-boot-starter-parent管理 --> </plugin> </plugins> </build>执行mvn clean package后,target目录下会生成两个文件:
your-artifact-version.jar.original:这是Maven标准插件生成的原始“瘦jar”。your-artifact-version.jar:这是Spring Boot插件重新打包后的“胖jar”(可执行jar)。
这个胖jar的内部结构是特殊的:
example.jar | +-META-INF | +-MANIFEST.MF (包含Main-Class: org.springframework.boot.loader.JarLauncher) +-BOOT-INF | +-classes (你的应用类文件) | +-lib (所有第三方依赖jar包) +-org +-springframework +-boot +-loader (Spring Boot的类加载器)这种结构使得Spring Boot能够支持从jar包内嵌套的jar文件中加载类,这是一个关键创新。插件会自动处理依赖、资源合并和主类配置,几乎无需额外操心。
高级配置:你还可以配置分层,优化Docker镜像构建。
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <layers> <enabled>true</enabled> </layers> </configuration> </plugin>启用分层后,打包时会生成一个layers.idx文件,将依赖、资源等按变更频率分层。在构建Docker镜像时,可以将不常变的层(如依赖库)放在底层,利用Docker缓存大幅提升构建和推送速度。
6. 常见问题、排查技巧与实操心得
即使配置正确,打包和运行过程中也可能遇到各种问题。这里记录一些典型的“坑”和解决方法。
6.1 类找不到(ClassNotFoundException/NoClassDefFoundError)
这是最常见的问题。
- 排查步骤1:检查依赖范围。在
pom.xml中,依赖的<scope>很重要。test范围的依赖不会被打包。确保运行时必需的依赖是compile(默认)或runtime范围。 - 排查步骤2:确认包是否真的打进去了。使用
jar tf your-shaded.jar | grep 'ClassName'命令(Linux/Mac)或在Windows上用压缩软件打开生成的胖jar,检查对应的类文件是否存在。如果不存在,说明打包过程可能过滤掉了。 - 排查步骤3:检查重定位规则。如果你配置了重定位,但你的代码或某些配置文件(如Spring的XML、注解扫描路径)仍然引用了原始的包名,就会导致类找不到。需要确保所有引用点都更新到新的包名。
6.2 版本冲突与诡异行为
两个不同版本的库被合并,高版本覆盖了低版本,但高版本的API可能不兼容。
- 排查:运行
mvn dependency:tree -Dverbose查看详细的依赖树,寻找冲突。在IDE中,也可以使用依赖分析工具。 - 解决:
- 排除法:在引入依赖时,使用
<exclusions>排除掉传递进来的冲突版本。<dependency> <groupId>com.some.group</groupId> <artifactId>some-artifact</artifactId> <exclusions> <exclusion> <groupId>conflicting-group</groupId> <artifactId>conflicting-artifact</artifactId> </exclusion> </exclusions> </dependency> - 重定位法:如前所述,使用
maven-shade-plugin的重定位功能,将冲突的库隔离起来。 - 统一版本管理:在
pom.xml的<dependencyManagement>节或父POM中强制指定某个库的版本。
- 排除法:在引入依赖时,使用
6.3 资源文件丢失或服务加载失败
比如使用了Java的ServiceLoader机制(如JDBC驱动、SLF4J绑定器)或Spring的spring.handlers/spring.schemas,合并后资源文件被覆盖。
- 解决:确保在
shade插件配置中包含了ServicesResourceTransformer和AppendingTransformer(用于Spring的XML schema文件)。对于其他需要合并的资源,可以使用AppendingTransformer并指定资源路径。
6.4 打包速度慢或JAR文件巨大
当项目依赖非常多时,打包过程可能会很慢,生成的jar包可能达到100MB以上。
- 优化建议1:排除不必要的依赖。仔细审查依赖树,移除那些未被实际使用的依赖(可以使用
mvn dependency:analyze辅助分析)。 - 优化建议2:拆分模块。对于非常庞大的单体应用,考虑将其拆分为多个服务或模块,每个模块独立打包,体积自然减小。
- 优化建议3:使用Spring Boot分层。如前所述,这虽然不减小编译后的包体积,但能优化Docker镜像的构建和分发效率。
- 实操心得:在CI/CD流水线中,可以将打好的全量包上传到制品库(如Nexus、Jfrog Artifactory),而不是每次构建都重新打包。可以配置Maven只在版本发布时执行
shade或assembly这类重型操作。
6.5 如何验证打包结果?
打包完成后,不要直接扔到生产环境。有几个快速的验证方法:
- 检查内容:
jar tf target/your-app.jar | head -30快速浏览jar包顶层结构。 - 检查主清单:
jar xf target/your-app.jar META-INF/MANIFEST.MF && cat META-INF/MANIFEST.MF查看Main-Class是否正确设置。 - 本地试运行:
java -jar target/your-app.jar --spring.profiles.active=test(Spring Boot)或java -jar target/your-app.jar直接运行,观察启动日志和基本功能是否正常。 - 依赖验证:写一个简单的集成测试,在测试中启动这个jar包并调用一个核心接口。
打包看似是开发流程的最后一步,但一个稳定、可靠、高效的打包策略,是应用顺利交付和稳定运行的基石。从简单的可执行jar到复杂的定制化发布包,Maven的插件生态给了我们充足的工具。理解每个插件的原理和适用场景,根据项目的实际需求(是微服务、库、还是桌面应用)来选择和配置,才能让“打包”这件事从烦恼变成保障。我个人在经历了多次因打包问题导致的深夜排查后,最大的体会就是:在项目初期就确定并标准化打包方案,写好文档,能省去后期无数的麻烦。对于团队项目,一个配置好的pom.xml和清晰的README,比任何口头交接都管用。
