Maven 多模块项目如何管理?聚合和继承该怎么取舍?
Maven 多模块项目通常要同时解决两件事:一次构建多个模块,以及让多个模块共享同一套版本、插件和属性配置。聚合解决的是“构建顺序和构建入口”,继承解决的是“公共配置从哪里来”。两者经常放在同一个父 POM 里,但它们不是一回事,也不是必须绑定使用。
一个典型结构会把根目录作为 parent,打包类型设为 pom,再通过 <modules> 声明子模块。执行根目录的 mvn clean install 时,Maven 会读取各模块之间的依赖关系,按 reactor 顺序编译,而不是简单按目录顺序跑。
xml<project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>demo-parent</artifactId> <version>1.0.0</version> <packaging>pom</packaging> <modules> <module>demo-api</module> <module>demo-service</module> <module>demo-web</module> </modules> </project>
继承则写在子模块的 pom.xml 中。子模块通过 <parent> 继承父 POM 里的 properties、dependencyManagement、pluginManagement 等配置,但是否被父项目聚合构建,要看父 POM 有没有把它放进 <modules>。
xml<parent> <groupId>com.example</groupId> <artifactId>demo-parent</artifactId> <version>1.0.0</version> <relativePath>../pom.xml</relativePath> </parent> <artifactId>demo-service</artifactId>
实际管理思路
在真实项目里,根 POM 最好只承担“统一入口”和“统一约束”两类职责。版本号、编码、Java release、插件版本适合放在父 POM,因为这些配置越分散,越容易出现本地和 CI 不一致。业务模块自己的依赖、打包方式和资源配置,应该留在模块内部,否则父 POM 会慢慢变成谁也不敢改的公共垃圾箱。
聚合模块的边界也要控制清楚。一个仓库里可以有多个聚合入口,例如 pom.xml 构建全量模块,pom-service.xml 只聚合后端服务模块,但不要让开发者必须记住十几个入口。模块之间依赖要保持单向,常见方向是 web 依赖 service,service 依赖 api 或 domain,底层模块不要反过来引用上层模块。
版本管理上,团队通常会把第三方依赖放进父 POM 的 dependencyManagement,把内部模块版本统一成 ${project.version}。这样发布时能保证同一批模块版本一致,也能减少“api 是 1.0.1、service 还是 1.0.0”的联调问题。边界是公共 parent 如果被很多仓库继承,升级就要更谨慎,最好配合 release notes 和兼容性测试。
另外,父 POM 不一定要发布业务代码,但它本身也要有清晰版本。团队里如果把 parent 当成随手可改的配置文件,子模块升级时就会出现不可追踪的差异。更稳的做法是把公共约束当成产品维护,每次改动都说明影响范围。
追问
聚合和继承最大的区别是什么?
聚合关心“从哪个入口构建哪些模块”,继承关心“子模块复用哪些配置”。一个模块可以继承某个公司级 parent,但不出现在当前仓库的 <modules> 中,这在公共 parent 单独发布时很常见。反过来,一个聚合 POM 也可以只负责组装构建,不给子模块继承配置。踩坑点是把两者混为一谈,结果改了父 POM 版本却发现某些模块根本没有继承到。
父 POM 里应该放 dependencies 还是 dependencyManagement?
公共依赖确实可以放进 <dependencies>,但这样所有子模块都会自动引入,容易让模块边界变脏。更稳妥的做法是把版本放在 <dependencyManagement>,子模块需要时再显式声明依赖。取舍标准很简单:所有模块都必需的基础依赖可以直接放,业务库、驱动、Web 框架这类只给部分模块用的依赖应只管理版本。这样排查冲突时也能看清依赖到底是谁主动引入的。
多模块构建时只想编译一个模块怎么办?
可以用 -pl 指定模块,用 -am 顺带构建它依赖的本仓库模块。例如 mvn clean install -pl demo-web -am 会构建 demo-web 以及它依赖的 demo-api、demo-service。如果还想构建依赖它的模块,可以用 -amd,但在大仓库里会拉起很多模块,耗时可能反而更长。常见踩坑是只跑 -pl demo-web,本地仓库里的上游模块还是旧版本,导致问题看起来像代码没生效。
bashmvn clean install -pl demo-web -am mvn test -pl demo-service mvn clean install -pl '!demo-web'
模块应该按业务拆还是按技术层拆?
小项目按技术层拆成 api、service、web 很直观,团队沟通成本低。业务复杂后,更推荐按领域或边界上下文拆模块,否则所有业务都挤在一个 service 模块里,最后还是一个大泥球。取舍点在于依赖方向是否稳定:底层公共模块不应该反向依赖上层业务模块。模块拆得太细也会让构建、发布和版本管理变复杂,所以不要为了“看起来架构好”硬拆。
多模块项目最容易遇到什么坑?
第一个坑是循环依赖,例如 demo-api 依赖 demo-service,demo-service 又依赖 demo-api,Maven reactor 会直接报错。第二个坑是父 POM 的 <relativePath> 写错,IDE 里可能还能识别,命令行构建却解析到仓库里的旧 parent。第三个坑是插件版本没有放进 <pluginManagement>,不同模块各跑各的插件版本,CI 上才暴露差异。处理这类问题时,先用 mvn -q help:effective-pom 和 mvn dependency:tree 看最终配置,比盯着源码猜更可靠。