Maven 是 Java 生态中最主流的构建与依赖管理工具。它以 POM(Project Object Model)为核心,通过约定优于配置的理念,将编译、测试、打包、部署等流程标准化,同时依托中央仓库解决了依赖分发的难题。本文系统梳理 Maven 的核心概念与日常使用要点。

Maven 解决什么问题

早期 Java 项目依赖管理依赖手工拷贝 jar 包,存在三大痛点:

  • 依赖混乱:第三方库及其传递依赖难以追踪,版本冲突频发
  • 构建各异:每个项目的编译打包流程不一致,交接成本高
  • 分发困难:构件(artifact)没有统一的存储与获取机制

Maven 用统一坐标(GAV)定位构件、用标准生命周期固化构建流程、用仓库体系分发构件,一次性解决上述问题。

安装与配置

安装

Maven 官网 下载二进制包解压,配置环境变量:

1
2
3
4
export MAVEN_HOME=/opt/maven
export PATH=$MAVEN_HOME/bin:$PATH

mvn -v # 验证安装

settings.xml 关键配置

全局配置文件位于 $MAVEN_HOME/conf/settings.xml,用户级配置位于 ~/.m2/settings.xml(优先级更高)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<settings>
<!-- 本地仓库路径 -->
<localRepository>/data/maven-repo</localRepository>

<!-- 阿里云镜像,加速中央仓库访问 -->
<mirrors>
<mirror>
<id>aliyun</id>
<name>Aliyun Maven</name>
<url>https://maven.aliyun.com/repository/public</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>

<!-- 私服认证 -->
<servers>
<server>
<id>company-releases</id>
<username>deploy</username>
<password>secret</password>
</server>
</servers>
</settings>

Maven Wrapper(mvnw)

克隆开源项目时常在根目录看到 mvnwmvnw.cmd 两个脚本,这就是 Maven Wrapper。它解决的问题是:本机无需预先安装 Maven,且所有开发者、CI 使用完全一致的 Maven 版本,避免「我本地能构建,你那边报错」的环境差异。

工作原理

Wrapper 由三部分组成,全部提交进版本库:

1
2
mvnw/mvnw.cmd                         # 启动脚本(*nix / Windows)
.mvn/wrapper/maven-wrapper.properties # 版本与下载地址配置

执行 ./mvnw 时,脚本检查本地是否已有指定版本的 Maven,没有则自动下载到 ~/.m2/wrapper/dists/ 并缓存,随后转发所有参数给真正的 Maven。mvnwmvn 的命令行用法完全一致:

1
2
./mvnw clean install      # 等价于 mvn clean install
./mvnw -v # 查看 wrapper 锁定的 Maven 版本

生成与升级

在已有项目中生成 wrapper:

1
2
mvn wrapper:wrapper                        # 使用默认版本
mvn wrapper:wrapper -Dmaven=3.9.9 # 指定版本

生成的 maven-wrapper.properties 核心内容:

1
2
3
4
5
# 实际为一行,此处用 \ 折行仅为展示
distributionUrl=\
https://repo.maven.apache.org/maven2/\
org/apache/maven/apache-maven/3.9.9/\
apache-maven-3.9.9-bin.zip

升级 Maven 版本时重新执行 wrapper:wrapper 并提交变更即可,团队成员拉取代码后自动切换到新版本。

使用建议

  1. 新项目一律生成 wrapper 并提交,README 中的构建命令统一写 ./mvnw
  2. CI 流水线用 ./mvnw 而非 mvn,构建机无需维护 Maven 安装
  3. 不要把 .mvn/wrapper 加进 .gitignoremaven-wrapper.jar(若存在)是引导程序,也需要提交

POM 与坐标

GAV 坐标

每个构件由三段坐标唯一标识:

元素含义示例
groupId组织或项目归属,通常是反向域名org.springframework.boot
artifactId模块名spring-boot-starter-web
version版本号3.4.1

加上可选的 packaging(打包方式,默认 jar)与 classifier(附属分类,如 sources),构成完整坐标。

最小 POM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demo-app</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>
UTF-8
</project.build.sourceEncoding>
</properties>
</project>

modelVersion 固定为 4.0.0;properties 中集中定义版本号与编译参数,便于统一管理。

SNAPSHOT 版本

-SNAPSHOT 后缀表示开发中的不稳定版本,每次部署都会更新时间戳,远程仓库允许覆盖。发布版本(release)则不可变。CI 日常构建用 SNAPSHOT,正式发版时去掉后缀。

依赖管理

依赖范围 scope

scope编译期测试期运行期典型场景
compile(默认)常规业务依赖
providedServlet API,容器提供
runtimeJDBC 驱动
testJUnit、Mockito
system本地 jar,已废弃,避免使用

依赖传递与调解

Maven 自动解析传递依赖,遵循两条调解规则:

  1. 路径最短优先:A→B→C→D:2.0 与 A→E→D:1.0 中,D 取 1.0
  2. 先声明优先:路径等长时,POM 中先声明的一方胜出

排查依赖冲突:

1
2
mvn dependency:tree
mvn dependency:tree -Dverbose -Dincludes=commons-lang3

-Dverbose 显示被调解省略的冲突版本,是定位 NoSuchMethodErrorClassNotFoundException 等运行时异常的第一手段。

排除依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.example</groupId>
<artifactId>legacy-lib</artifactId>
<version>2.1.0</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

