Maven 使用详解:构建、依赖与多模块管理
Maven 是 Java 生态中最主流的构建与依赖管理工具。它以 POM(Project Object Model)为核心,通过约定优于配置的理念,将编译、测试、打包、部署等流程标准化,同时依托中央仓库解决了依赖分发的难题。本文系统梳理 Maven 的核心概念与日常使用要点。
Maven 解决什么问题
早期 Java 项目依赖管理依赖手工拷贝 jar 包,存在三大痛点:
- 依赖混乱:第三方库及其传递依赖难以追踪,版本冲突频发
- 构建各异:每个项目的编译打包流程不一致,交接成本高
- 分发困难:构件(artifact)没有统一的存储与获取机制
Maven 用统一坐标(GAV)定位构件、用标准生命周期固化构建流程、用仓库体系分发构件,一次性解决上述问题。
安装与配置
安装
从 Maven 官网 下载二进制包解压,配置环境变量:
1 | |
settings.xml 关键配置
全局配置文件位于 $MAVEN_HOME/conf/settings.xml,用户级配置位于 ~/.m2/settings.xml(优先级更高)。
1 | |
Maven Wrapper(mvnw)
克隆开源项目时常在根目录看到 mvnw、mvnw.cmd 两个脚本,这就是 Maven Wrapper。它解决的问题是:本机无需预先安装 Maven,且所有开发者、CI 使用完全一致的 Maven 版本,避免「我本地能构建,你那边报错」的环境差异。
工作原理
Wrapper 由三部分组成,全部提交进版本库:
1 | |
执行 ./mvnw 时,脚本检查本地是否已有指定版本的 Maven,没有则自动下载到 ~/.m2/wrapper/dists/ 并缓存,随后转发所有参数给真正的 Maven。mvnw 与 mvn 的命令行用法完全一致:
1 | |
生成与升级
在已有项目中生成 wrapper:
1 | |
生成的 maven-wrapper.properties 核心内容:
1 | |
升级 Maven 版本时重新执行 wrapper:wrapper 并提交变更即可,团队成员拉取代码后自动切换到新版本。
使用建议
- 新项目一律生成 wrapper 并提交,README 中的构建命令统一写
./mvnw - CI 流水线用
./mvnw而非mvn,构建机无需维护 Maven 安装 - 不要把
.mvn/wrapper加进.gitignore;maven-wrapper.jar(若存在)是引导程序,也需要提交
POM 与坐标
GAV 坐标
每个构件由三段坐标唯一标识:
| 元素 | 含义 | 示例 |
|---|---|---|
groupId | 组织或项目归属,通常是反向域名 | org.springframework.boot |
artifactId | 模块名 | spring-boot-starter-web |
version | 版本号 | 3.4.1 |
加上可选的 packaging(打包方式,默认 jar)与 classifier(附属分类,如 sources),构成完整坐标。
最小 POM
1 | |
modelVersion 固定为 4.0.0;properties 中集中定义版本号与编译参数,便于统一管理。
SNAPSHOT 版本
-SNAPSHOT 后缀表示开发中的不稳定版本,每次部署都会更新时间戳,远程仓库允许覆盖。发布版本(release)则不可变。CI 日常构建用 SNAPSHOT,正式发版时去掉后缀。
依赖管理
依赖范围 scope
| scope | 编译期 | 测试期 | 运行期 | 典型场景 |
|---|---|---|---|---|
compile(默认) | ✓ | ✓ | ✓ | 常规业务依赖 |
provided | ✓ | ✓ | ✗ | Servlet API,容器提供 |
runtime | ✗ | ✓ | ✓ | JDBC 驱动 |
test | ✗ | ✓ | ✗ | JUnit、Mockito |
system | ✓ | ✓ | ✗ | 本地 jar,已废弃,避免使用 |
依赖传递与调解
Maven 自动解析传递依赖,遵循两条调解规则:
- 路径最短优先:A→B→C→D:2.0 与 A→E→D:1.0 中,D 取 1.0
- 先声明优先:路径等长时,POM 中先声明的一方胜出
排查依赖冲突:
1 | |
-Dverbose 显示被调解省略的冲突版本,是定位 NoSuchMethodError、ClassNotFoundException 等运行时异常的第一手段。
排除依赖
1 | |
dependencyManagement 统一版本
在父 POM 中锁定版本,子模块声明依赖时省略 version:
1 | |
dependencyManagement 只声明版本不引入依赖;import scope 可导入 BOM(Bill of Materials),一次性继承整套版本矩阵,Spring Boot 项目均基于此模式。
生命周期与插件
三套生命周期
1 | |
生命周期阶段(phase)顺序执行,执行某个阶段会自动执行其之前的所有阶段。阶段本身不做任何事,实际工作由绑定到阶段的插件目标(goal)完成。
常用命令
1 | |
插件配置
以 maven-compiler-plugin 为例:
1 | |
<parameters>true</parameters> 保留方法参数名,Spring MVC 参数绑定、MyBatis 多参数映射依赖此信息。
常用插件速查:
| 插件 | 用途 |
|---|---|
| maven-compiler-plugin | 源码编译 |
| maven-surefire-plugin | 单元测试执行 |
| maven-jar-plugin | 打 jar 包,配置 Manifest |
| maven-shade-plugin | 打可执行 fat jar,处理类冲突 |
| maven-enforcer-plugin | 强制约束(JDK 版本、依赖收敛) |
| versions-maven-plugin | 批量升级版本号 |
多模块项目
聚合与继承
大型项目拆分为多个模块,根 POM 同时承担聚合(aggregation)与父继承(parent)两个角色:
1 | |
根 POM 的 packaging 必须为 pom。子模块通过 <parent> 继承根 POM,自动获得统一版本号与 dependencyManagement 约束。
模块构建
1 | |
-pl(–projects):指定模块-am(–also-make):连同上游依赖模块一起构建- Maven 依据模块间依赖自动拓扑排序构建顺序
依赖收敛
多模块下用 enforcer 插件防止版本漂移:
1 | |
Profile 环境切换
针对不同环境(dev/test/prod)激活不同配置:
1 | |
激活方式:
1 | |
配合 maven-resources-plugin 的资源过滤(filtering),可在打包时将 ${env} 占位符替换进配置文件,实现一套代码多环境构建。
部署到远程仓库
distributionManagement 指定发布目标:
1 | |
<id> 必须与 settings.xml 中 <server> 的 id 对应,Maven 据此查找认证凭据。执行 mvn deploy 后,release 与 snapshot 构件自动按版本后缀路由到对应仓库。
最佳实践
- 版本号集中在
<properties>:避免散落各处的硬编码版本 - 父 POM 用
dependencyManagement锁定版本,子模块不写 version - 依赖冲突先跑
dependency:tree -Dverbose,用 exclusion 排除或在父 POM 强制版本 - 多模块项目用
-pl+-am增量构建,避免全量编译 - 禁止提交 SNAPSHOT 依赖的 release 版本,用 enforcer 插件卡点
- 项目提交 Maven Wrapper,团队成员与 CI 统一用
./mvnw构建 - CI 环境使用
-B(批处理模式)与-ntp(关闭传输进度),保持日志干净
1 | |
Maven 与 Gradle 对比
| 维度 | Maven | Gradle |
|---|---|---|
| 配置语言 | XML,声明式 | Groovy/Kotlin DSL,可编程 |
| 构建性能 | 一般 | 增量构建、构建缓存,更快 |
| 学习曲线 | 平缓,约定清晰 | 陡峭,灵活性带来复杂度 |
| 生态成熟度 | 极成熟,企业标配 | 增长快,Android 官方工具 |
| 多模块支持 | 完善 | 完善且更灵活 |
存量企业项目以 Maven 为主;新建项目若对构建速度敏感(大型多模块),Gradle 值得评估。
总结
Maven 的价值不在于功能强大,而在于标准化:统一的坐标、生命周期与目录结构让任何 Java 工程师都能零成本接手陌生项目。掌握 GAV 坐标、依赖调解规则、生命周期与插件绑定、多模块聚合这四块核心内容,即可应对日常绝大多数构建问题。遇到疑难杂症时,dependency:tree -Dverbose 与 help:effective-pom 是两件最锋利的排查武器。






