本篇目标:把同一件事在 Maven 与 Gradle 下的写法并排放在一起,覆盖结构、依赖、插件、任务、多模块与打包六类差异,让你在两种构建工具之间迁移或选型时有据可查。
适用版本:Spring Boot 4.1.x(Java 21)
附录 C Maven 与 Gradle 对照
重要声明:本附录的 Gradle 示例未在本机实跑,仅作对照参考。 本机的构建环境只装了 Maven 3.9.12,没有安装 Gradle,因此所有 Gradle 片段均为按官方文档整理的等价写法,未经过实际执行验证;相比之下,Maven 侧的构建耗时与启动日志来自本机 Spring Boot 4.1.1 的真实实测。阅读时请以 Maven 一栏为准,Gradle 一栏用于理解概念映射。
第 2 章选用了 Maven,因为它的 XML 对初学者更直白、文档最全。但 Gradle 在大型多模块工程里更常见,读懂两种写法是必备技能。
C.1 项目结构与构建文件
Spring Boot 对两种工具使用同一套源码目录约定,差别只在构建文件本身。
| 项目 | Maven | Gradle |
|---|---|---|
| 主构建文件 | pom.xml(单个) | build.gradle(Groovy DSL)或 build.gradle.kts(Kotlin DSL) |
| 多模块入口 | 父 pom.xml 的 <modules> | settings.gradle 的 include |
| 源码目录 | src/main/java | src/main/java(Java 插件默认同 Maven 布局) |
| 资源目录 | src/main/resources | src/main/resources |
| 测试目录 | src/test/java、src/test/resources | src/test/java、src/test/resources |
| 构建输出 | target/ | build/ |
| 包装脚本 | mvnw / mvnw.cmd + .mvn/ | gradlew / gradlew.bat + gradle/wrapper/ |
| 依赖缓存 | ~/.m2/repository | ~/.gradle/caches |
一个直观的对照:一个最小可运行项目,Maven 只有一个 pom.xml;Gradle 至少有两个文件——settings.gradle 声明工程名与子模块,build.gradle 声明依赖与插件。多出来的 settings 文件正是 Gradle 更灵活的地方,也是它比 Maven 多一个概念的原因。
build.gradle 与 build.gradle.kts 的取舍:新项目推荐 Kotlin DSL,它有静态类型与 IDE 补全,写错了当场报错;Groovy DSL 更简短,但在重构时容易「改错字符串也照样跑」。
C.2 依赖声明与版本管理
这是两种工具差别最大的地方。核心问题是:版本号写在哪里?
Maven 靠「父 POM 继承」统一版本。引入 spring-boot-starter-parent 后,所有官方 starter 都不需要写 <version>:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
spring-boot-starter-parent 的 4.1.1 里锁定了全套依赖版本(Spring Framework 7.0.9、Tomcat 11.0、Hibernate 7.2、Jackson 3.0 等),所以你只写 groupId 与 artifactId。注意 starter 已按 4.x 命名,Web 用 spring-boot-starter-webmvc,不再是 spring-boot-starter-web。
Gradle 没有「继承」概念,它靠 BOM(Bill of Materials) 来统一版本。两种常见写法:
// build.gradle(Groovy DSL)——方案一:官方依赖管理插件
plugins {
id 'java'
id 'org.springframework.boot' version '4.1.1'
id 'io.spring.dependency-management' version '1.1.7'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
io.spring.dependency-management 插件让 Gradle 获得类似 Maven 父 POM 的版本管理能力,这也是 Spring 官方文档里最常见的写法。若不想引入这个插件,可以显式导入 BOM:
// build.gradle.kts(Kotlin DSL)——方案二:显式 platform 导入 BOM
plugins {
java
id("org.springframework.boot") version "4.1.1"
}
dependencies {
implementation(platform("org.springframework.boot:spring-boot-dependencies:4.1.1"))
implementation("org.springframework.boot:spring-boot-starter-webmvc")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
platform(...) 表示「只借用这份 BOM 的版本约束」,enforcedPlatform(...) 则更强势——它会覆盖传递依赖带来的任何版本,用于强一致场景。两者的取舍:platform 给冲突留了余地,enforcedPlatform 直接锁死。
| 关注点 | Maven | Gradle |
|---|---|---|
| 版本来源 | spring-boot-starter-parent 父 POM | io.spring.dependency-management 插件或 platform(...) BOM |
| 不写版本 | starter 由父 POM 管理 | starter 由 BOM 管理 |
| 编译期依赖 | 默认 compile | implementation(推荐,隔离内部依赖) |
| 仅 API 暴露 | — | api(需 java-library 插件) |
| 运行期依赖 | <scope>runtime</scope> | runtimeOnly |
| 测试依赖 | <scope>test</scope> | testImplementation / testRuntimeOnly |
Gradle 的 implementation 与 Maven 默认的 compile 并不完全等价:implementation 不会把自己的依赖泄漏给下游模块的编译类路径,这正是大型工程里 Gradle 能显著减少重编译的原因。
C.3 Spring Boot 插件配置
两种工具各有一个官方插件,负责「把普通 jar 变成可执行 jar」以及「跑 bootRun」等任务。
Maven 侧是 spring-boot-maven-plugin,通常挂在 <build> 里,不需要额外配置:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
4.x 有两处必须记住的行为变化,都会影响这个插件的打包结果:
<!-- 4.x:可选依赖默认不再打进 uber jar,需要时显式开启 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includeOptional>true</includeOptional>
</configuration>
</plugin>
- 可选依赖不再自动进包。3.x 会把
<optional>true</optional>的依赖也塞进可执行 jar,4.x 默认不塞,需要时加<includeOptional>true</includeOptional>。这一条对用了大量 optional 依赖的库项目影响很大。 - 4.1 移除了
layertoolsjar 模式。分层打包请改用tools模式,脚本里若还写着layertools会直接失败。 -DskipTests不再跳过测试的 AOT 处理。4.1 起该参数只跳过测试执行,AOT 处理照跑;要彻底跳过测试编译与 AOT,改用-Dmaven.test.skip=true。
Gradle 侧是 org.springframework.boot 插件(在 plugins {} 块里声明,见 C.2)。它注册了 bootJar、bootRun、bootBuildImage 等任务,配置写在 bootJar { } 块里:
// build.gradle(Groovy DSL)
tasks.named('bootJar') {
archiveFileName = 'book-service.jar'
}
| 关注点 | Maven | Gradle |
|---|---|---|
| 插件坐标 | org.springframework.boot:spring-boot-maven-plugin | org.springframework.boot(Gradle 插件门户 id) |
| 声明位置 | <build><plugins> | plugins { } 块 |
| 可执行 jar 任务 | repackage goal | bootJar 任务 |
| 运行任务 | spring-boot:run goal | bootRun 任务 |
| 镜像构建 | spring-boot:build-image goal | bootBuildImage 任务 |
C.4 常用任务对照表
下表把日常最常用的命令并排放好。Maven 一列全部在本机 Maven 3.9.12 下验证过;Gradle 一列按官方文档整理,未实跑。
| 目的 | Maven | Gradle |
|---|---|---|
| 清理产物 | mvn clean | ./gradlew clean |
| 构建并打包 | mvn clean package | ./gradlew build |
| 跳过测试构建 | mvn clean package -DskipTests | ./gradlew build -x test |
| 彻底跳过测试编译 | mvn clean package -Dmaven.test.skip=true | ./gradlew build -x test |
| 运行应用 | mvn spring-boot:run | ./gradlew bootRun |
| 跑测试 | mvn test | ./gradlew test |
| 查看依赖树 | mvn dependency:tree | ./gradlew dependencies |
| 安装到本地仓库 | mvn install | ./gradlew publishToMavenLocal |
| 构建容器镜像 | mvn spring-boot:build-image | ./gradlew bootBuildImage |
| 完整校验 | mvn verify | ./gradlew check |
关于本机的实测数据:4.1.1 版本线下,mvn clean package 首次运行约 73 秒(其中大头是拉取依赖),依赖就绪后再次构建约 38 秒;生成的 jar 用 java -jar 启动,Spring Boot 自身启动耗时约 0.8 到 1.1 秒(日志里的 Started ... in 1.101 seconds)。Gradle 的构建耗时未在本机测量,不对其做任何数字估计。
一个容易混淆的对照:Maven 的 package 同时包含了编译与打包,Gradle 的 build 则是 assemble 加 check 的聚合任务——build 会跑测试,assemble 只打包。要精确对应 Maven 的「编译 + 打包但不测」,Gradle 侧用 ./gradlew assemble。
C.5 多模块工程的组织差异
当项目从单体拆成多模块时,两种工具的差异会明显放大。
Maven 用父 POM 聚合 + 继承:一个顶层 pom.xml 用 <modules> 列出子模块,子模块用 <parent> 指回顶层,继承依赖管理与插件配置。
<!-- 顶层聚合 pom -->
<modules>
<module>book-domain</module>
<module>book-web</module>
</modules>
<!-- book-web/pom.xml 指回父 -->
<parent>
<groupId>com.example</groupId>
<artifactId>book-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
Gradle 用**settings.gradle 声明成员 + 在根构建脚本里共享配置**,没有继承关系:
// settings.gradle
rootProject.name = 'book'
include 'book-domain'
include 'book-web'
// 根 build.gradle:给所有子项目共享插件与依赖
subprojects {
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
repositories {
mavenCentral()
}
}
差异的本质:Maven 的父子是强继承,子模块默认拿到父的一切,想排除得显式声明;Gradle 是按需配置,父项目默认不向子项目注入任何东西,要用 subprojects {} / allprojects {} 主动共享。后者更灵活,但配置不当会让子项目缺少插件而报错。
| 关注点 | Maven | Gradle |
|---|---|---|
| 成员声明 | 父 POM <modules> | settings.gradle 的 include |
| 配置共享 | <parent> 继承 | subprojects {} / allprojects {} |
| 共享依赖管理 | 父 POM <dependencyManagement> | 根脚本 subprojects 里统一声明 |
| 子模块构建范围 | 从根目录 -pl :module 指定 | ./gradlew :module:build |
| 默认行为 | 子模块继承父配置 | 子模块默认空白 |
C.6 打包产物的差异
两者最终都产出「可执行 jar」,内部结构一致,差别在生成方式与细节开关。
Maven 的机制是两步:maven-jar-plugin 先打出一个普通 jar,spring-boot-maven-plugin 的 repackage goal 再把它重打成可执行 jar,同时把原始 jar 备份为 .jar.original。Gradle 的 bootJar 任务则一步生成可执行 jar,jar 任务仍然产出普通 jar。
可执行 jar 的内部布局两种工具相同:
book-service.jar
├── META-INF/
│ └── MANIFEST.MF # Main-Class 指向 loader,Start-Class 指向你的主类
├── BOOT-INF/
│ ├── classes/ # 你的编译产物
│ └── lib/ # 全部依赖 jar
└── org/springframework/boot/loader/ # 4.x 的 loader 实现
| 关注点 | Maven | Gradle |
|---|---|---|
| 生成任务 | repackage goal(在 package 阶段触发) | bootJar 任务 |
| 是否保留原始 jar | 保留为 .jar.original | jar 任务单独产出普通 jar |
| 产物位置 | target/ | build/libs/ |
| 可选依赖 | 4.x 默认不打入,需 <includeOptional>true</includeOptional> | 由 bootJar 的 includes 控制 |
| war 打包 | <packaging>war</packaging> + repackage | war 插件 + bootWar 任务 |
| 分层打包 | 4.1 起用 tools 模式(layertools 已移除) | bootJar 的 layers 配置 |
要验证产物是否可执行,两种工具生成后都能用同一条命令启动:
java -jar target/book-service-0.0.1-SNAPSHOT.jar # Maven
java -jar build/libs/book-service-0.0.1-SNAPSHOT.jar # Gradle
4.1 移除 layertools 是这里唯一需要专门记的坑:若你的镜像构建脚本依赖 java -Djarmode=layertools -jar app.jar extract,升级到 4.1 后会失败,必须改成 tools 模式。
C.7 该选哪个
给一张决策表,结论不是「谁更好」而是「谁更适合你的场景」。
| 场景 | 建议 | 理由 |
|---|---|---|
| 初学、单模块项目 | Maven | XML 结构直白,父 POM 版本管理省心,官方入门文档默认用它 |
| 团队以 Java 老手为主、追求构建速度 | Gradle | 增量构建与构建缓存更快,Kotlin DSL 有类型检查 |
| 大型多模块、需要复杂条件逻辑 | Gradle | 脚本即代码,subprojects 灵活共享,Maven 的 XML 表达力受限 |
| 与既有 Maven 生态集成 | Maven | 父 POM、dependency:tree 等插件生态成熟 |
| 需要 Gradle 构建缓存 / 远程缓存 | Gradle | 内建缓存机制,CI 提速明显 |
本书正文统一用 Maven,原因很简单:初学阶段把精力放在 Spring 本身,而不是构建脚本的语法上;等你需要 Gradle 时,本附录的对照表能帮你在一两个小时内完成迁移。真要迁移,注意三处:starter 用 4.x 新名(spring-boot-starter-webmvc)、版本管理换成 BOM、可执行 jar 改由 bootJar 生成。
小结
Maven 与 Gradle 在 Spring Boot 里做的是同一件事,差别集中在四层:版本管理(父 POM 继承 vs BOM)、依赖作用域(compile / implementation 语义不同)、多模块组织(强继承 vs 按需共享)、打包任务(repackage vs bootJar)。记住 4.x 的三处变更——可选依赖默认不进 uber jar、layertools 模式被移除、-DskipTests 不再跳过 AOT——它们会直接影响你的构建脚本。再次提醒,本附录 Gradle 示例未在本机实跑,落地前请以官方文档为准;Maven 侧的耗时与启动数据来自本机 Spring Boot 4.1.1 实测。想按症状查构建与启动问题,转 附录 D
;配置项键名回到 附录 B
。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。