2.4 Assembly 交付物打包:把成品打成你要的形状 本节摘要:maven-assembly-plugin 通过描述文件自定义交付物的内容与格式,fileSet 收录项目文件、dependencySet 收录依赖,支持 zip 与 tar.gz 等格式。本节完成一次银行环境交付的完整案例,并给出 Assembly 与 Shade 的分工边界。 交付现场的一道选择题 risk-engine 要交付到银行的托管环境:对方不给外网,要求交付物是"一个压缩包,解压即一个目录,里面有程序 jar、全部依赖、启动脚本、版本说明"。 打出的瘦 jar 显然不满足——它只有自己的代码,依赖得靠 classpath 另行拼装。 这就是 Assembly 插件的战场:按你的描述组装任意形状的交付物。
本节摘要:maven-assembly-plugin 通过描述文件自定义交付物的内容与格式,fileSet 收录项目文件、dependencySet 收录依赖,支持 zip 与 tar.gz 等格式。本节完成一次银行环境交付的完整案例,并给出 Assembly 与 Shade 的分工边界。
risk-engine 要交付到银行的托管环境:对方不给外网,要求交付物是"一个压缩包,解压即一个目录,里面有程序 jar、全部依赖、启动脚本、版本说明"。mvn package 打出的瘦 jar 显然不满足——它只有自己的代码,依赖得靠 classpath 另行拼装。
这就是 Assembly 插件的战场:按你的描述组装任意形状的交付物。先把决策口径立起来,因为很多团队在 Assembly 与 Shade 之间反复纠结:
| 方案 | 产物形态 | 典型场景 | 主要代价 |
|---|---|---|---|
| Assembly(含依赖目录) | zip 或 tar.gz:程序 jar 加 lib 目录加脚本 | 离线交付、运维托管环境 | 依赖多时文件数量大 |
| Assembly(单文件内嵌) | 一个 fat jar | 工具类程序、批处理 | 类冲突叠加 无隔离 |
| Shade | 合并后的单个 jar,可改类名避冲突 | 类库复用、Spark 作业 | 类合并风险高 排查难 |
| Spring Boot repackage | 可执行 jar 内嵌容器 | Spring 服务 | 只适合 Boot 项目 |
银行场景明显属于第一行:要目录结构、要脚本、要压缩包,Assembly 描述文件正是为此而生。
描述文件是 XML,核心元素就两类:fileSet 收录项目内的文件,dependencySet 收录 Maven 解析出的依赖。给 risk-engine 写的第一版:
<assembly> <id>bank-dist</id> <formats> <!-- 产物格式:zip 起文件名后缀的作用 --> <format>zip</format> </formats> <includeBaseDirectory>true</includeBaseDirectory> <fileSets> <!-- 收录项目文件:启动脚本与说明 --> <fileSet> <directory>src/main/assembly/bin</directory> <outputDirectory>bin</outputDirectory> <fileMode>0755</fileMode> <!-- 脚本要可执行 权限位写明 --> </fileSet> <fileSet> <directory>src/main/assembly/docs</directory> <outputDirectory>docs</outputDirectory> </fileSet> </fileSets> <dependencySets> <!-- 收录全部运行期依赖 --> <dependencySet> <outputDirectory>lib</outputDirectory> <scope>runtime</scope> <useProjectArtifact>true</useProjectArtifact> <!-- 程序 jar 自身也进 lib 目录 --> </dependencySet> </dependencySets> </assembly>
POM 里的接线:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.4.2</version> <configuration> <descriptors> <!-- 描述文件路径 --> <descriptor>src/main/assembly/bank-dist.xml</descriptor> </descriptors> </configuration> <executions> <execution> <!-- 绑定 package 阶段:每次打包自动组装 --> <id>make-distribution</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>
执行与产物验证:
mvn clean package # 输出关键行:Building zip: target/risk-engine-1.0.0-bank-dist.zip # 解压验证目录结构(运维验收口径) unzip -l target/risk-engine-1.0.0-bank-dist.zip # risk-engine-1.0.0/ # bin/start.sh bin/stop.sh 启动与停止脚本 # docs/CHANGELOG.md 版本说明 # lib/risk-engine-1.0.0.jar 程序本体 # lib/jackson-databind-2.13.4.jar 运行期依赖全套
启动脚本里用通配符拼 classpath:java -cp "lib/*" com.shop.risk.Main,依赖 jar 全部躺在 lib 目录,新增或升级依赖不需要改脚本。这就是目录式交付对运维环境的价值:依赖的增减对启动命令零感知。
第一版交付当晚翻车:银行反馈启动报 NoSuchMethodError。回到依赖树一查,lib 目录里躺着两个不同大版本的同一构件——Assembly 默认忠实收录解析结果,而当时的 POM 恰好有多条路径引入同名构件。修复分两步:先按第 3 章的方法用 dependencyManagement 统一了版本;再在 dependencySet 加了一道保险:
<dependencySet> <outputDirectory>lib</outputDirectory> <scope>runtime</scope> <!-- 明确冲突时的取舍策略:优先取调解胜出版本 --> <useStrictFiltering>false</useStrictFiltering> </dependencySet>
更重要的教训是流程性的:组装类插件的产物必须做"启动冒烟"再出门。Assembly 只负责搬运文件,不校验 classpath 的完整性;内容对不对,只有真正加载一次才知道。作战室后来的规矩是交付包在离线沙箱里启动并调用健康检查,通过才允许移交。
💡 关键直觉:描述文件应随项目源码演进并纳入评审。交付物的内容(哪些文件、哪些依赖、什么权限)本质上是架构决策,散落在某个人的本地配置里,等于每次交付都在掷骰子。
第 2 章收束。构建机器的传动结构、插件军团、多环境面孔与交付形态都已就位。下一章进入全册的核心战役——依赖战争:传递依赖如何蔓延、冲突如何定位、版本仲裁如何建制。