Maven / Gradle 构建
Java 中级 · 工程化
构建工具负责依赖管理、编译、打包、多模块组织。Maven 靠「约定 + XML」简单稳定,Gradle 靠「代码化 + 增量构建」灵活高效。本篇重点讲清 Maven 的依赖传递与仲裁——这是「jar 包冲突」的头号根源。
一、Maven 核心概念
1.1 坐标与依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
<scope>compile</scope> <!-- 作用域 -->
<exclusions> <!-- 排除传递依赖 -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>| scope | 编译 | 测试 | 运行 | 打包 | 典型用途 |
|---|---|---|---|---|---|
compile(默认) | ✅ | ✅ | ✅ | ✅ | 核心依赖 |
provided | ✅ | ✅ | ✅ | ❌ | servlet-api(容器提供) |
runtime | ❌ | ✅ | ✅ | ✅ | JDBC 驱动 |
test | ❌ | ✅ | ❌ | ❌ | JUnit |
system | ✅ | ✅ | ✅ | ❌ | 本地 jar(不推荐) |
二、依赖传递与仲裁(重点)
依赖会传递:A 依赖 B,B 依赖 C,则 A 自动拿到 C。这很方便,也是冲突的根源。
2.1 两条仲裁规则
- 最短路径优先:
A → C(1.0)(深度 1)胜过A → B → C(2.0)(深度 2),用 1.0。 - 先声明优先:深度相同时,
pom.xml里先声明的那条被采用。
2.2 查看与解决冲突
bash
# 打印依赖树,找冲突
mvn dependency:tree
# 分析冲突原因
mvn dependency:analyze
# 看某个依赖为什么被引入
mvn dependency:tree -Dincludes=commons-logging:commons-logging解决冲突的手段:
xml
<!-- 1. 排除传递依赖 -->
<exclusions><exclusion>...</exclusion></exclusions>
<!-- 2. 直接声明想要的版本(路径最短,优先) -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version> <!-- 统一版本 -->
</dependency>
</dependencies>
</dependencyManagement>NoSuchMethodError / ClassNotFoundException 的头号原因
运行时报 NoSuchMethodError,多半是依赖冲突——编译期用的是 A 版本,运行期类路径里却被仲裁成了 B 版本,方法签名不匹配。用 mvn dependency:tree 定位,用 dependencyManagement 统一版本。
三、生命周期与插件
Maven 有三套生命周期,最常用的是 default:
text
validate → compile → test → package → verify → install → deploy| 命令 | 作用 |
|---|---|
mvn clean | 清理 target |
mvn compile | 编译主代码 |
mvn test | 跑单元测试 |
mvn package | 打成 jar/war |
mvn install | 装进本地仓库 |
mvn deploy | 发布到远程仓库/私服 |
生命周期本身是抽象的,真正干活的是插件:
xml
<build>
<plugins>
<!-- 编译插件:指定 JDK 版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<!-- Spring Boot 打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>四、多模块工程
xml
<!-- 父 pom:统一版本与依赖管理 -->
<groupId>com.demo</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>common</module>
<module>service</module>
<module>web</module>
</modules>
<dependencyManagement>
<!-- 子模块继承版本,无需重复写 -->
</dependencyManagement>
<!-- 子 pom -->
<parent>
<groupId>com.demo</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>web</artifactId>为什么要用 dependencyManagement
dependencyManagement 只声明版本不实际引入;子模块引入依赖时省略 version,自动继承。这样全工程版本统一,改一处即可,是解决冲突的官方姿势。
五、Gradle 对比
Gradle 用 Groovy/Kotlin DSL 写构建脚本,是代码而非 XML,更灵活,且增量构建 + 构建缓存更快。
kotlin
// build.gradle.kts
plugins { java; id("org.springframework.boot") version "3.2.0" }
group = "com.demo"; version = "1.0.0"
java { sourceCompatibility = JavaVersion.VERSION_17 }
repositories { mavenCentral() }
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}| 维度 | Maven | Gradle |
|---|---|---|
| 配置 | XML(约定强) | Groovy/Kotlin DSL |
| 构建速度 | 一般 | 快(增量/缓存/并行) |
| 灵活性 | 中 | 高 |
| 学习曲线 | 低 | 中 |
| 生态 | 成熟稳定 | 现代(Android/K8s 生态) |
六、最佳实践
- 用
dependencyManagement统一版本,子模块省略 version。 - 定期
mvn dependency:tree排查冲突,exclusions排掉不需要的传递依赖。 provided不要打进包(servlet-api 等),否则容器里冲突。- 私服(Nexus) 缓存依赖 + 分发内部构件,别看不了中央仓库就崩。
settings.xml配镜像(阿里云镜像加速),CI 里可关快照更新。- 版本锁定:生产用 release 版本,禁用
-SNAPSHOT上线。
📚 官方文档与参考资料
✏️ 练习与自测
- Maven 依赖仲裁的两条规则是什么?如果路径深度相同会怎样?
compile、provided、runtime、test四个 scope 的区别是什么?JDBC 驱动该用哪个?- 线上报
NoSuchMethodError,如何用 Maven 命令定位是哪个依赖冲突?怎么解决? dependencyManagement和dependencies有什么区别?多模块工程为什么推荐前者?
本节小结
构建工具的核心是依赖管理。Maven 靠 最短路径优先 + 先声明优先 仲裁版本,冲突用 exclusions / dependencyManagement 解决;生命周期由插件驱动;多模块用 parent + dependencyManagement 统一版本。遇到 NoSuchMethodError 先跑 mvn dependency:tree。