Maven 教程

Maven 是 Java 世界的项目构建与依赖管理工具。写单个文件时 javac 一个、java 跑一个还够用;一旦项目用上第三方 jar、要打包分发,手工拼 classpath 就撑不住了。Maven 把「依赖从哪来、代码放哪、怎么编译打包」这套事标准化下来。

本文只讲 Maven 本身(构建 + 依赖);Gradle、CI/CD、私服搭建不在范围内。阅读前需要会命令行,装好 JDK(见 Java 背景和基本结构)。

1. Maven 是什么

1. 从「jar 地狱」说起

手工用库是这样的:要发个 HTTP 请求,得下 httpclient.jar,可它又依赖 httpcore.jar、commons-logging.jar……版本还得对得上——这版 httpclient 只认那版 httpcore。项目一多,几十个 jar 摆在 lib/ 里,升级一个牵动一片,删错一个编译就炸。这个状态叫依赖地狱(dependency hell)。

Maven 把这件事从「手工管理文件」变成「声明坐标」——每个库用它的 g/a/v 三件套标识(groupId / artifactId / version,详见第 4 节),Maven 认这组坐标去取。

与其他生态对照

生态依赖管理构建
JavaMaven / GradleMaven / Gradle
C / C++手工下库Make / CMake
Node.jsnpm / pnpmnpm
Pythonpip脚本 / Make

2. 它替你管两件事

  • 依赖管理:你只声明「我要用 mysql-connector-j 8.4.0」,Maven 自动去仓库下载它,把它的传递依赖也一并解析出来,统一放进 classpath。不用再手动下 jar、手动 -cp 拼。
  • 构建:把「编译 → 测试 → 打包 → 安装」固定成命令,一条 mvn package 从源码出 jar。
对照 C:相当于把 Make/CMake 干的活(构建)和「去官网下库、手动摆好头文件和 .lib」干的活(依赖),合成一套标准工具。零散敲 javac 是更原始的状态。

3. POM:项目的说明书

Maven 项目的核心是一个 XML 文件 pom.xml(Project Object Model,项目对象模型)。项目坐标、依赖、插件、构建配置全写在里面。看到 pom.xml 就知道这是个 Maven 项目。

4. 约定优于配置

Maven 规定好默认的目录结构、输出目录、生命周期。照约定放文件就几乎不用配置,要偏离约定时才去 pom 里改。这是它「开箱即用」的原因。

5. 它不只是构建工具

所有构件都用坐标(g/a/v)在全球范围内唯一标识,任何一个项目都能靠坐标引用仓库里的任何构件——这让 Java 生态有了统一的「包管理中心」。对照其它语言:Node 有 npm,Python 有 pip,Java 这边就是 Maven 仓库,而 Maven 兼做客户端与构建工具(Gradle 也用它同一套仓库)。

2. 项目结构

