用 Maven 这么多年,说实话一直停留在会敲mvn clean package的阶段。生命周期、插件、依赖传递这些词都听过,但从没认真理过它们之间到底怎么串起来的。直到最近一次插件配置死活不对、依赖冲突排查半天没头绪,才下决心把原理补了一遍。
这篇把我捋明白的核心机制整理出来——生命周期、阶段与插件的关系、依赖管理的几条原则。懂了这些,再看 pom 配置和构建日志就不是天书了。下一篇讲maven常用命令和内网离线的配置和依赖管理。
生命周期:Maven 构建的六个阶段
Maven 把一次构建拆成几个固定的阶段,按顺序执行:
清理:删掉上次编译的结果,为重新编译做准备 编译:把 Java 源码编译成字节码 测试:跑单元测试,记录展示结果 打包:把工程封装成压缩包。Java 工程 jar,Web 工程 war 安装:把打包结果装进本地仓库 发布:把打包结果部署到远程仓库,或把 war 丢到服务器运行
关键在于:这些阶段是有顺序的,执行后面阶段会自动带上前面所有阶段。
三套标准生命周期
Maven 定义了三套独立的生命周期:
default(主构建):处理项目的构建、打包、部署。最常用,包含 validate → compile → test → package → install → deploy 等阶段 clean:清理项目,比如删 target 目录。独立于 default,要清理+构建就得 mvn clean packagesite:生成项目文档站点。用得少,但它是独立的一套
ℹ️ 三套生命周期相互独立。
mvn clean package的意思是:先跑 clean 生命周期的 clean 阶段,再跑 default 生命周期的 package 阶段(及之前所有阶段)。不是"clean 是 package 的前置"。
阶段和插件:谁定步骤,谁干活
这是 Maven 最核心的机制,先记住三个角色:
阶段(Phase):生命周期里的一个步骤,比如 compile、test、package 插件(Plugin):实际干活的 Java 程序,比如编译代码、跑测试、打 jar 目标(Goal):插件里的一个具体功能,比如 maven-compiler-plugin 的 compile 目标
一句话总结:Maven 把插件的 Goal 绑定到生命周期的 Phase 上,由插件去执行具体任务。
举个例子,mvn package 执行时,实际发生的是:
validate → compile → test → package每个阶段触发它绑定的插件目标——compile 阶段触发 compiler 插件的 compile 目标,test 阶段触发 surefire 插件的 test 目标,依此类推。
常见插件和目标对应关系:
⚠️ 你不能直接"运行一个插件目标"——除非显式调用(
mvn 插件:目标)。但你运行一个阶段时,Maven 会自动执行该阶段及之前所有阶段绑定的插件目标。这就是为什么mvn package会连带编译、测试一起跑。
Mojo:插件目标的实现
插件里每个目标都是一个 Mojo(Maven plain Old Java Object),就是插件目标的实现类。开发插件时,用 @Parameter 注解声明参数:
@Mojo(name = "run")publicclassAntrunMojoextendsAbstractMojo {@Parameterprivate Target target;publicvoidexecute() { ... }}这里的 private Target target 对应 pom 里 <target> 配置项。Mojo 里 @Parameter 标注的字段名,就是 pom 配置标签的名字。 懂了这个,看到插件文档里一堆配置项就不懵了——每个都是 Mojo 里一个 @Parameter 字段。
常用插件配置:指定编译 JDK 版本
maven-compiler-plugin 最常用来配 JDK 版本。这里有个坑:3.8.0 之前的版本默认用 JDK 1.5 编译。不显式指定,编译出来的是低版本字节码,用了高版本语法直接报错。
除了改 settings.xml,最干净的方式是在 pom 里配:
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.2</version><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding></configuration></plugin></plugins></build>source 是源码语法版本,target 是编译字节码版本,encoding 管编码。三个一起配最稳。
Maven 依赖:坐标、排除与冲突原则
GAV 坐标和仓库路径
Maven 靠 GAV(groupId、artifactId、version)定位一个依赖,在仓库里的物理路径是:
仓库地址/{groupId点替换成斜杠}/{artifactId}/{version}/{artifactId}-{version}.jar⚠️ 关键:groupId 里的点要全替换成斜杠。比如
com.example在仓库里是com/example/目录。第一次找依赖找不到,多半是这块没对应上。
完整坐标形式是 groupId:artifactId:version[:packaging[:classifier]]。packaging 默认 jar,classifier 用来区分同版本的不同产物(sources、javadoc 等)。
排除传递依赖
依赖 A 引进来,它会带一堆传递依赖。如果其中某个有冲突或不要,用 exclusions 排掉:
<dependency><groupId>xxx</groupId><artifactId>xxx</artifactId><exclusions><exclusion><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId></exclusion></exclusions></dependency>依赖冲突的两条原则
多个依赖间接引入同一个 jar 的不同版本,Maven 按这两条规则选:
原则一:路径最短者优先。 A→B→C→commons 1.1,A→D→commons 1.2,选 1.2(路径短) 原则二:路径相同时,先声明者优先。 在 pom 里谁写前面就用谁的版本
ℹ️ 排查冲突用
mvn dependency:tree,能打出来完整依赖树,一眼看清某个 jar 是从哪条链路引进来的、最终用了哪个版本。这条命令下一篇会详细说。推荐使用 IDEA 插件:Maven Helper,应该是标配了。
依赖继承:dependencyManagement
多模块项目里,子模块要统一依赖版本,靠父 pom 的 dependencyManagement:
<dependencyManagement><dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.9</version><scope>test</scope></dependency></dependencies></dependencyManagement>子模块声明依赖时,只写 groupId 和 artifactId,省掉 version 和 scope:
<dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId></dependency></dependencies>⚠️ 注意区分:
dependencyManagement只是声明版本,不会真正引入依赖。子模块不写,就拉不进来。真正引入依赖的还是<dependencies>。这个区别搞混了,就会出现"父 pom 配了但子模块没生效"的问题。
小结
这一篇把 Maven 构建的核心原理捋了一遍:
生命周期:三套(default/clean/site),每套多个阶段,阶段有顺序 插件:Goal 绑定到 Phase,插件是干活的,阶段是指挥的 Mojo: @Parameter字段名 = pom 配置标签名依赖:GAV 定位、exclusions 排除、最短路径和先声明两条冲突原则 继承:dependencyManagement 统一版本,dependencies 真正引入
懂了这些,看 pom 配置和构建日志就有底了。下一篇讲日常命令大全、内网离线环境搭建和变量属性,更偏实操。
你是怎么学的 Maven?是一开始就系统啃了原理,还是跟我一样先会敲命令、后来才补的课?留言区聊聊。
觉得有用点个在看,转发给同样在用 Maven 的同事。想第一时间收后续更新,关注「扶锐随笔」。
下篇见。
夜雨聆风