mid - Maven依赖管理与构建生命周期
Maven 依赖管理与构建生命周期
阅读顺序:基础、POM 与仓库 → 依赖、生命周期与插件 → 多模块、版本与私服
这篇回答两个核心问题:项目最终用了哪些 JAR 和版本?一条 Maven 命令为什么会按那个顺序执行那些任务?
依赖传递
依赖分为直接依赖和传递依赖。项目声明 A,而 A 又依赖 B,当前项目通常也会得到 B。真正进入 classpath 的版本应以依赖树为准:
代码块收起展开
mvn dependency:tree
mvn dependency:tree -Dverbose
mvn dependency:tree -Dincludes=org.slf4j:slf4j-api依赖冲突
Maven 的裁决规则:
- 路径最近优先:层级更浅的版本生效,直接依赖通常覆盖传递依赖。
- 同深度先声明优先:路径一样长时,POM 中先声明的直接依赖路径获胜。
- 同一个 POM 不应重复声明相同
groupId + artifactId;不要依赖“后写覆盖前写”的偶然行为。
稳定做法是用 dependencyManagement 明确统一版本,而不是靠调整声明顺序:
代码块收起展开
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.16</version>
</dependency>
</dependencies>
</dependencyManagement>dependencyManagement 只提供版本和默认配置,不会真正引入依赖;模块仍要在 <dependencies> 中声明。
可选依赖 optional
当前项目仍能使用该依赖,但依赖当前项目的下游不会自动得到它:
代码块收起展开
<dependency>
<groupId>com.example</groupId>
<artifactId>feature-adapter</artifactId>
<version>1.0.0</version>
<optional>true</optional>
</dependency>适合“某功能有多种可选实现”的库;下游需要该功能时应自己显式声明。
排除依赖 exclusions
代码块收起展开
<dependency>
<groupId>com.example</groupId>
<artifactId>legacy-client</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>排除项不写版本,因为它表示切断该路径上的整个构件。若项目仍需要它,应在当前 POM 重新显式声明正确版本。
依赖范围 scope
| scope | 主代码编译 | 测试 | 运行/打包 | 向下游传递 | 常见场景 |
|---|---|---|---|---|---|
compile | 是 | 是 | 是 | 是 | 默认范围,普通依赖 |
provided | 是 | 是 | 否 | 否 | Servlet API,由容器提供 |
runtime | 否 | 是 | 是 | 是 | JDBC 驱动、运行时实现 |
test | 否 | 是 | 否 | 否 | JUnit、Mockito |
system | 是 | 是 | 否 | 否 | 本机绝对路径 JAR,应避免 |
import | 不适用 | 不适用 | 不适用 | 不适用 | 在 dependencyManagement 导入 BOM |
不要为了临时通过编译随意改 scope:主代码直接引用的 API 不能放在 test 或 runtime;只有运行环境确实提供的 API 才使用 provided。
BOM
BOM 是一份 packaging=pom 的版本清单,使用 type=pom、scope=import 在 dependencyManagement 中导入:
代码块收起展开
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.11.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>BOM 统一一组经过兼容性验证的版本,但不会自动引入其中所有依赖。Spring Boot、JUnit、Testcontainers 等生态常用 BOM;导入后仍在 <dependencies> 中按需声明具体构件。
生命周期与插件
Maven 内置三套相互独立的生命周期:
clean:清理旧构建结果。default:验证、编译、测试、打包、校验、安装、发布。site:生成和发布项目站点。
常用 default 阶段:
validate → compile → test → package → verify → install → deploy
执行 mvn verify 会从 validate 依次执行到 verify,但不会自动执行另一套生命周期中的 clean,所以常用 mvn clean verify。
生命周期规定顺序,phase 表示阶段,plugin goal 才执行具体工作。
| 阶段 | 常见插件目标 |
|---|---|
compile | maven-compiler-plugin:compile |
test | maven-surefire-plugin:test |
package | maven-jar-plugin:jar |
verify | maven-failsafe-plugin:verify |
install | maven-install-plugin:install |
deploy | maven-deploy-plugin:deploy |
mvn dependency:tree 是直接调用 Dependency Plugin 的 tree goal,不是生命周期阶段。
插件版本与测试
插件也要固定版本,否则不同时间或环境可能解析出不同默认行为。父 POM 中的 pluginManagement 只提供版本和默认配置,子模块通常仍需声明插件后才会应用。
代码块收起展开
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
</plugins>
</build>插件配置中的三个层次:
plugin:选择并配置一个插件。goal:插件提供的具体能力,如compiler:compile、dependency:tree。execution:把一个或多个 goal 绑定到某个 phase,并可为这次执行单独配置参数。
jar、war、pom 等 packaging 会带来不同的默认插件绑定;直接执行 mvn pluginPrefix:goal 则不要求 goal 必须绑定到生命周期。
代码块收起展开
mvn package -DskipTests
mvn package -Dmaven.test.skip=true-DskipTests:编译测试代码,但不运行。-Dmaven.test.skip=true:连测试代码也不编译,更容易掩盖问题。
CI 和合并前应正常运行测试。Surefire 通常执行单元测试,报告在 target/surefire-reports/;Failsafe 常用于集成测试,并在 verify 阶段完成校验。
实用排错顺序
代码块收起展开
mvn -v
mvn help:effective-settings
mvn help:effective-pom
mvn dependency:tree -Dverbose
mvn clean verify -e| 现象 | 优先检查 |
|---|---|
Could not resolve artifact | 坐标、镜像、网络、私服权限 |
Non-resolvable parent POM | relativePath、父版本、父仓库 |
package ... does not exist | scope、exclusion、版本冲突 |
invalid target release | mvn -v 的实际 JDK |
ClassNotFoundException | runtime classpath、provided、最终打包内容 |
下载失败时可用 mvn -U clean verify 强制检查更新;不要直接清空整个 .m2/repository,应只处理报错坐标对应的目录或 .lastUpdated 文件。
依赖冲突与质量检查
代码块收起展开
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind
mvn dependency:tree -Dverbose
mvn dependency:analyze处理冲突的顺序:先找到各版本来源路径,再选已验证兼容的版本,用 dependencyManagement 固定;只在确有必要时使用 exclusions,最后运行测试并关注 NoSuchMethodError、ClassNotFoundException 等二进制兼容问题。
dependency:analyze 能发现“使用但未声明”和“声明但未使用”,但反射、注解处理器、ServiceLoader 可能造成误报,不能机械删除。
缓存与离线模式
代码块收起展开
mvn -U clean verify # 强制检查远程更新
mvn -o test # 只使用本地缓存下载中断可能留下 .lastUpdated。应从报错确定具体坐标,先检查镜像、代理、证书和权限,再只删除该构件版本目录中的失败标记或损坏文件。
JDK 与测试定位
以 mvn -v 显示的 Java Home 为 Maven 实际环境。IDE 使用 JDK 21,并不代表终端 Maven 没有仍在使用 JDK 8。
代码块收起展开
mvn -Dtest=UserServiceTest test
mvn -Dtest=UserServiceTest#shouldCreateUser test- 单元测试报告:
target/surefire-reports/。 - 集成测试报告:
target/failsafe-reports/。
测试单独通过、全量失败时,重点检查共享状态、固定端口、数据库数据和并行执行;只执行 package 也可能漏掉 verify 阶段才完成的集成测试校验。
可复现构建检查
- 使用 Maven Wrapper 固定 Maven 版本。
- 用
maven.compiler.release固定目标 Java 版本。 - 依赖与插件版本由 POM、父 POM 或 BOM 明确管理。
- 不使用
LATEST、RELEASE或无边界版本范围。 - release 构件不可覆盖,snapshot 只用于开发。
- CI 从干净工作区执行
mvn clean verify。 - POM 不包含密码、令牌和绝对本地路径。
需要强制 Maven/JDK 版本时可使用 Maven Enforcer Plugin;需要多 JDK 工具链时使用 Maven Toolchains Plugin。