Maven 的标准目录(约定优于配置的具体体现):

  • src/main/java:源代码的根。包名 com.example.app 对应子目录 src/main/java/com/example/app/。也就是说,目录结构必须与 package 声明一致。
  • src/main/resources:非代码资源,构建时被复制到 classpath 根下。db.properties 这类配置文件放这里,代码里就能当 classpath 资源读。
  • src/test/*:测试代码与测试资源,只在跑测试时参与,不打包。
  • target/:构建输出目录,编译出的 .class、打的 jar 都在这。每次构建重新生成,只属于产物、不进版本库,写进 .gitignore。

之所以是 src/main/java 而不是「随便放」,就是第 1 节的约定优于配置:Maven 知道去哪儿找源码、去哪儿放产物,你才不用每次告诉它。

在 IDEA 里:直接用 IDEA 打开 pom.xml(或包含它的目录),它会自动识别为 Maven 项目并导入依赖;右侧边栏的 Maven 面板可以双击执行生命周期。IDEA 内置的 Maven 和自己的命令行 Maven 用的是同一套本地仓库。

.gitignore 至少要有:

3. 安装与配置 ★

3.1 前置:JDK

Maven 自己不编译 Java,它调用 JDK。先确认有 JDK、且 JAVA_HOME 指向它:

没有的话先装 JDK,见 Java 背景和基本结构。

3.2 安装

macOS(Homebrew):

通用(解压式):下载 apache-maven-x.y.z-bin.tar.gz(或 .zip),解压到任意目录,把其中的 bin 加进 PATH:

Windows 则把解压后的 bin 目录加进系统环境变量 Path。无论哪种方式,都要保证 JAVA_HOME 在环境变量里,Maven 靠它找 JDK。

3.3 验证

输出三行左右:

能看到就装好了。如果 Maven 版本对、Java 那行却报错或指向 JRE,回头看 JAVA_HOME。

3.4 settings.xml:本地仓库与镜像

Maven 有两处配置文件:

位置作用域建议
$MAVEN_HOME/conf/settings.xml全机器(所有用户)少动
~/.m2/settings.xml当前用户改这个,不动安装目录、升级不丢

~/.m2/ 是 Maven 的用户目录,本地仓库和 settings 都在这里。用户级文件不存在时新建一个即可。下面是一份最小可用的 ~/.m2/settings.xml(独立文件,不在 pom.xml 里):

里面主要就两项:

  • localRepository(本地仓库):下载的 jar 存哪。默认 ~/.m2/repository,位于用户主目录、通常就在系统盘;jar 越攒越多会占掉可观空间,想挪到别的磁盘就改这里。
  • mirror(镜像):Maven 默认从国外中央仓库取包,国内很慢,第一次构建常卡在下载。* 表示所有仓库请求都改走这个地址;若只想代理中央仓库、不影响别的仓库,写成 central。

4. pom.xml 骨架

最小的可构建 pom.xml——下面是整个文件, 是根元素:

  • modelVersion:POM 模型的版本,固定 4.0.0,照抄。
  • project 上的 xmlns / xsi 是 XML 命名空间与 schema 声明:Maven 构建不依赖它,但带上 IDE 才能按 POM 的 XSD 校验、补全,所以照抄即可。真正必须的只有下面的 modelVersion。
  • groupId / artifactId / version:坐标,三件套唯一确定一个构件。
  • packaging:产物类型,jar(默认,不写也是 jar)/ war / pom。

坐标(g/a/v)

元素含义惯例
groupId所属组织域名倒写:com.example、org.apache
artifactId项目(构件)名小写短横线:my-app、spring-boot-starter-web
version版本号1.0.0、1.0-SNAPSHOT

三点说明:

  • 坐标不仅标识你自己,也是引用别人的方式——引入依赖时写的就是对方的 g/a/v,例如 com.mysql:mysql-connector-j:8.4.0。
  • 版本号主体(如 1.0)自己定,但末尾的 -SNAPSHOT 是 Maven 的保留后缀,拼写和位置固定、不能改写。SNAPSHOT 表示「开发中快照」——同一个版本号可以反复覆盖重发、会被重新拉取;但正式版本(1.0.0)一旦发布就不该再改。
  • 命名允许的字符有限(字母、数字、-、_),别用中文或空格。

properties:自定义变量(写在 下,与 groupId 同级)

定义后用 ${...} 引用。maven.compiler.release 告诉编译器按哪个 Java 版本编译,能避免 JDK 版本与目标版本不匹配的报错;project.build.sourceEncoding 统一源码编码,防乱码。这两个几乎是每个项目的标配。

5. 依赖管理 ★

5.1 dependencies:声明依赖

写在 下,与 groupId 同级:

不写 会报错。声明后 Maven 自动下载并放进编译与运行 classpath。坐标去 mvnrepository 搜库名,页面上直接给出 g/a/v,复制粘贴即可。

5.2 scope:依赖的作用范围

写在某个依赖内部(嵌在 → → 某个 里),和 / 同级:

不写 就是默认的 compile。四种常用取值:

scope编译测试运行打包典型例子
compile(默认)✅✅✅✅通用库(Jackson)
test❌✅❌❌JUnit
provided✅✅❌❌servlet-api(容器提供)
runtime❌✅✅✅JDBC 驱动
  • compile 是默认值,不写就是它。
  • test 只在测试代码里可见,主代码想引用会编译失败。
  • provided 编译时要用、运行时由容器提供,所以不打进包——典型是 Servlet API,Tomcat 里已经有了,你打进去反而冲突。
  • runtime 编译时不直接调它的类(靠反射/SPI 加载),运行时才需要,所以进包但不参与编译。

另有 system(引用本地文件路径,不可移植,不推荐)、import(只在 dependencyManagement 里用),认得即可。

5.3 传递依赖

依赖会传染:A 依赖 B、B 依赖 C,你引入 A,Maven 就自动把 B 和 C 一起解析进来。省事,代价是依赖树会膨胀、版本会打架。比如你的直接依赖引了 commons-logging:1.1,另一条传递链引了 commons-logging:1.2,同一个库出现了两个版本——这就是依赖冲突。

5.4 依赖冲突与仲裁

同一构件被不同路径引入不同版本时,Maven 按两条规则选一个(就近原则):

  1. 最短路径优先:谁离你「更近」用谁的。以「依赖层数」算路径长度。直接依赖长度为 1,永远赢过传递来的(≥2)。
    • 例:你直接声明 A:1.0,而 B → C → A:2.0,则用 A:1.0。
  2. 最先声明优先:路径长度相同时,谁在 dependencies 里先写用谁的。
    • 例:A:1.0 和 A:2.0 都是长度 2 的传递依赖,则写在前面的那条链带来的版本胜出。

排查冲突:

打印整棵依赖树(含坐标与 scope),顺着看每个构件从哪条路径来、最终选了哪个版本。想只看某个库:

5.5 exclusions:排除传递依赖

不想让某个传递依赖进来(版本不对、有漏洞、用不上),在引入它的那条 内部排掉(位置同样在 → → 里):

exclusion 只写 g/a 即可,不必写版本。

5.6 dependencyManagement:统一版本

dependencyManagement 用在多模块项目:顶层父 pom 统一登记各子模块要用的依赖版本(多模块见第 8 节)。它只登记版本、不真的引入依赖——子模块要用某个库时照常写 ,但可以省掉 ,版本从这里的父 pom 取。

父 pom(写在它的 下):

子模块(写在它自己的 下)——引用同一坐标就不用再写版本:

一句话区分:dependencies 是「我依赖它」,dependencyManagement 是「如果谁依赖它,用这个版本」。

5.7 optional:可选依赖

写在依赖内部( → → 里),和 、 同级:

加了它之后,这个依赖不会被传递给下游——别人依赖你时,你的可选依赖不会自动变成他的。适合「只在特定场景用、不想强加给使用者」的库。

注意方向和 5.5 的 exclusions 相反:exclusions 挡的是传进来的(我不要),optional 挡的是传出去的(我不给)。

6. 构建流程 ★

6.1 三套生命周期

Maven 把构建拆成三套互相独立的生命周期,每套是一串阶段(phase):

生命周期作用主要阶段
clean清理pre-clean → clean → post-clean
default构建(核心)validate → compile → test → package → verify → install → deploy
site生成站点文档pre-site → site → post-site → site-deploy

关键规则:执行某个阶段,会先依次执行它前面的所有阶段。比如 mvn package 实际依次跑了 validate → compile → test → package——所以打包前会自动编译、自动跑测试;测试不过,打包就中断。

三套生命周期彼此独立,所以跨生命周期时,命令里写多个阶段就按书写的先后执行:mvn clean package 是先跑 clean 生命周期(删 target/)、再跑 default 到 package(编译测试打包)。顺序不能随手换——写成 mvn package clean 就变成「先打包、再删掉 target/」,等于白干。(只有同一个生命周期内写多个阶段时,顺序才由生命周期定,永远向后推进。)

6.2 default 各阶段含义

  • compile:编译 src/main/java 到 target/classes
  • test:编译并运行 src/test/java 里的单元测试
  • package:把编译产物打成 jar/war,产出到 target/
  • install:把包装进本地仓库(3.4 里 localRepository 配的那个目录),同机器其他项目才能依赖到它
  • deploy:上传到远程仓库(私服)

中间还有 resources(复制资源)、test-compile 等细阶段,日常不用记,知道大流程即可。

6.3 阶段与插件 goal 的绑定

Maven 核心只管流程骨架,真正干活的是插件:每个阶段(phase)绑定了一个或多个插件的目标(goal)。常用的绑定:

阶段默认绑定的插件目标
resourcesmaven-resources-plugin:resources
compilemaven-compiler-plugin:compile
testmaven-surefire-plugin:test
packagemaven-jar-plugin:jar(war 则是 maven-war-plugin:war)
installmaven-install-plugin:install
deploymaven-deploy-plugin:deploy

日常用默认插件就够了;要定制(换编译版本、打可执行 jar、加代码检查)才在 里配置插件及其 goal。

6.4 常用命令

  • 一次可以写多个阶段,按顺序执行。
  • -DskipTests 跳过测试执行(仍编译测试代码):mvn clean package -DskipTests。
  • -U 强制更新 SNAPSHOT 依赖(忽略本地缓存)。
  • -o 离线模式,只用本地仓库。
  • -e 出错时打印完整堆栈,-X 打印 debug 日志,排查构建问题时有用。

6.5 打包产物

  • target/my-app-1.0-SNAPSHOT.jar:普通 jar,只含你自己的类,不含依赖。所以它单独跑不起来——java -jar 会因找不到依赖的类而报错。要让一个 jar 能独立运行,得打胖 jar(fat jar):把自己的类和全部依赖合并进同一个 jar,并写明入口类;由 maven-shade-plugin / maven-assembly-plugin 生成。
  • war:packaging 为 war 的 Web 项目产物,丢进 Tomcat 的 webapps 目录部署。它和 jar 的区别:jar 是通用包(库或程序),无固定内部结构,由 JVM 运行或作依赖引用;war 是 Web 应用专用包,内部按约定放 WEB-INF/(web.xml、类、lib/ 里的依赖 jar)和页面资源,由容器加载部署——war 里其实就装着 jar。
  • install 的意义:只有装进本地仓库,别的项目才能用 g/a/v 依赖到它——多模块之间的依赖就靠这一步。

7. 仓库

Maven 从三种仓库找构件,顺序:本地 → 远程(镜像 / 私服)→ 中央。

仓库位置说明
本地仓库~/.m2/repository下载过的都缓存在这,永远先查这里
中央仓库repo.maven.apache.org全世界公共构件,通常经镜像访问
私服公司内网Nexus / Artifactory,代理中央 + 存内部构件

本地仓库按坐标组织成目录:groupId 的 . 换成 /,再接 artifactId/version/。例如 com.mysql:mysql-connector-j:8.4.0 落在:

构件下不动时,删掉对应目录(或整个本地仓库)重新构建即可——本地仓库随时能重建,是纯缓存,删了不会丢东西。

私服是企业内网的中间仓库:对外代理中央仓库(并缓存),对内存放公司自研构件。好处是快、可控、能审计,还能保存不对外发布的内部库。你自己搭私服不在本文范围,知道有这一层就行。

8. 模块管理:聚合与继承

项目变大时拆成多个模块(如 common、dao、web):每个模块是一个独立的小 Maven 工程,各有自己的 pom.xml;顶层再放一个 pom 把它们串起来,一条命令整体构建。顶层这个叫父 pom(或聚合 pom),被它管的模块叫子模块。父 pom 对子模块有两种关系,可单用也可合用:

别把「模块」和 Java 的「包」(package)混了:模块是一个独立工程(有自己的 pom.xml、src/、坐标,产出一个 jar);包只是模块内部源码的目录分组。一个模块里有很多包,一个包只属于一个模块。

聚合(aggregation):父 pom 用 列出子模块(写在父 pom 的 下),一条命令构建全部。

里填的是子模块所在目录的相对路径(相对父 pom),不是坐标——common 就指同级那层 common/ 目录(里面有自己的 pom.xml)。它常和该模块的 artifactId 同名,但填的是位置。

继承(inheritance):子模块用 声明父 pom(写在子模块的 下,通常紧跟 ),继承父的配置——依赖版本(靠 dependencyManagement)、properties、插件、groupId 等。

两者常一起用:父 pom 既聚合子模块,又被子模块继承。区别一句话——聚合是「一起构建」,继承是「共享配置」。父 pom 的 packaging 必须是 pom(它不产出 jar),并且它通常只放配置、不放业务代码。

典型结构:

在父目录执行 mvn clean install,会按模块间依赖顺序依次构建全部子模块。

9. properties 与 profile

properties:pom 内的变量(写在 下,与 groupId 同级),${...} 引用,减少重复、方便统一改。

除自定义外,也能引用 Maven 内置的变量。它们不来自代码,而是来自 Maven 解析出的项目模型与运行环境:

  • ${project.*}:当前 pom 里的信息,如 ${project.version}(就是这份 pom 的 )、${project.groupId}、${project.build.directory}(构建输出目录,默认 target)
  • ${settings.*}:来自 settings.xml,如 ${settings.localRepository}
  • ${env.*}:环境变量,如 ${env.JAVA_HOME}
  • ${basedir}:pom 所在的目录

profile:按条件切换配置( 写在 下)。比如开发/生产两套配置,用 -P 激活:

db.url 不是 Maven 的固定标签,是自定义属性(名字随你起),值是一串 JDBC 连接地址;定义后在 pom 别处用 ${db.url} 引用。不同 profile 给同一属性不同的值,切 profile 就换了值——在命令行激活上面的 prod 就写 mvn package -Pprod(-P 的值贴着或空一格都行,-P prod 等价)。

profile 还能按条件自动激活(17 之类),或定义在 settings.xml 里做「只在某台机器生效」的配置。日常知道能手动 -P 切换即可。

10. 实战:把手工作坊改造成 Maven 工程

第一阶段那种 javac Hello.java 的裸工程,一旦要用第三方库(比如 MySQL 驱动)就得手动下 jar、手动 -cp。Maven 化之后:源码挪进 src/main/java、依赖写进 pom、一条命令构建。

下面这个例子用「能否拿到 MySQL 驱动」来验证依赖确实被拉进来了——不需要本机装 MySQL,跟着敲就能跑通。

1. 建目录

2. 写 pom

展开收起

3. 写 App.java

DriverManager.getDriver 只查「哪个驱动认这个 URL」,不会真的建立连接,所以不需要本机有 MySQL。能打印出驱动类名,就说明 Maven 把 mysql-connector-j 下载并放进了 classpath——本节要演示的正是这一点。

4. 构建

第一次会去下载 MySQL 驱动及其依赖(配了镜像才快),之后缓存进本地仓库。构建日志会打印每个阶段(clean → compile → test → package),产物在 target/demo-1.0-SNAPSHOT.jar。

5. 运行

最直接的是在 IDEA 里点 App 的 main 方法旁的运行按钮。若要在命令行跑同一个主类:

拆解这行:exec:java 是「调用 exec-maven-plugin 的 java 目标」,写法是 插件前缀:目标——它和 clean / package 那种生命周期阶段不是一回事;-Dexec.mainClass=... 是传给该插件的参数,指定要运行的全限定类名。

跑出来会打印 驱动已就位: com.mysql.cj.jdbc.Driver v8.4 之类——Maven 把驱动下载并放进了 classpath,工程跑通。

对比改造前后:不用再手动找 jar、手动拼 classpath;换机器只要 mvn package 就能重建整个可运行工程。这就是后面 JDBC 命令行工具要用的地基。

附录

常见报错对照

报错原因解决
mvn: command not foundPATH 没配把 Maven bin 加进 PATH
No compiler is provided... Did you mean JAVA_HOME找到的是 JRE 不是 JDK,或 JAVA_HOME 没设装 JDK,JAVA_HOME 指向 JDK 根
Could not find artifact / Could not resolve dependencies坐标写错,或镜像没配、仓库里没有核对 g/a/v;配阿里云镜像
invalid target release: 17maven.compiler.release 高于当前 JDK改成 JDK 支持的版本,或升级 JDK
程序包 xxx 不存在 / cannot find symbol依赖没加,或 scope 不对补 dependency;检查 scope
依赖版本不符预期传递依赖冲突mvn dependency:tree 定位,用 exclusions / dependencyManagement 压
... was cached in the local repository, resolution will not be reattempted之前下载失败被缓存加 -U 强制更新
下载卡住 / 极慢走的国外中央仓库配镜像
Could not transfer ... Connection refused代理 / 网络 / 私服不可达检查网络与代理,或换镜像

pom 元素位置速查

pom.xml 里每个元素写在哪儿:

元素写在哪
modelVersion 下
groupId 下
artifactId 下
version 下
packaging 下
parent 下
properties 下
dependencyManagement 下
dependencies 下
modules 下
profiles 下
build 下
dependency 内( 里也有一个同名 )
scope 内
optional 内
exclusions 内
exclusion 内
module 内
profile 内
plugin → 内
settings.xml 的 settings / localRepository / mirrors / mirror 属于 settings.xml,不在 pom 里。

术语速查

术语含义
POMProject Object Model,即 pom.xml,项目配置的总文件
坐标(g/a/v)groupId / artifactId / version,全局唯一标识一个构件
构件(artifact)一个可被依赖的产物,如一个 jar
生命周期clean / default / site 三套构建流程
阶段(phase)生命周期中的一步,如 compile、package
插件(plugin)真正执行任务的组件,如 maven-compiler-plugin
目标(goal)插件里的一个具体任务,绑定到阶段上执行
scope依赖的作用范围:compile / test / provided / runtime
传递依赖A 依赖 B、B 依赖 C,则 A 间接依赖 C
依赖仲裁冲突时按「最短路径优先、最先声明优先」定版本
胖 jar(fat jar)把自己的类与依赖打进同一个可执行 jar

常用命令速查

命令作用
mvn -v看版本 / 环境
mvn clean清 target/
mvn compile编译
mvn test跑测试
mvn package打包
mvn install打包 + 装进本地仓库
mvn dependency:tree看依赖树
mvn clean package -DskipTests清理打包、跳过测试
mvn -U clean package强制更新依赖后重新构建
mvn -e -X ...打印完整错误 / debug 日志

参考资料

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论