2316 字
约 7 分钟
14
S06-B07 · Maven 生命周期入门:从 mvn package 到依赖解析

敲下 mvn package,满屏日志过后 BUILD SUCCESS。可 target/ 里为什么多出 jar、测试为什么自己跑起来、第三方包是谁塞进去的——说不清这三件事,遇到"本地能跑、同事拉下来 ClassNotFound"就只能删 .m2 重下碰运气。

这篇只拆机制:阶段怎么串、谁在阶段上干活、依赖怎么被选中、仓库按什么顺序找。


1. mvn package 其实是"走到某个阶段"

Maven 有三套互相独立的生命周期,一条命令只在其中一套里走:

生命周期 干什么 常走的终点
clean 删 target/ clean
default 编译、测试、打包、安装、发布 package / install / deploy
site 生成文档站点 site / site-deploy

mvn clean package 是两套生命周期串联,不是一条链子。所以 mvn package 不会清掉上次的 class 文件——旧的 .class 留在 target/classes 里,改了类名之后照样能打进 jar,这是本地"看起来正常"的一大来源。

default 生命周期的阶段顺序(常用的一段):

validate → initialize → generate-sources → process-sources
→ generate-resources → process-resources → compile
→ process-classes → generate-test-sources → process-test-sources
→ generate-test-resources → process-test-resources
→ test-compile → process-test-classes → test
→ prepare-package → package → pre-integration-test → integration-test
→ post-integration-test → verify → install → deploy

关键规则:指定一个阶段,它前面的所有阶段都会被执行。所以 mvn package 必然跑过 compile 和 test;mvn test 不会打包。

2. 阶段是空壳,插件才干活

阶段本身没有逻辑,只是一串"钩子位置"。真正执行的是绑定在这些位置上的插件目标(goal)。