dependencyManagement 统一版本

在父 POM 中锁定版本,子模块声明依赖时省略 version:

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>4.1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

dependencyManagement 只声明版本不引入依赖;import scope 可导入 BOM(Bill of Materials),一次性继承整套版本矩阵,Spring Boot 项目均基于此模式。

生命周期与插件

三套生命周期

1
2
3
4
5
6
clean:    pre-clean → clean → post-clean

default: validate → compile → test → package
→ verify → install → deploy

site: pre-site → site → post-site → site-deploy

生命周期阶段(phase)顺序执行,执行某个阶段会自动执行其之前的所有阶段。阶段本身不做任何事,实际工作由绑定到阶段的插件目标(goal)完成。

常用命令

1
2
3
4
5
6
7
8
mvn clean package           # 清理并打包
mvn clean install # 打包并安装到本地仓库
mvn deploy # 发布到远程仓库
mvn test # 运行测试
mvn package -DskipTests # 跳过测试(不编译不执行)
mvn package -Dmaven.test.skip=true # 连测试类都不编译
mvn compile -o # 离线模式
mvn help:effective-pom # 查看合并继承后的完整 POM

插件配置

maven-compiler-plugin 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>21</release>
<parameters>true</parameters>
</configuration>
</plugin>
</plugins>
</build>

<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
2
3
4
5
6
7
8
9
10
11
12
<project>
<groupId>com.example</groupId>
<artifactId>shop</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>

<modules>
<module>shop-common</module>
<module>shop-service</module>
<module>shop-web</module>
</modules>
</project>

根 POM 的 packaging 必须为 pom。子模块通过 <parent> 继承根 POM,自动获得统一版本号与 dependencyManagement 约束。

模块构建

1
2
3
mvn clean install                 # 构建全部模块
mvn -pl shop-web -am install # 只构建 shop-web 及其依赖模块
mvn -pl shop-service install # 只构建单个模块(不含依赖)
  • -pl(–projects):指定模块
  • -am(–also-make):连同上游依赖模块一起构建
  • Maven 依据模块间依赖自动拓扑排序构建顺序

依赖收敛

多模块下用 enforcer 插件防止版本漂移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>enforce</id>
<goals><goal>enforce</goal></goals>
<configuration>
<rules>
<dependencyConvergence/>
<requireJavaVersion>
<version>[21,)</version>
</requireJavaVersion>
</rules>
</configuration>
</execution>
</executions>
</plugin>

Profile 环境切换

针对不同环境(dev/test/prod)激活不同配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<profiles>
<profile>
<id>dev</id>
<activation><activeByDefault>true</activeByDefault></activation>
<properties>
<env>dev</env>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<env>prod</env>
</properties>
<build>
<plugins>
<!-- prod 环境专属插件配置 -->
</plugins>
</build>
</profile>
</profiles>

激活方式:

1
2
mvn package -P prod        # 命令行激活
mvn help:active-profiles # 查看当前生效的 profile

配合 maven-resources-plugin 的资源过滤(filtering),可在打包时将 ${env} 占位符替换进配置文件,实现一套代码多环境构建。

部署到远程仓库

distributionManagement 指定发布目标:

1
2
3
4
5
6
7
8
9
10
<distributionManagement>
<repository>
<id>company-releases</id>
<url>https://nexus.example.com/repository/releases/</url>
</repository>
<snapshotRepository>
<id>company-snapshots</id>
<url>https://nexus.example.com/repository/snapshots/</url>
</snapshotRepository>
</distributionManagement>

<id> 必须与 settings.xml<server> 的 id 对应,Maven 据此查找认证凭据。执行 mvn deploy 后,release 与 snapshot 构件自动按版本后缀路由到对应仓库。

最佳实践

  1. 版本号集中在 <properties>:避免散落各处的硬编码版本
  2. 父 POM 用 dependencyManagement 锁定版本,子模块不写 version
  3. 依赖冲突先跑 dependency:tree -Dverbose,用 exclusion 排除或在父 POM 强制版本
  4. 多模块项目用 -pl + -am 增量构建,避免全量编译
  5. 禁止提交 SNAPSHOT 依赖的 release 版本,用 enforcer 插件卡点
  6. 项目提交 Maven Wrapper,团队成员与 CI 统一用 ./mvnw 构建
  7. CI 环境使用 -B(批处理模式)与 -ntp(关闭传输进度),保持日志干净
1
./mvnw -B -ntp clean install

Maven 与 Gradle 对比

维度MavenGradle
配置语言XML,声明式Groovy/Kotlin DSL,可编程
构建性能一般增量构建、构建缓存,更快
学习曲线平缓,约定清晰陡峭,灵活性带来复杂度
生态成熟度极成熟,企业标配增长快,Android 官方工具
多模块支持完善完善且更灵活

存量企业项目以 Maven 为主;新建项目若对构建速度敏感(大型多模块),Gradle 值得评估。

总结

Maven 的价值不在于功能强大,而在于标准化:统一的坐标、生命周期与目录结构让任何 Java 工程师都能零成本接手陌生项目。掌握 GAV 坐标、依赖调解规则、生命周期与插件绑定、多模块聚合这四块核心内容,即可应对日常绝大多数构建问题。遇到疑难杂症时,dependency:tree -Dverbosehelp:effective-pom 是两件最锋利的排查武器。