Maven 从入门到工程实践:POM、仓库、生命周期、依赖与打包
欢迎你来读这篇博客,这篇博客主要是关于 Maven。
其中包括 Maven 的基础概念、项目结构、POM、GAV 坐标、仓库配置、生命周期、常用命令、依赖范围、资源目录、测试跳过、普通 Java 项目打包、Spring Boot 项目打包,以及一些常见问题和工程实践经验。
如果把 Java 项目比作一座房子,那么 Maven 就是施工图纸、材料清单、仓库管理员和流水线工头的合体。它不写业务代码,但它决定代码如何被组织、依赖如何被拉取、项目如何被编译、测试、打包和发布。少了它,项目不是不能跑,而是容易跑得像“手搓火箭”:能飞,但每次点火都心惊肉跳。
序言
刚开始学习 Java 的时候,我们经常会把各种 jar 包手动复制到项目里。这个阶段看起来很直观:需要什么 jar,就下载什么 jar,然后放到 lib 目录下。
但项目一复杂,问题就开始来了:
- jar 包太多,不知道每个 jar 是干什么的;
- 不同 jar 之间还依赖别的 jar,手动维护非常麻烦;
- 多个 jar 版本冲突,编译时没问题,运行时突然报错;
- 每个人本地环境不同,别人能跑,我这边跑不起来;
- 打包上线时,不清楚哪些依赖需要带上,哪些不需要;
- 项目结构不统一,接手项目的人需要先猜目录含义。
Maven 要解决的就是这些工程化问题。
它的核心思想可以概括为一句话:
用统一的项目模型,管理 Java 项目的依赖、构建、测试、打包和发布。
Maven 不只是一个“下载 jar 包的工具”。它更像是一套 Java 项目的构建协议:你按照它约定的结构放代码,按照它的 POM 写配置,按照它的生命周期执行命令,它就能用相对稳定的方式把项目构建出来。
正文
chapter 1:Maven 是什么
Maven 是 Apache 旗下的项目管理和构建工具,主要服务于 Java 项目。它通过一个名为 pom.xml 的文件描述项目的信息,包括项目坐标、依赖、插件、构建方式、仓库、模块关系等。
Maven 的常见作用有:
- 管理依赖;
- 编译源代码;
- 执行单元测试;
- 打包 jar 或 war;
- 安装构件到本地仓库;
- 发布构件到远程仓库;
- 管理多模块项目;
- 通过插件扩展构建能力。
Maven 有几个非常重要的概念:
| 概念 | 说明 |
|---|---|
| POM | Project Object Model,项目对象模型,核心文件是 pom.xml |
| GAV | Maven 坐标,由 groupId、artifactId、version 组成 |
| Repository | 仓库,用于存放 jar、pom、插件等构件 |
| Lifecycle | 生命周期,描述项目构建流程 |
| Phase | 生命周期阶段,例如 compile、test、package |
| Goal | 插件目标,例如 dependency:tree、dependency:copy-dependencies |
| Plugin | 插件,真正执行编译、测试、打包等动作 |
| Scope | 依赖作用域,决定依赖在编译、测试、运行时是否生效 |
所以 Maven 的运行逻辑不是“执行一个神秘命令”,而是:
读取 POM → 解析依赖 → 确定生命周期阶段 → 调用对应插件 → 生成构建结果。
chapter 2:Maven 项目的标准目录结构
Maven 推崇“约定优于配置”。也就是说,只要你按照 Maven 默认的目录结构组织项目,很多配置就可以不用写。
一个标准 Maven 项目通常长这样:
1 | |
各目录含义如下:
| 路径 | 作用 |
|---|---|
pom.xml |
Maven 项目的核心配置文件 |
src/main/java |
主程序 Java 源码 |
src/main/resources |
主程序资源文件,例如配置文件、模板文件 |
src/test/java |
测试代码 |
src/test/resources |
测试资源文件 |
target |
Maven 构建后的输出目录 |
重点记住:
src/main/java里的代码会被编译到target/classes;src/main/resources里的资源也会被复制到target/classes;src/test/java里的代码会被编译到target/test-classes;src/test/resources里的资源会被复制到target/test-classes;target是构建产物目录,一般不提交到 Git。
这套结构的价值非常大:团队里所有 Java 项目都遵守同一种结构,新人接手项目时,不用先考古半天。
chapter 3:POM 与 GAV 坐标
pom.xml 是 Maven 项目的灵魂文件。它描述了一个项目“是谁”“依赖谁”“怎么构建”“怎么打包”。
最小可用的 pom.xml 如下:
1 | |
其中最核心的是 GAV 坐标:
1 | |
GAV 的含义如下:
| 元素 | 说明 | 示例 |
|---|---|---|
groupId |
组织或公司标识,通常是域名反写 | com.example |
artifactId |
项目或模块名称 | maven-demo |
version |
项目版本号 | 1.0.0 |
packaging |
打包类型 | jar、war、pom |
Maven 用 GAV 唯一定位一个构件。比如:
1 | |
这段配置的意思是:当前项目需要依赖 org.apache.commons:commons-lang3:3.14.0 这个构件。
注意:版本号里如果带有 SNAPSHOT,通常表示这是一个开发中的快照版本,例如:
1 | |
SNAPSHOT 版本不是稳定发布版,在团队内部开发阶段很常见,但正式发布时一般应该使用明确的 release 版本。
chapter 4:Maven 仓库机制
Maven 仓库用于存放构件。构件可以是 jar,也可以是 pom、war、maven 插件等。
Maven 仓库主要分为三类:
| 仓库类型 | 说明 |
|---|---|
| 本地仓库 | 当前机器上的缓存目录,默认在用户目录的 .m2/repository 下 |
| 中央仓库 | Maven 官方中央仓库,大量开源依赖都在这里 |
| 远程仓库 | 公司私服、第三方仓库、镜像仓库等 |
Maven 解析依赖的大致过程是:
- 先看本地仓库有没有;
- 本地没有,再去远程仓库下载;
- 下载成功后缓存到本地仓库;
- 后续构建优先使用本地缓存。
默认本地仓库路径通常是:
1 | |
如果你想修改本地仓库路径,可以在 settings.xml 中配置:
1 | |
settings.xml 通常有两个位置:
1 | |
一般更推荐修改用户目录下的 settings.xml,因为它只影响当前用户,不会污染 Maven 安装目录。
chapter 5:配置 Maven 镜像与阿里云仓库
国内使用 Maven 时,经常会遇到依赖下载慢的问题。这时候可以配置镜像仓库。
比较推荐的方式是在 settings.xml 中配置 mirror,而不是在每个项目的 pom.xml 里都写一遍仓库地址。
示例:
1 | |
如果你希望所有远程仓库请求都经过某个公司私服或统一代理,可以使用:
1 | |
但这里要注意:
mirrorOf="central":只镜像 Maven Central;mirrorOf="*":镜像所有仓库请求;- 公司内部项目更推荐搭建 Nexus、Artifactory 这类私服,统一代理外部依赖,并管理内部 jar;
- 不建议在大量业务项目的
pom.xml里硬编码公共镜像地址,否则以后换仓库会很麻烦。
项目级别的 repositories 更适合配置项目确实需要的特殊仓库,例如公司私服:
1 | |
如果仓库需要账号密码,不要把账号密码写进 pom.xml。应该在 settings.xml 的 servers 中配置:
1 | |
这里的 id 要和 repository 里的 id 对应。
chapter 6:Maven 生命周期、阶段、插件的关系
Maven 最容易混淆的地方就是生命周期、阶段、插件、目标。
先说结论:
生命周期定义流程,阶段定义步骤,插件真正干活,目标是插件里的具体动作。
Maven 内置三套生命周期:
| 生命周期 | 作用 |
|---|---|
clean |
清理构建产物 |
default |
编译、测试、打包、安装、部署 |
site |
生成项目站点文档 |
最常用的是 default 生命周期,它包含很多阶段,常见阶段如下:
1 | |
这些阶段是有顺序的。执行后面的阶段,会自动执行前面的阶段。
例如:
1 | |
并不是只执行 package,它会先执行:
1 | |
再比如:
1 | |
会执行:
1 | |
这就是为什么执行 mvn install 时,测试也会被跑起来。不是 Maven 多管闲事,而是生命周期就是这么安排的。
常见命令如下:
| 命令 | 作用 |
|---|---|
mvn clean |
删除 target 目录 |
mvn compile |
编译主程序 |
mvn test-compile |
编译测试代码 |
mvn test |
执行单元测试 |
mvn package |
打包项目 |
mvn install |
打包并安装到本地仓库 |
mvn deploy |
发布到远程仓库 |
mvn dependency:tree |
查看依赖树 |
mvn help:effective-pom |
查看最终生效的 POM |
mvn -o package |
离线构建 |
其中 dependency:tree 这种命令不是生命周期阶段,而是插件目标。它的格式一般是:
1 | |
例如:
1 | |
chapter 7:依赖范围 scope 详解
Maven 的依赖不只是“引入或不引入”这么简单。每个依赖还可以配置作用域,也就是 scope。
常见 scope 如下:
| scope | 编译可用 | 测试可用 | 运行可用 | 是否传递 | 常见场景 |
|---|---|---|---|---|---|
compile |
是 | 是 | 是 | 是 | 默认值,大多数普通依赖 |
provided |
是 | 是 | 否 | 否 | Servlet API、容器提供的依赖 |
runtime |
否 | 是 | 是 | 是 | JDBC 驱动、运行时才需要的依赖 |
test |
否 | 是 | 否 | 否 | JUnit、Mockito 等测试依赖 |
system |
是 | 是 | 是 | 否 | 本地 jar,强烈不推荐 |
import |
不直接参与 | 不直接参与 | 不直接参与 | 不直接参与 | BOM 导入,只能用于 dependencyManagement |
示例:测试依赖使用 test:
1 | |
示例:Servlet API 使用 provided:
1 | |
为什么 Servlet API 通常是 provided?
因为运行时 Tomcat、Jetty 这类容器会提供这部分类。如果你自己再打进去,可能会造成类冲突。
再比如 JDBC 驱动可以配置为 runtime:
1 | |
system scope 虽然能引用本地 jar,但不推荐使用:
1 | |
原因很简单:
- 本地路径不可移植;
- 别人拉代码后可能没有这个 jar;
- CI/CD 环境可能构建失败;
- Maven 无法像普通依赖一样管理它。
更推荐的做法是:
- 把本地 jar 安装到本地仓库;
- 或上传到公司 Maven 私服;
- 或把它改造成一个标准 Maven 模块。
安装本地 jar 的命令示例:
1 | |
chapter 8:properties、dependencyManagement 与 BOM
当项目依赖很多时,如果每个依赖都手写版本号,后期维护会很痛苦。
比如 Spring 相关依赖都要保持同一个版本:
1 | |
这样做的好处是:以后升级 Spring 版本时,只需要改一个地方。
但在大型项目里,仅靠 properties 还不够。更常见的做法是使用 dependencyManagement 统一管理版本:
1 | |
子模块真正使用依赖时,就可以不写版本:
1 | |
注意:dependencyManagement 只是管理版本,不会自动引入依赖。真正引入依赖,还是要写到 dependencies 里。
Spring Boot 项目经常使用 BOM 管理依赖版本:
1 | |
或者直接继承 Spring Boot Parent:
1 | |
这就是为什么 Spring Boot 项目里很多依赖不写版本也能正常工作。不是版本消失了,而是被父 POM 或 BOM 管理起来了。
chapter 9:资源目录与编码配置
Maven 默认会把 src/main/resources 下的文件复制到 target/classes。
例如:
1 | |
构建后会变成:
1 | |
如果需要自定义资源目录,可以配置 resources:
1 | |
如果开启 filtering:
1 | |
Maven 会替换资源文件中的占位符,例如:
1 | |
构建后会被替换成当前项目的 artifactId 和 version。
但要小心:不是所有资源都适合 filtering。比如图片、字体、证书、二进制文件,如果被 Maven 当文本处理,可能会损坏。
中文乱码问题也很常见。建议在 POM 中统一配置编码:
1 | |
如果是普通 Java 项目,还可以配置编译插件:
1 | |
如果你还在使用 JDK 8,可以用:
1 | |
JDK 9 之后更推荐使用 release,因为它不仅控制语法级别,还能限制可用 API,避免“本地能编译,低版本 JDK 运行报错”的尴尬局面。
chapter 10:测试与跳过测试
执行:
1 | |
Maven 会编译并运行测试代码。默认情况下,测试通常由 maven-surefire-plugin 执行。
打包时如果想跳过测试,常见有两种方式。
第一种:
1 | |
或者:
1 | |
它的特点是:
- 不运行测试;
- 但会编译测试代码。
第二种:
1 | |
它的特点是:
- 不运行测试;
- 也不编译测试代码。
区别非常重要:
| 参数 | 是否编译测试代码 | 是否执行测试 |
|---|---|---|
-DskipTests |
是 | 否 |
-Dmaven.test.skip=true |
否 | 否 |
工程建议:
- 本地临时打包可以使用
-DskipTests; - 如果测试代码本身编译不过,而你只是想快速打包,可以使用
-Dmaven.test.skip=true; - CI/CD 主流水线不建议长期跳过测试;
- 如果测试太慢,应该区分单元测试和集成测试,而不是一刀切跳过;
- 如果某些测试不稳定,应该修复测试,而不是让跳过测试成为团队传统手艺。
也可以在 POM 中配置默认跳过测试:
1 | |
如果某次想重新启用测试:
1 | |
chapter 11:普通 Java 项目如何打包
普通 Java 项目打包时,经常遇到一个问题:
1 | |
原因通常是:你的 jar 包里没有把第三方依赖一起带上,或者 MANIFEST.MF 中没有正确声明 classpath。
普通 Java 项目一般有三种打包方式。
方式一:主 jar 与依赖 libs 分离
这种方式会生成:
1 | |
配置示例:
1 | |
执行:
1 | |
优点:
- 依赖清晰;
- 可以单独替换某些 jar;
- 适合传统部署。
缺点:
- 部署时必须保证
app.jar和libs目录一起上传; - 少一个依赖就可能运行失败。
方式二:打成 fat jar
fat jar 就是把项目代码和依赖都打进一个大 jar。
可以使用 maven-shade-plugin:
1 | |
优点:
- 一个 jar 就能运行;
- 部署简单。
缺点:
- jar 体积大;
- 依赖冲突时排查更麻烦;
- 可能需要处理资源文件合并问题。
方式三:Spring Boot 项目使用 spring-boot-maven-plugin
如果是 Spring Boot 项目,通常不需要自己手动配置 jar plugin 和 dependency plugin。
使用:
1 | |
执行:
1 | |
Spring Boot Maven 插件会帮你完成 repackage,生成可执行 jar。
这也是 Spring Boot 项目推荐的方式:不要硬凹 Maven 原生 jar 配置,能交给 Boot 插件就交给 Boot 插件。专业的事交给专业的插件,程序员少熬一晚是一晚。
chapter 12:多模块项目中的 Maven 管理
当项目变大后,通常会拆成多模块:
1 | |
父工程的 pom.xml 通常使用 pom 打包:
1 | |
父工程声明模块:
1 | |
子模块继承父工程:
1 | |
父 POM 常用来做几件事:
- 统一 Java 版本;
- 统一依赖版本;
- 统一插件版本;
- 统一编码;
- 统一仓库;
- 统一打包规则;
- 管理模块之间的依赖关系。
推荐结构:
1 | |
注意:
dependencyManagement不会自动引入依赖;pluginManagement不会自动启用插件;- 子模块需要使用时,仍然要在自己的
dependencies或plugins中声明; - 父工程版本要稳定,否则子模块发布会混乱;
- 多模块项目中,模块之间依赖要避免循环引用。
chapter 13:常见 Maven 问题排查
1. mvn 命令无法识别
现象:
1 | |
或:
1 | |
排查:
- Maven 是否已下载并解压;
- 是否配置
MAVEN_HOME; - 是否把
%MAVEN_HOME%\bin或$MAVEN_HOME/bin加入 PATH; - 是否重新打开终端;
- 执行
mvn -v验证。
2. 依赖下载失败
常见原因:
- 网络问题;
- 镜像配置错误;
- 仓库地址不可用;
- 公司代理限制;
- 依赖版本不存在;
- 本地仓库缓存了失败文件。
常用命令:
1 | |
-U 表示强制更新 snapshot 和 release 依赖。
也可以删除本地仓库中对应依赖目录后重新下载。
3. 依赖冲突
现象:
1 | |
很可能是依赖版本冲突。
查看依赖树:
1 | |
只看某个依赖:
1 | |
排除传递依赖:
1 | |
工程建议:
- 直接使用的依赖,最好显式声明;
- 不要完全依赖传递依赖;
- 日志相关依赖尤其要小心;
- Spring Boot 项目尽量尊重 Boot 的依赖管理,不要随便覆盖版本。
4. 打包成功但运行失败
如果执行:
1 | |
出现:
1 | |
说明 jar 里没有配置主类。
如果出现:
1 | |
说明运行时找不到依赖。
解决思路:
- 普通 Java 项目配置
maven-jar-plugin的mainClass; - 使用
maven-dependency-plugin复制依赖; - 或使用
maven-shade-plugin打 fat jar; - Spring Boot 项目使用
spring-boot-maven-plugin。
5. JDK 版本不匹配
比如本地 JDK 17,项目要求 JDK 8,或者反过来,都可能出问题。
建议明确配置:
1 | |
或者:
1 | |
不要让 Maven 猜你的 JDK 版本。它猜错的时候,背锅的一般是你。
chapter 14:我对 Maven 的工程实践建议
1. 不要在业务项目里到处写仓库地址
能放 settings.xml 的镜像配置,就不要散落在每个项目的 pom.xml 中。
项目 POM 应该描述项目本身,不应该承担过多个人环境配置。
2. 不要滥用 system scope
systemPath 看起来方便,但对团队协作和 CI/CD 非常不友好。
更好的方式是:
- 安装到本地仓库;
- 上传公司私服;
- 改造成标准 Maven 模块;
- 实在不行,也要写清楚依赖来源和导入方式。
3. Spring Boot 项目优先使用官方依赖管理
Spring Boot 已经帮你维护了一整套依赖版本组合。除非你明确知道为什么要覆盖,否则不要随便指定 starter 相关依赖版本。
4. 使用 dependency:tree 排查冲突
遇到依赖冲突,不要靠猜。
先执行:
1 | |
再判断到底是谁引入了冲突版本。
“凭感觉改依赖”就像闭眼拆炸弹,刺激,但不推荐。
5. CI/CD 中不要长期跳过测试
本地为了快速打包,可以临时跳过测试。
但如果主干流水线长期跳过测试,项目质量迟早要还债,而且通常是上线当天连本带利一起还。
6. 多模块项目要收敛版本
父 POM 应该统一管理:
- Java 版本;
- 编码;
- 依赖版本;
- 插件版本;
- 仓库策略;
- 发布规则。
否则模块一多,版本就会变成“野生动物园”。
7. 构建配置要能被新人读懂
好的 pom.xml 不只是能跑,还应该能读。
建议保留必要注释,尤其是:
- 为什么要排除某个依赖;
- 为什么要覆盖某个版本;
- 为什么某个依赖使用
provided; - 为什么某个插件绑定到某个 phase;
- 为什么某个仓库必须存在。
这些注释不是写给 Maven 看的,是写给三个月后的自己看的。三个月后的自己通常什么都不记得,但很会骂人。
参考资料
- Apache Maven 官方文档:POM Reference
- Apache Maven 官方文档:Introduction to the Build Lifecycle
- Apache Maven 官方文档:Introduction to the Dependency Mechanism
- Apache Maven 官方文档:Settings Reference
- Apache Maven 官方文档:Introduction to Repositories
- Apache Maven 官方文档:Using Mirrors for Repositories
- Apache Maven Surefire Plugin:Skipping Tests
- Apache Maven Dependency Plugin:copy-dependencies
- NorthCastle:Maven 基础与进阶系列
- Maven 打包跳过测试的多种方式相关文章
- Maven POM、GAV、scope、生命周期、打包、资源目录、阿里云仓库配置等相关文章
启示录
Maven 的价值不只是“帮我们下载 jar 包”,而是把 Java 项目的构建过程标准化、可复制化、可维护化。
小项目里,Maven 让你少复制几个 jar。
大项目里,Maven 决定你的构建体系会不会失控。
真正掌握 Maven,不是背几个命令,而是理解:
- POM 描述项目;
- GAV 定位构件;
- 仓库保存构件;
- scope 控制依赖边界;
- 生命周期定义构建流程;
- 插件执行具体动作;
- 父 POM 和 BOM 统一工程规则。
技术越往后走,越会发现:能把项目跑起来只是第一步,能让项目稳定、清晰、可协作、可交付,才是工程能力。
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。