ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Maven多模块工程:父项目打包时子模块不构建的解决方案

Maven多模块工程:父项目打包时子模块不构建的解决方案 先说结论这不是 IDEA 的 bug也不完全是 Maven 配置写错而是大多数人对“父项目打包”这个动作的理解和 Maven 实际行为之间差了层窗户纸。我见过不少同事在 IDEA 里对父工程右键点Maven Lifecycle package跑完发现 target 目录里只有一个孤零零的父包子模块一个都没动第一反应就是“IDEA 是不是出问题了”。这篇文章就专门把这层窗户纸捅破讲清楚父项目打包时子项目的设置逻辑顺带把 Maven 多模块工程在 IDEA 里正确打包的姿势一起给出来。这里说的“父项目打包”实际指的是 Maven 聚合工程里的父 POM。你在 IDEA 里看到的父子结构本质上是 Maven 的parent继承和modules聚合两个概念叠加在一起。如果你对这两个概念没有任何区分那后面所有排查都会绕圈子。这篇文章会从最核心的 packaging 和 modules 配置讲起再带你把 IDEA 里的操作流程走一遍最后把最常见的坑和排查思路全部列出来。无论你是刚接触 Maven 多模块工程的新手还是已经在用 Spring Cloud、Dubbo 这类多模块项目的开发者都值得花几分钟看完。1. 先搞清楚Maven 的“父项目打包”到底是个什么动作1.1 聚合与继承两个容易混的概念很多人在父 POM 里看到parent和modules两个标签下意识觉得是同一个东西其实完全不是。继承解决的是“子工程和父工程之间的依赖关系”。子模块通过parent指向父 POM从而继承父 POM 里声明的依赖版本、插件配置、仓库地址等公共信息。它解决的是“重复配置”的问题让多个子模块不用各自维护一份相同的依赖版本清单。聚合解决的是“一次构建多个模块”的问题。父 POM 通过modules声明自己下面有哪些子模块这样当你在父工程目录执行 Maven 命令时Maven 会按顺序把这些子模块全部纳入本次构建这个机制叫做 reactor反应堆。它解决的是“构建顺序和批量构建”的问题。继承不要求父 POM 和子模块在同一个仓库里子模块完全可以引用一个外部的父 POM聚合则要求modules里声明的模块目录确实存在于当前工程中。理解这个区别之后你就知道为什么“父项目打包”这件事经常出问题如果父 POM 只配置了parent相关的继承结构但modules里没有把所有子模块列进去那 Maven 执行打包时根本不知道有这些子模块存在自然不可能去构建它们。1.2 为什么父 POM 打包不会自动带上子模块这里的核心问题其实是你在 IDEA 里点击父项目的package时Maven 会做什么Maven 的 reactor 机制会读取当前 POM 的modules把所有声明的子模块加入构建队列并按照依赖关系排序后逐个执行对应的 lifecycle。也就是说如果父 POM 的modules配置完整且正确执行mvn package时 Maven 确实会依次构建所有子模块。这在命令行下表现得非常明显你会看到 Build Reactor 列表里列出了 parent 和各个子模块。但为什么大家会觉得很困惑因为大多数人忽略了packaging这个设置对 Maven 构建行为的影响。父 POM 的packaging有三种常见类型packaging作用典型场景pom只作为一个描述性 POM本身不产生任何可部署的产物只用来聚合模块或管理依赖父工程、聚合工程jar会被构建成一个可复用的 JAR 包普通 Java 库、Spring Boot 模块war会被构建成一个 Web 应用包传统 Web 项目当父项目的packaging是pom时Maven 在父项目上执行package并不会产生一个 jar 包它只会帮你去触发子模块的构建同时把父 POM 自身安装到本地仓库或部署到远程仓库。如果你把父项目设置成jarMaven 就会试图把父 POM 打成一个 jar 包而这个 jar 包里通常是空的没有实际代码没有任何意义。更关键的是这种错误的 packaging 设置会让 Maven 的 reactor 行为变得奇怪父项目自身会执行完整的 jar 构建流程导致整体构建时间变长也可能掩盖真正的问题。所以你在 IDEA 里看到“父项目打包不打包子项目”先不要急着怀疑 IDEA 的 Maven 插件第一步永远是去查父 POM 的 packaging 和 modules 配置。1.3 一个最典型的父 POM 结构我直接给一个典型的聚合工程父 POM 示例你看完就明白正确的结构长什么样了。project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIddemo-parent/artifactId version1.0.0-SNAPSHOT/version !-- 关键父项目 packaging 必须为 pom -- packagingpom/packaging modules moduledemo-common/module moduledemo-service/module moduledemo-web/module /modules properties spring.boot.version2.7.18/spring.boot.version maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version${spring.boot.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement build pluginManagement plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.8.1/version configuration source${maven.compiler.source}/source target${maven.compiler.target}/target /configuration /plugin /plugins /pluginManagement /build /project注意modules里写的是模块的相对路径相对父 POM 的目录不是 artifactId。很多人把module里的内容写成demo-common但实际目录名可能叫common-core这会导致 Maven 找不到模块目录而报错或跳过。1.4 IDEA 里“父项目”和“子项目”的展示逻辑IDEA 的 Maven 工具窗口里每一个 Maven 工程都会被识别为一个根节点父项目会显示为父节点子模块会折叠在父节点下面。如果你发现 IDEA 里只显示了一个父项目没有展开看到子模块那大概率是你的modules配置有问题或者 IDEA 还没有成功导入子模块。这时候不要急着去改代码先执行一次 Maven 的reimport。在 IDEA 右侧 Maven 面板里点击刷新按钮或者在 pom.xml 上右键选择Maven Reload Project。重新加载后父节点下面应该出现所有子模块。一个小技巧IDEA 底部有一个 Maven 工具栏你可以在View Tool Windows Maven里打开它。它会把所有模块按照层级展示出来每层都可以展开看 Lifecycle、Dependencies、Plugins。如果你的子模块没出现在这里那你打包的时候怎么折腾都不会带上它们。2. 核心设置packaging、modules 和父子依赖2.1 packaging 写成 pom 才是父项目的地基前面说过父项目 packaging 必须是pom这是聚合工程的铁律。很多从单体项目转多模块开发的同学习惯性地把父项目 packaging 留空。Maven 默认 packaging 是jar这样父项目就不算一个合格的聚合工程虽然 Maven 有时候能容错运行但各种奇怪问题会接踵而来。如果父 POM 的 packaging 是 jar执行mvn package时Maven 会把父项目当作一个普通 Java 模块来构建它不会去检查modules自然也不会构建子模块。更坑的是它还会执行编译流程要求父目录下必须有源码目录和 Java 文件否则编译报错。所以你如果遇到“父项目打包时子项目完全不参与”第一步就是确认packagingpom/packaging有没有写。没写的话补上。2.2 modules 里写的是模块路径不是 artifactId再看一遍modules标签的写法。很多人会在这里犯一个非常隐蔽的错写对了模块名但模块实际目录名不匹配。假设你的子模块 artifactId 叫demo-common但磁盘上的目录叫common。Maven 对module标签的解析规则是把它当作相对路径去父 POM 所在目录下找这个路径找到后读取该目录下的 pom.xml再通过 pom.xml 里的 artifactId 来确认模块身份。如果你写的是demo-common目录却叫commonMaven 就会报错找不到模块。还有一种情况模块目录里没有 pom.xml。常见的错误是把一个普通目录误加进modules导致 reactor 构建时报错。所以在配置modules时务必确认每个路径都能找到对应目录且目录下存在 pom.xml。2.3 子模块如何正确引用父 POM子模块的 pom.xml 里需要声明 parent这样才能继承父 POM 的依赖和插件配置。标准写法如下。parent groupIdcom.example/groupId artifactIddemo-parent/artifactId version1.0.0-SNAPSHOT/version relativePath../pom.xml/relativePath /parent modelVersion4.0.0/modelVersion artifactIddemo-common/artifactId这里有一个relativePath标签。它表示父 POM 相对于子模块 POM 的路径。默认情况下Maven 会先查看../pom.xml如果找不到再去本地仓库找。建议在子模块中显式写上relativePath一方面加速解析另一方面避免 Maven 跑到远程仓库找父 POM导致拿到旧版本。不过要注意如果你本地仓库有同 groupId、同 artifactId、同 version 的旧版父 POM而子模块强制指定了relativePath那它会优先使用相对路径下的父 POM不会受影响。2.4 dependencyManagement 和 parent 的关系父 POM 里用dependencyManagement统一管理依赖版本这是多模块项目最常用的做法。它不会立即给子模块引入依赖而是定义了一份“版本字典”子模块需要哪个依赖就自己声明但不写版本号。dependencyManagement dependencies dependency groupIdcom.example/groupId artifactIddemo-common/artifactId version${project.version}/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version${spring.boot.version}/version /dependency /dependencies /dependencyManagement子模块里这样就够了dependencies dependency groupIdcom.example/groupId artifactIddemo-common/artifactId /dependency /dependencies这样做的好处是版本号在父 POM 里只出现一次升级版本时只改父 POM不用每个子模块都去动。这里会引出一个模块间依赖的坑如果demo-service依赖demo-common而demo-common没有被显式包含在聚合构建里或者没有被安装到本地仓库那么单独构建demo-service时可能报错找不到依赖。后面会专门说这个问题。3. IDEA 中实操从配置到验证一套完整流程3.1 准备标准的多模块工程结构在 IDEA 里手把手搭一个多模块工程建议按下面的目录结构来。demo-parent/ ├── pom.xml ├── demo-common/ │ ├── pom.xml │ └── src/main/java/... ├── demo-service/ │ ├── pom.xml │ └── src/main/java/... └── demo-web/ ├── pom.xml └── src/main/java/...在 IDEA 中创建时“父工程”不要勾选模板只需在右侧 Maven 坐标里填好 groupId、artifactId、version然后修改父 POM 的 packaging 为 pom再手动把modules配置写进去。每个子模块可以直接在父工程目录下右键New Module选择 Maven 类型创建。IDEA 会自动补全子模块的parent部分。如果你是从 Git 上拉下来的现成工程直接以父 POM 作为工程文件打开IDEA 会自动识别并导入子模块。3.2 IDEA Maven 面板的正确查看方式打开 IDEA 右侧 Maven 工具窗口你会看到类似下面的层级demo-parent ├── Lifecycle ├── Dependencies ├── Plugins └── demo-common ├── Lifecycle ├── Dependencies └── Plugins每个子模块下面都有独立的 Lifecycle 列表里面包含clean、validate、compile、test、package、install、deploy等常用命令。这个列表实际上是 Maven 默认生命周期各个 phase 的映射。需要留意的一点是IDEA 上显示的 Lifecycle 列表只是方便你快速执行某个 phase它并不代表 Maven 内部只有这些命令。你在父项目上点packageMaven 实际执行的是从 lifecycle 第一个 phase 到package的全部阶段对当前 reactor 内的所有模块都生效。如果你只想构建某个子模块可以在 Maven 面板中展开该子模块右键它的package这样 Maven 只会构建该模块以及它依赖的其他模块。但这里有个细节如果该子模块依赖的兄弟模块还没有安装到本地仓库直接点击它的package也能成功前提是这个兄弟模块也在这个 reactor 中且你是用父项目触发的。3.3 三种打包方式对比package / install / deployIDEA 的 Maven 面板里package、install、deploy是三个高频按钮很多人分不清它们的区别在这个问题上也很关键。命令行为产物去向场景mvn package编译、测试并打包本模块 target 目录只想看看产物或本地调试验证mvn install编译、测试、打包并安装到本地仓库本模块 target 本地 Maven 仓库需要被其他本地工程依赖mvn deploy编译、测试、打包、安装并部署到远程仓库本地仓库 私服/中央仓库发布给团队或外部使用在父项目上执行package子模块的 jar 只会出现在各个子模块自己的 target 目录不会安装到本地仓库。如果兄弟模块之间互相依赖而且没有先 install 过那么单独对某个子模块执行package时它去找被依赖的兄弟模块只能去本地仓库找找不到就会报错。这时候有两种解法一是先在父项目上执行install把所有子模块装进本地仓库再单独打包二是用后面要讲的-pl和-am参数让 Maven 在你指定构建目标模块的同时自动把依赖到的兄弟模块也加进 reactor。3.4 只打包某个子模块的正确姿势很多场景下你并不需要把整个父项目全部打包只想打包demo-web这一个模块。但直接对demo-web执行mvn package如果它依赖demo-common而本地仓库里没有demo-common的对应版本就会直接失败。正确做法是在父项目目录下执行带-pl和-am参数的 Maven 命令mvn package -pl demo-web -am参数说明-pl后面跟的是要构建的模块列表可以写多个用逗号分隔例如-pl demo-web,demo-service。-am是--also-make的缩写意思是构建指定模块的同时把模块依赖到的其他模块也加入 reactor 一起构建。如果不加-amMaven 只会构建指定模块不会去构建它的兄弟依赖模块。在 IDEA 里操作的话可以在 Maven 面板右上角的执行配置里填入这些参数。点开面板上的Maven Executor设置或者在 Runner 的 VM Options 旁边的 Command line 里手动输入。这个组合参数在 CI/CD 流水线上也很有用。比如 Jenkins 里面配置打包任务时只需要构建某个改动的服务直接在构建命令里加上-pl和-am能省下大量全量构建的时间。3.5 全量打包父项目 install 的真实行为当你在 IDEA 里选中父项目执行installMaven 会启动 reactor把父 POM 以及modules里声明的所有子模块按依赖关系排序后逐一构建。执行过程中每个模块都会经历完整的 lifecyclecompile、test、package、install。这个完整过程跑下来最终所有子模块的 jar 都会安装到本地 Maven 仓库。之后再对单个子模块单独执行package就能在本地仓库找到对应的兄弟依赖不会报错。所以遇到“父项目打包不带子项目”的另一个常见原因是你执行的是package而不是install并且子模块之间存在依赖关系。但注意这并不能解释“子模块根本没有构建”的情况。真正“子模块完全不参与构建”的原因还是modules配置或 packaging 配置有问题。4. 常见问题排查与技巧实录4.1 父项目 packaging 没设 pom直接报错或行为异常我在排查这类问题时见过最多的情况就是父 POM 的 packaging 没有写。Maven 默认把它当 jar 处理执行mvn package时会尝试编译父项目如果父项目目录下只有 pom.xml 没有 src那就会直接编译失败。这种问题通常表现为在 IDEA 里右键父项目点击package然后控制台报错找不到源码目录或者干脆 BUILD FAILURE。解决方式很简单父 POM 加上packagingpom/packaging重新 reimport 即可。怎么验证 packaging 生效执行mvn help:effective-pom能看到当前 POM 生效后的最终状态里面会显示 packaging 为 pom。4.2 modules 标签没有把所有子模块列出来这是一种非常隐蔽的情况。项目明明有 5 个子模块但父 POM 的modules里只写了 3 个。IDEA 里可能会显示全部 5 个模块因为 IDEA 可能通过子模块自身的parent识别到了它们。但 Maven 的 reactor 只认modules里的内容所以执行父项目打包时没有被列入的 2 个子模块完全不参与构建。这种问题的特征是你看到 IDEA 里项目结构很完整但构建结果里始终没有那 2 个模块的产物。排查思路是直接打开父 POM检查modules列表跟实际子模块目录一一比对。4.3 IDEA 里 Maven 视图没自动刷新界面和实际工程对不上IDEA 的 Maven 视图偶尔会“失真”。比如你从 Git 拉取代码后其他人改了 pom.xmlIDEA 不会每次自动刷新。这时候你看到的结构可能是旧的某个新增的子模块没有显示或者某个移除的模块还在列表里。遇到这种问题执行一次Reload All Maven Projects。IDEA 会重新解析每个模块的 pom.xml刷新整个 Maven 视图。在 Maven 面板左上角有一个刷新图标点击它或者右键父 POM 选择Maven Reload Project。这里有个更隐蔽的情况IDEA 缓存了错误的模块状态即使 reload 之后模块仍然不出现。此时可以执行File Invalidate Caches / Restart清缓存重启 IDE再重新导入。4.4 子模块之间互相依赖单独 package 子模块失败这个问题在拆分微服务时特别常见。demo-web依赖demo-servicedemo-service依赖demo-common。如果你在 IDEA 里单独对demo-web执行package而demo-common和demo-service都没有安装到本地仓库Maven 会报错Could not resolve dependencies for project com.example:demo-web:1.0.0-SNAPSHOT Failure to find com.example:demo-common:1.0.0-SNAPSHOT原因是 Maven 对单个模块执行构建时不会自动去寻找它的兄弟模块只会去本地仓库找依赖。解决办法有两条在父项目上执行mvn install -pl demo-common,demo-service -am先把需要的兄弟模块安装到本地仓库。以后都从父项目上执行mvn package -pl demo-web或mvn install -pl demo-web -am让 Maven 在 reactor 中处理依赖。建议直接用方案二一次构建全部搞定。4.5 另一个经典坑install 到本地仓库后再打包有人习惯先在父项目上执行install把所有模块装进本地仓库然后再修改某个子模块的代码再单独对那个子模块执行package。这时候打包用的兄弟依赖其实是从本地仓库拿的旧版本 jar不是当前工作区的最新代码。这就是所谓的“本地仓库污染”。很多诡异的问题由此产生代码看起来改对了但运行时的表现还是老样子。解决办法在打包前先对依赖的子模块执行一次install再打包目标模块。或者干脆每次从父项目上用-pl和-am组合让所有相关模块在本次 reactor 内一并构建避免使用过期的本地仓库产物。4.6 常见问题速查表症状根因解决方式父项目执行 package子模块完全不构建父 POM 的 packaging 不是 pom或 modules 没写全检查 packaging 和 modules 配置只打包子模块提示找不到兄弟依赖兄弟依赖未安装到本地仓库用-pl xxx -am或在父项目先 installIDEA 里看不到新增子模块Maven 视图未刷新或模块未加入 modulesReload All Maven Projects改正 modules构建报错找不到某个模块目录modules 里的路径和实际目录名不一致逐一比对 modules 路径改代码后打包运行还是旧逻辑本地仓库拿到旧的兄弟依赖 jar全量 install 或用 -am 构建4.7 关于 Jenkins 和发布场景的补充如果你的项目最终通过 Jenkins 这类 CI 工具打包发布同样会遇到这个问题。建议在流水线里使用统一的构建命令例如mvn clean install -DskipTests -pl demo-web -am这样做有两个好处第一-pl明确指定要发布的目标模块-am自动把依赖模块带进 reactor不需要提前手动 install第二-DskipTests跳过测试避免测试环境不稳定导致构建中断。注意不是-Dmaven.test.skiptrue后者连测试代码都不会编译区别在特定场景下会影响构建结果。另外发布到 Docker 或服务器时除了打包 jar还要把子模块依赖的配置一并考虑进去。多模块工程中经常有某个子模块只有配置没有代码这时候一定要保证它被modules包含否则打包出的服务缺配置文件部署上去就是各种启动报错。5. 一个标准的多模块打包示例这里给一个更完整的 Spring Boot 多模块示例帮助你落地验证。假设模块结构如下demo-parent/ ├── pom.xml ├── demo-common/ │ └── pom.xml ├── demo-service/ │ └── pom.xml └── demo-web/ └── pom.xml父 POMproject xmlnshttp://maven.apache.org/POM/4.0.0 modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIddemo-parent/artifactId version1.0.0-SNAPSHOT/version packagingpom/packaging modules moduledemo-common/module moduledemo-service/module moduledemo-web/module /modules properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version2.7.18/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement /project子模块 demo-common它是被其他模块依赖的基础库不包含 Spring Boot 启动类project xmlnshttp://maven.apache.org/POM/4.0.0 modelVersion4.0.0/modelVersion parent groupIdcom.example/groupId artifactIddemo-parent/artifactId version1.0.0-SNAPSHOT/version relativePath../pom.xml/relativePath /parent artifactIddemo-common/artifactId /project子模块 demo-service依赖 demo-commonproject xmlnshttp://maven.apache.org/POM/4.0.0 modelVersion4.0.0/modelVersion parent groupIdcom.example/groupId artifactIddemo-parent/artifactId version1.0.0-SNAPSHOT/version relativePath../pom.xml/relativePath /parent artifactIddemo-service/artifactId dependencies dependency groupIdcom.example/groupId artifactIddemo-common/artifactId /dependency /dependencies /project子模块 demo-web它是最终的 Web 启动模块包含SpringBootApplication启动类project xmlnshttp://maven.apache.org/POM/4.0.0 modelVersion4.0.0/modelVersion parent groupIdcom.example/groupId artifactIddemo-parent/artifactId version1.0.0-SNAPSHOT/version relativePath../pom.xml/relativePath /parent artifactIddemo-web/artifactId dependencies dependency groupIdcom.example/groupId artifactIddemo-service/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project在 IDEA 中选中父项目 demo-parent执行mvn clean install -DskipTests你会看到控制台输出依次构建 demo-common、demo-service、demo-web。如果只想要 web 这个可执行模块的产物可以执行mvn clean package -DskipTests -pl demo-web -am构建完成后demo-web/target目录会生成可直接运行的 Spring Boot jar。我个人的建议是多模块项目本地开发时多用install而非package并且尽量在父项目层发起构建少对单个子模块直接点按钮。这样能避免兄弟模块依赖版本不同步产生的诡异问题。另外所有模块统一从父 POM 继承版本不要在子模块里硬写版本号否则升级依赖时会非常痛苦。如果按照上面的步骤配置后还是出现“父项目打包不打包子项目”那就去检查一下 Maven 的 settings.xml 里有没有配置特殊的 mirror 仓库导致父 POM 解析失败还有子模块的 pom.xml 是否被 IDEA 标记为“忽略”状态。在 IDEA 的 Maven 设置里有个 Ignored Files 列表如果某个 pom 被勾选忽略它就不会出现在 Maven 视图中构建时自然也不会参与。
返回列表