Maven全量包打包指南:maven-shade-plugin与assembly-plugin实战

发布时间:2026/8/14 10:44:31
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项目如果你的目标是生成可执行jarmaven-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 groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.5.1/version !-- 请使用最新稳定版 -- executions execution phasepackage/phase !-- 绑定到package生命周期阶段 -- goals goalshade/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 excludeMETA-INF/*.SF/exclude excludeMETA-INF/*.DSA/exclude excludeMETA-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 implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.yourcompany.yourapp.Main/mainClass !-- 替换为你的主类全限定名 -- /transformer /transformers /configurationManifestResourceTransformer这个转换器专门用于处理META-INF/MANIFEST.MF文件。这里我们只指定了Main-Class它还会自动帮你合并其他必要的属性。3.3 处理资源文件与依赖冲突重定位这是maven-shade-plugin最强大的部分。资源文件合并问题多个依赖jar包可能包含同名的资源文件例如META-INF/services/javax.xml.parsers.SAXParserFactory服务加载文件。默认情况下后面的会覆盖前面的这可能导致某些 SPI 机制失效。解决方案使用ServicesResourceTransformer。transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.yourcompany.yourapp.Main/mainClass /transformer !-- 合并Service Loader配置文件 -- transformer implementationorg.apache.maven.plugins.shade.resource.ServicesResourceTransformer/ /configuration依赖冲突包名冲突问题这是更棘手的问题。比如你的项目同时依赖了库A和库B它们都使用了com.google.common.collect包下的类但版本不同。简单合并会导致其中一个版本的类被覆盖运行时行为不可预测。终极武器重定位Relocation重定位的原理是在打包时修改某些类来自特定依赖的包名为它们创建一个新的、唯一的命名空间从而避免冲突。configuration !-- ... 其他配置 ... -- relocations relocation patterncom.google.common/pattern !-- 原始包名 -- shadedPatterncom.yourcompany.shaded.google.common/shadedPattern !-- 重定位后的包名 -- excludes excludecom.google.common.test.**/exclude !-- 可选排除测试包 -- /excludes /relocation !-- 可以配置多个重定位规则 -- relocation patternorg.apache.commons.io/pattern shadedPatterncom.yourcompany.shaded.apache.commons.io/shadedPattern /relocation /relocations /configurationpattern需要重定位的原始包名前缀。shadedPattern重定位后的新包名前缀。excludes可以排除某些不需要重定位的子包。实操心得重定位不是免费的。所有使用了被重定位类的代码包括你自己的代码如果你直接引用了Guava其 import 语句也需要相应修改。通常我们只重定位那些“传递性依赖”即你项目代码不直接引用但多个底层库都依赖且版本冲突的组件。确定是否需要重定位以及重定位哪些包需要分析项目的依赖树mvn dependency:tree。3.4 一个完整的、生产可用的配置示例结合以上所有要点一个健壮的配置如下plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.5.1/version executions execution phasepackage/phase goals goalshade/goal /goals configuration createDependencyReducedPomfalse/createDependencyReducedPom filters filter artifact*:*/artifact excludes excludeMETA-INF/*.SF/exclude excludeMETA-INF/*.DSA/exclude excludeMETA-INF/*.RSA/exclude !-- 有时也需要排除LICENSE文件以避免重复 -- excludeMETA-INF/LICENSE/exclude excludeMETA-INF/LICENSE.txt/exclude /excludes /filter /filters transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.example.myapp.Application/mainClass /transformer transformer implementationorg.apache.maven.plugins.shade.resource.ServicesResourceTransformer/ !-- 合并Apache许可证等NOTICE文件 -- transformer implementationorg.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer/ 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 !-- 按需配置重定位 -- !-- 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 xmlnshttp://maven.apache.org/ASSEMBLY/2.2.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/ASSEMBLY/2.2.0 http://maven.apache.org/xsd/assembly-2.2.0.xsd idfull/id !-- 构建后缀如 -full.zip -- formats formatzip/format !-- 打包格式也可以是 dir, tar.gz, jar 等 -- /formats includeBaseDirectoryfalse/includeBaseDirectory !-- 是否在包内包含项目根目录名 -- dependencySets dependencySet outputDirectory/lib/outputDirectory !-- 依赖包输出到 /lib 目录 -- scoperuntime/scope !-- 包含运行时依赖 -- unpackfalse/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 -- fileMode0755/fileMode !-- 赋予脚本可执行权限 -- includes includestartup.sh/include includeshutdown.sh/include /includes /fileSet /fileSets /assembly4.2 配置POM并绑定执行然后在pom.xml中配置插件并指向这个描述符。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-assembly-plugin/artifactId version3.7.1/version configuration descriptors descriptorsrc/main/assembly/package.xml/descriptor /descriptors !-- 如果你希望主jar包也是可执行的需要额外配置manifest -- archive manifest mainClasscom.example.myapp.Application/mainClass /manifest /archive /configuration executions executions execution idmake-assembly/id phasepackage/phase goals goalsingle/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 groupIdorg.springframework.boot/groupId artifactIdspring-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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId configuration layers enabledtrue/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 groupIdcom.some.group/groupId artifactIdsome-artifact/artifactId exclusions exclusion groupIdconflicting-group/groupId artifactIdconflicting-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.activetestSpring Boot或java -jar target/your-app.jar直接运行观察启动日志和基本功能是否正常。依赖验证写一个简单的集成测试在测试中启动这个jar包并调用一个核心接口。打包看似是开发流程的最后一步但一个稳定、可靠、高效的打包策略是应用顺利交付和稳定运行的基石。从简单的可执行jar到复杂的定制化发布包Maven的插件生态给了我们充足的工具。理解每个插件的原理和适用场景根据项目的实际需求是微服务、库、还是桌面应用来选择和配置才能让“打包”这件事从烦恼变成保障。我个人在经历了多次因打包问题导致的深夜排查后最大的体会就是在项目初期就确定并标准化打包方案写好文档能省去后期无数的麻烦。对于团队项目一个配置好的pom.xml和清晰的README比任何口头交接都管用。