阶段 默认绑定的 goal 产物
process-resources maven-resources-plugin:resources target/classes 下的配置/静态资源
compile maven-compiler-plugin:compile target/classes/*.class
test maven-surefire-plugin:test target/surefire-reports/
package maven-jar-plugin:jar(jar 打包时) target/*.jar
install maven-install-plugin:install 本地仓库副本
deploy maven-deploy-plugin:deploy 远程仓库副本

package 绑什么取决于 <packaging>:jar 出 jar,war 出 war,pom 只用于聚合与父工程。

自己往阶段上挂东西,靠 <executions> 的 <phase>:

<plugin>
  <artifactId>maven-antrun-plugin</artifactId>
  <version>3.1.0</version>
  <executions>
    <execution>
      <id>echo-after-resources</id>
      <phase>process-resources</phase>
      <goals><goal>run</goal></goals>
      <configuration>
        <target><echo message="resources 已完成,准备编译"/></target>
      </configuration>
    </execution>
  </executions>
</plugin>

<phase> 决定"什么时候",<goals> 决定"做什么",<id> 改名等于新增一条绑定。

3. pom.xml 的四层结构

一份能跑的最小 pom.xml 只有四类内容:

层 元素 回答的问题 写错的表现
坐标 groupId / artifactId / version / packaging 我是谁 装进仓库后别人引不到
依赖 <dependencies> 我需要谁 编译期找不到符号
构建 <build> 怎么编译、打什么包 插件版本漂移、产物不对
父工程 <parent> 我从谁继承默认值 版本号被父工程覆盖,改本地不生效
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>top.example</groupId>
  <artifactId>demo</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>jar</packaging>

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

  <dependencies>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>2.0.13</version>
    </dependency>
  </dependencies>
</project>

<packaging> 写在坐标层,却决定了 package 阶段绑哪个 goal。

4. 依赖解析:谁被选中

解析是递归的:先取直接依赖,再取它们的依赖,直到没有新坐标。同一坐标出现多次时按两条规则裁决:

  1. 最近优先:路径短的赢,深度 1 永远赢深度 2;
  2. 同深度先声明者赢:pom.xml 里写在前面那条生效。
$ mvn dependency:tree -Dverbose
[INFO] top.example:demo:jar:1.0.0-SNAPSHOT
[INFO] +- org.springframework:spring-web:jar:6.1.4:compile
[INFO] |  \- org.springframework:spring-core:jar:6.1.4:compile
[INFO] \- com.example:legacy-lib:jar:2.0.0:compile
[INFO]    \- (org.springframework:spring-core:jar:5.3.0:compile - omitted for conflict with 6.1.4)

读法:

记号 含义
+- / \- 同级依赖,\- 是最后一个
缩进层级 依赖深度,第一层是直接依赖
omitted for conflict 被调解规则淘汰,括号里是被淘汰的版本
omitted for duplicate 同坐标同版本重复出现,只保留第一次
managed from X 版本由 dependencyManagement 统一指定

树里赢了不代表运行时赢:容器或父类加载器可能换成别的版本,最终 classpath 用 mvn dependency:build-classpath 核对。

5. scope 五种,传播性各不同

scope 编译期可见 运行期可见 打包进产物 传递给下游
compile(默认) 是 是 是 是
provided 是 否(由容器提供) 否 否
runtime 否 是 是 是
test 仅测试 仅测试 否 否
import — — — 仅用于 dependencyManagement

provided 是最容易出事的:本地 IDE 里有 servlet-api,编译通过;打成 war 后容器提供的是另一个版本,方法签名一变就是运行期 NoSuchMethodError。import 不是普通依赖,它只能出现在 <dependencyManagement> 里,用来把一份 BOM 的版本清单导入进来:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>3.3.6</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

6. SNAPSHOT 与 release 的行为差异

维度 -SNAPSHOT release
版本可变性 同一版本号可反复覆盖 按约定不可变
本地仓库更新 按 updatePolicy 检查远程 metadata 存在即复用
依赖方拿到什么 可能每天不同 固定
适用场景 团队内部联调 对外发布、上线制品

-U 强制刷新只对 SNAPSHOT 有意义(以及清理"上次下载失败留下的 .lastUpdated 标记")。release 版本重复 deploy 到同一仓库,多数仓库管理器会拒绝覆盖。

7. 多模块里的三个不同概念

概念 写法 作用 常见误解
聚合(aggregation) <modules> 一条命令构建多个子模块 以为聚合等于继承
继承(inheritance) <parent> 子模块继承配置与版本 以为必须同目录
依赖(dependency) <dependencies> 模块间引用代码 以为子模块自动互相可见
root/pom.xml            <modules> 里列 a、b      ← 只负责"一起构建"
root/pom.xml            <dependencyManagement>   ← 只负责"统一版本"
a/pom.xml               <parent> 指向 root       ← 继承默认值
b/pom.xml               依赖 a 的坐标            ← 才产生编译期可见

聚合不产生依赖:b 能用 a,是因为 a 装进了本地仓库,不是因为同在一个 <modules> 里。

8. 仓库优先级与"下载了但不是它"

查找顺序是固定的三层:

~/.m2/repository  →  settings.xml 中的 mirror(覆盖原仓库地址)  →  远程仓库

<mirrorOf>*</mirrorOf> 会把包括 central 在内的所有仓库请求都改写到镜像,于是"我明明在 pom 里配了私服"却仍然从镜像拿包。判断实际来源有三个办法:

$ mvn -X dependency:resolve 2>&1 | grep -i "download"
$ cat ~/.m2/repository/top/example/demo/1.0.0/_remote.repositories
$ mvn help:effective-settings

_remote.repositories 记录构件来自哪个仓库 id,effective-settings 展示镜像合并后的真实结果。清 .m2 后行为突变,多因这个标记被一起删掉。

9. 最小可跑示例

$ mkdir -p demo/src/main/java/top/example && cd demo
$ # 写入上面那份 pom.xml,再写一个 Hello.java
$ mvn -q package
$ mvn dependency:tree
$ mvn -o package      # 离线:只读本地仓库,验证依赖是否齐

10. 排错

现象 原因 处理
上线后 NoSuchMethodError 依赖调解选中了低版本,或容器覆盖 dependency:tree 定位,用 dependencyManagement 钉版本
mvn package 报测试失败 test 阶段先于 package 修测试;-DskipTests 只跳过执行,-Dmaven.test.skip=true 连编译都跳过
本地能跑、别人拉下来报错 用了未 deploy 的 SNAPSHOT 该模块先 mvn install/deploy,或统一走私服
install 后行为没变 本地仓库有更早的同版本构件 确认版本号是否递增,SNAPSHOT 加 -U
多模块版本不同步 子模块各自写死版本 版本提到父工程 <properties>,子模块引用 ${revision} 或父版本
清了 .m2 才正常 旧构件残留 / _remote.repositories 指向旧仓库 只删对应坐标目录,不整仓删除

11. 边界

需求 更合适的做法 理由
构建缓存与增量 Gradle build cache Maven 只做产物级跳过,无跨机缓存
依赖漏洞扫描 dependency-check 或平台 SCA 生命周期本身不检查 CVE
环境隔离的构建 容器内构建 + 固定基础镜像 本机 JDK/Maven 版本漂移无法复现
发布流水线 CI 里 deploy + 制品库 人手 install 会把污染带进本地仓库

12. 自测

  1. mvn package 会不会执行 test 阶段?依据是哪条规则?
  2. process-resources 阶段默认绑定的 goal 是哪个,产物落在哪个目录?
  3. 一个依赖在深度 2 是 2.0,深度 3 是 3.0,最终选哪个?为什么?
  4. provided 与 compile 在打包和传递上差在哪?
  5. 聚合(<modules>)为什么不能让模块之间直接引用代码?
  6. 明明配了私服却从镜像下载,该用哪两个命令确认?
S06-B07 · Maven 生命周期入门:从 mvn package 到依赖解析
https://www.clxhxhhr.top/posts/6076/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。