当前位置:首页 > 技术 > 正文内容

Maven多模块项目高效打包策略

访客 技术 2026年11月8日 1

在大型软件开发中,Maven多模块项目结构司空见惯。尤其当项目包含多个子模块,并且主模块是一个需要打包成可执行JAR的Spring Boot应用时,如何有效地进行构建、部署和发布,成为提升开发效率和运维便利性的关键。本文将探讨一套优化Maven多模块项目打包流程的策略,旨在解决复杂工程在组织、发布、交付中的常见挑战。

面临的挑战与优化契机 在日常的开发与测试环境中,我们常常发现,代码改动往往集中在少数几个模块甚至单个模块。此时,如果每次改动都需要执行一个耗时且资源消耗大的完整构建过程,会严重拖慢开发迭代速度。尽管Spring Boot DevTools可以在本地IDE环境中提供热部署能力,但在某些受限的开发或集成测试环境(例如,由于防火墙限制导致无法直接连接ZooKeeper或数据库),DevTools的功能会受到影响,从而降低开发体验。

因此,区分项目中的"稳定"部分和"频繁变动"部分,并针对性地采用不同的打包和部署策略,能够显著提高灵活性和效率。

项目结构与依赖管理 一个典型的企业级应用通常具有清晰的目录结构,例如包含bin(启动脚本)、lib(库文件)、conf(配置文件)等。Maven的maven-assembly-plugin插件可以帮助我们实现这种结构化的最终发布包(通常是一个ZIP压缩文件)。

在依赖管理方面,项目依赖可分为几类:

  1. 内部模块依赖: 项目自身的其他子模块编译生成的JAR包。
  2. 企业私服依赖: 托管在私有Maven仓库中的其他项目或团队发布的JAR包。
  3. 公共仓库依赖: 来自Maven中央仓库的第三方JAR包。

依赖资产的分类与期望路径 为了更好地组织最终发布件,我们需要明确各类资产在压缩包中的存放位置。

依赖类型 原始位置示例 期望解压后路径
启动脚本 your-application/bin /bin (顶层脚本目录)
主启动模块JAR (如 your-server.jar) your-server/target/your-server.jar / (应用根目录)
主模块资源/配置 (如 application.yml) your-server/src/main/resources /conf (配置目录)
其他业务子模块 JAR (如 your-dao.jar, your-common.jar) your-dao/target/your-dao.jar your-server.jar/ (内嵌在主JAR中)
私服内部依赖 (如 rpc-provider.jar) ~/.m2/repository your-server.jar/META-INF/lib (内嵌在主JAR的库目录)
第三方公共库 ~/.m2/repository target/lib (作为独立外部库)

多种Maven插件的协同作用 单一的Maven打包插件往往难以满足复杂多模块项目的全部需求。例如,仅使用spring-boot-maven-plugin打出的"胖JAR"可能体积庞大(数百MB),不便于传输和部署。而只使用maven-jar-plugin或maven-dependency-plugin则会失去Spring Boot应用的可执行特性。

因此,结合使用多种插件,在构建的不同阶段发挥各自的优势,是实现高效打包的关键。

常用Maven打包插件功能速览

插件名称 主要阶段 核心作用 组织范围考量
maven-jar-plugin package 将项目的类和资源打包成标准JAR文件 精细控制JAR内包含/排除的文件 (如排除配置文件等)
maven-shade-plugin package 解决依赖冲突,或将多个JAR的类合并到单个JAR内 可用于将所有内部子模块的代码合并到主应用程序JAR中
spring-boot-maven-plugin package:repackage 生成可执行的Spring Boot JAR,并设置启动布局 针对可执行应用进行优化,将依赖置于 META-INF/lib 目录
maven-dependency-plugin package:copy-dependencies 复制项目的所有编译(compile)范围依赖到指定目录 常用于将所有外部第三方依赖复制到单独的 lib 目录
maven-assembly-plugin package:single 将项目输出(包括JAR、脚本、配置等)组合成一个完整的发布包(如ZIP) 用于构建完整的部署包,包含 bin, conf, lib 等结构
maven-enforcer-plugin enforce:enforce 强制执行项目环境、依赖版本等约束 用于规范团队开发,减少潜在问题,常见于大型开源项目

实践应用与优化策略 为了适应不同的部署场景,我们可以利用Maven的profile机制,实现灵活的构建。例如,只有在Linux服务器上需要生成完整的ZIP部署包时才激活maven-dependency-plugin和maven-assembly-plugin。

<profiles>
  <profile>
    <id>linux-deploy</id>
    <activation>
      <os>
        <name>linux</name>
        <family>unix</family>
        <arch>amd64</arch>
      </os>
      <activeByDefault>false</activeByDefault>
    </activation>
    <build>
      <plugins>
        <!-- 在此配置 maven-dependency-plugin 和 maven-assembly-plugin -->
      </plugins>
    </build>
  </profile>
</profiles>

快速迭代与"瘦JAR" 在日常开发和敏捷迭代中,我们通常只需要一个轻量级的可执行JAR。这时,仅需使用maven-jar-plugin、maven-shade-plugin和spring-boot-maven-plugin即可。通过这种方式,原本可能高达200MB的完整"胖JAR"或ZIP包,可以缩减到仅2MB左右,极大地加速了发布和部署过程。

内部模块与第三方依赖的解耦打包 关键的优化点在于区分对待本项目内部的子模块依赖和外部第三方依赖。

  • maven-shade-plugin: 强烈推荐使用此插件将本项目的所有子模块(业务代码)的字节码合并到主应用程序JAR中。这样做的好处是,主JAR包内包含了所有业务逻辑,而外部的lib目录则可以纯粹地存放第三方依赖,这部分代码通常变化极少。当业务逻辑发生迭代时,只需替换主应用程序JAR即可,无需关注庞大的第三方库。 相比之下,spring-boot-maven-plugin默认会将所有依赖打包到JAR内部的META-INF/lib目录,这更适合整合外部项目或私服中的模块作为整体嵌入。

明确了这些目标后,便可以精确配置各插件的include和exclude规则,以实现对不同类型依赖的精细化控制。

各插件职责划分一览

插件 负责依赖类型 配置方法示例
spring-boot-maven-plugin 其他项目内部依赖(如私服中的RPC服务) configuration.includes
maven-dependency-plugin 外部第三方依赖 configuration.excludeArtifactIds (排除本项目子模块及已处理的私服依赖)
maven-shade-plugin 本项目的子模块依赖 configuration.artifactSet.includes

附录:Maven插件配置示例 以下配置片段应置于Spring Boot应用模块(例如your-server/pom.xml)的build标签内,或根据需要放在profile.build内。

maven-shade-plugin 配置

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-shade-plugin</artifactId>
  <version>3.5.2</version> <!-- 请根据实际情况选择稳定版本 -->
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>shade</goal>
      </goals>
      <configuration>
        <createDependencyReducedPom>false</createDependencyReducedPom>
        <artifactSet>
          <includes>
            <!-- 将本项目的业务子模块合并到主JAR中 -->
            <include>com.example.app:app-api</include>
            <include>com.example.app:app-cli</include>
            <include>com.example.app:app-common</include>
            <include>com.example.app:app-dao</include>
            <!-- 假设此模块也有需要合并的api -->
            <include>com.example.app:another-api-module</include>
          </includes>
        </artifactSet>
        <!-- 其他可选配置如relocations等,用于处理依赖冲突 -->
      </configuration>
    </execution>
  </executions>
</plugin>

spring-boot-maven-plugin 配置

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <version>${springboot.maven.plugin.version}</version> <!-- 使用属性管理版本 -->
  <executions>
    <execution>
      <goals>
        <goal>repackage</goal>
      </goals>
      <configuration>
        <attach>false</attach> <!-- 不生成以-exec结尾的额外jar包 -->
      </configuration>
    </execution>
  </executions>
  <configuration>
    <mainClass>com.example.app.ApplicationServer</mainClass> <!-- 应用程序主启动类 -->
    <layout>JAR</layout> <!-- 生成标准可执行JAR包 -->
    <executable>true</executable> <!-- 开启可执行脚本注入,便于直接运行jar -->
    <includes>
      <!-- 将非本项目但需要内嵌的私服依赖打包到META-INF/lib -->
      <include>
        <groupId>com.example.rpc</groupId>
        <artifactId>rpc-provider</artifactId>
      </include>
      <include>
        <groupId>com.example.cloud</groupId>
        <artifactId>cloud-api</artifactId>
      </include>
    </includes>
    <!-- 如果没有特定需要包含的依赖,可写 <includes><include>none:none</include></includes> -->
  </configuration>
</plugin>

maven-dependency-plugin 配置

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-dependency-plugin</artifactId>
  <version>${maven-dependency-plugin.version}</version>
  <executions>
    <execution>
      <id>copy-external-dependencies</id> <!-- 定义一个清晰的执行ID -->
      <phase>package</phase>
      <goals>
        <goal>copy-dependencies</goal>
      </goals>
      <configuration>
        <outputDirectory>${project.build.directory}/lib</outputDirectory> <!-- 输出目录为target/lib -->
        <includeScope>runtime</includeScope> <!-- 只包含运行时依赖 -->
        <stripVersion>false</stripVersion> <!-- 保留依赖的版本号 -->
        <excludeTransitive>false</excludeTransitive> <!-- 包含传递性依赖 -->
        <excludeArtifactIds>
          <!-- 排除已由shade插件处理的本项目子模块,以及由spring-boot-maven-plugin处理的其他内部依赖 -->
          app-api,app-cli,app-common,app-dao,another-api-module,rpc-provider,cloud-api
        </excludeArtifactIds>
      </configuration>
    </execution>
  </executions>
</plugin>
返回列表

上一篇:使用钉钉内网穿透工具暴露本地服务

没有最新的文章了...

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。