第2课:SpringBoot4.x 项目构建:Maven/Gradle配置详解,父工程与依赖管理

文章目录

    • [1. 课前导读](#1. 课前导读)
    • [2. 知识目标](#2. 知识目标)
    • [3. 理论讲解](#3. 理论讲解)
      • [3.1 为什么Spring Boot 4.x要重构依赖体系](#3.1 为什么Spring Boot 4.x要重构依赖体系)
      • [3.2 Maven依赖管理三层模型](#3.2 Maven依赖管理三层模型)
      • [3.3 Spring Boot 4.x模块化对依赖坐标的影响](#3.3 Spring Boot 4.x模块化对依赖坐标的影响)
      • [3.4 Gradle配置的核心变化](#3.4 Gradle配置的核心变化)
    • [4. 环境搭建](#4. 环境搭建)
      • [4.1 Maven方式一:继承spring-boot-starter-parent(推荐单模块项目)](#4.1 Maven方式一:继承spring-boot-starter-parent(推荐单模块项目))
      • [4.2 Maven方式二:导入BOM(推荐多模块项目)](#4.2 Maven方式二:导入BOM(推荐多模块项目))
      • [4.3 Maven多模块项目最佳实践](#4.3 Maven多模块项目最佳实践)
      • [4.4 Gradle配置详解](#4.4 Gradle配置详解)
      • [4.5 使用版本目录(Version Catalog)](#4.5 使用版本目录(Version Catalog))
    • [5. 代码实战](#5. 代码实战)
      • [5.1 完整的多模块项目示例](#5.1 完整的多模块项目示例)
      • [5.2 运行与验证](#5.2 运行与验证)
      • [5.3 依赖冲突排查实战](#5.3 依赖冲突排查实战)
    • [6. 底层源码剖析](#6. 底层源码剖析)
      • [6.1 spring-boot-starter-parent的继承机制](#6.1 spring-boot-starter-parent的继承机制)
      • [6.2 spring-boot-dependencies BOM的结构](#6.2 spring-boot-dependencies BOM的结构)
      • [6.3 spring-boot-maven-plugin的repackage目标](#6.3 spring-boot-maven-plugin的repackage目标)
    • [7. 常见坑点与生产问题排查](#7. 常见坑点与生产问题排查)
    • [8. 本章总结](#8. 本章总结)
    • [9. 课后作业](#9. 课后作业)
  • [🔗 SpringBoot 4.x 从入门到企业级实战系列课程](#🔗 SpringBoot 4.x 从入门到企业级实战系列课程)

本文是《SpringBoot 4.x 从入门到精通》系列第2课,全文约13000字,建议阅读时间50分钟。

环境要求:Spring Boot 4.x,Spring Framework 7,Java 17+(推荐Java 21/25)


1. 课前导读

上一课我们完成了Spring Boot 4.x的HelloWorld程序,相信你已经感受到了4.x的"开箱即用"。但你有没有想过一个问题:为什么pom.xml里不写版本号也能正常工作?为什么spring-boot-starter-webmvc能自动引入Tomcat、Spring MVC、Jackson等一系列依赖?

答案就在项目构建 和依赖管理这两个核心机制里。

Spring Boot 4.0对项目构建体系做了自3.0以来最大的一次重构。spring-boot-starter-web被重命名为spring-boot-starter-webmvc,单一的spring-boot-autoconfigure模块被拆分为多个聚焦的小模块,每个模块都有独立的starter和test-starter。这意味着依赖坐标变了、依赖树变了、父工程的用法也变了。

如果你的企业项目中有多模块架构,或者你有自定义的父工程需要统一管理版本,这节课的内容将直接决定你的项目能否顺利迁移到4.x。

前置知识 :Maven或Gradle基本使用、了解dependencyManagement和dependencies的区别。

学习难点 :理解Spring Boot 4.x模块化对依赖坐标的影响;掌握父工程+BOM+Starter的正确分工;区分spring-boot-starter-parent与spring-boot-dependencies的使用场景。

【配图描述:Spring Boot 4.x 依赖管理三层模型图------Parent(规则继承)、BOM(版本管理)、Starter(功能引入)】


2. 知识目标

学完本节课,你必须掌握以下核心知识点:

  1. Spring Boot 4.x Maven配置完整体系 :spring-boot-starter-parent继承方式、spring-boot-dependencies BOM导入方式、spring-boot-maven-plugin插件的配置与执行。
  2. Spring Boot 4.x Gradle配置 :plugins DSL声明、io.spring.dependency-management插件、platform()BOM导入、版本目录(Version Catalog)的使用。
  3. 模块化Starter的依赖坐标变化 :spring-boot-starter-web→spring-boot-starter-webmvc,各模块test-starter的独立化,classic starter的过渡方案。
  4. 多模块项目中的父工程最佳实践 :父POM使用BOM管理版本、子模块通过starter引入功能、插件放在pluginManagement中按需启用。
  5. 依赖冲突排查与版本覆盖 :mvn dependency:tree的使用、BOM属性的覆盖方式、第三方库版本对齐策略。

3. 理论讲解

3.1 为什么Spring Boot 4.x要重构依赖体系

在理解4.x的依赖管理之前,我们需要先搞清楚一个根本问题:Spring Boot 3.x的依赖体系有什么问题?

Spring Boot 3.x中,所有的自动配置类都集中在一个约2MB的spring-boot-autoconfigure JAR包中。无论你的应用是Web应用、批处理应用还是消息驱动应用,这个JAR包都会被完整加载到classpath中。类加载器需要扫描这个JAR包中所有的AutoConfiguration.imports文件,然后根据条件注解逐一判断哪些配置类需要生效。

这带来了三个问题:

启动开销。 即使你的应用只用了Web和JDBC,JPA、Security、Redis、Kafka等模块的自动配置类仍然会被加载到JVM中,只是不生效而已。类加载和条件判断都消耗了启动时间。

内存浪费。 未使用的自动配置类及其依赖的类仍然占用元空间(Metaspace)内存。

模块耦合。 第三方Starter如果想提供自动配置,必须依赖整个spring-boot-autoconfigure模块,即使它只需要其中的一两个注解。

Spring Boot 4.0的模块化重构正是为了解决这些问题。将spring-boot-autoconfigure拆分为spring-boot-webmvc、spring-boot-jdbc、spring-boot-data-jpa等多个聚焦模块后,应用只加载实际引入的模块中的自动配置类。

【配图描述:Spring Boot 3.x单体autoconfigure vs 4.x模块化autoconfigure的类加载范围对比图】

3.2 Maven依赖管理三层模型

Spring Boot的Maven依赖管理可以抽象为三个层次,理解这个模型是掌握4.x项目构建的关键。

第一层:Parent(规则继承)。 spring-boot-starter-parent是Spring Boot提供的"约定优于配置"的体现。它定义了Java编译版本、UTF-8编码、资源过滤规则、插件配置等构建规范。继承Parent意味着你的项目自动获得了这些默认配置,不需要在每个项目中重复编写。

第二层:BOM(版本管理)。 spring-boot-dependencies是一个BOM(Bill of Materials),只做一件事:管理依赖的版本 。它不引入任何JAR包,只是声明了"如果使用这个依赖,应该用哪个版本"。Parent内部自动导入了这个BOM,所以继承Parent的项目可以省略<version>标签。

第三层:Starter(功能引入)。 spring-boot-starter-webmvc等Starter才是真正"引入JAR包并启用功能"的地方。一个Starter通常会引入多个依赖(如Spring MVC、Tomcat、Jackson)并触发相应的自动配置。

这三层的关系可以用一句话概括:Parent管规则,BOM管版本,Starter管功能 。在Spring Boot 4.x的多模块项目中,推荐的做法是------父POM通过dependencyManagement导入BOM来管理版本,子模块依赖Starter来启用功能,插件放在pluginManagement中按需启用。

3.3 Spring Boot 4.x模块化对依赖坐标的影响

这是4.x项目构建中最容易踩坑的地方。模块化重构直接改变了Starter的坐标名称。

Web模块的变化。 在3.x中,Web项目依赖spring-boot-starter-web。在4.x中,这个Starter被重命名为spring-boot-starter-webmvc。对应的测试依赖也从spring-boot-starter-test变为spring-boot-starter-webmvc-test。

Test Starter的独立化。 3.x中有一个统一的spring-boot-starter-test,包含JUnit、Mockito、AssertJ等所有测试工具。4.x中,每个功能模块都有独立的test-starter,例如spring-boot-starter-webmvc-test、spring-boot-starter-data-jpa-test、spring-boot-starter-security-test。这意味着测试依赖更加精准------测试Web层不需要引入JPA的测试工具。

Classic Starter过渡方案。 为了兼容大量现有项目,Spring Boot 4.0提供了"Classic Starter POMs"。这些Classic Starter包含对应模块的所有依赖,但排除了所有传递依赖。如果你在升级现有项目时不想逐个调整依赖坐标,可以暂时使用Classic Starter,但这只是一种过渡方案。

【配图描述:Spring Boot 3.x vs 4.x Starter坐标对照表】

3.4 Gradle配置的核心变化

Gradle用户同样需要关注4.x的变化。Spring Boot 4.x的Gradle插件版本同步升级到4.x,要求Gradle 7.6.4+或8.4+。

在Gradle中,Spring Boot的依赖管理有两种方式:

方式一:使用io.spring.dependency-management插件。 这是传统方式,插件会自动导入Spring Boot的BOM,使得依赖声明可以省略版本号。

方式二:使用platform()导入BOM。 这是Gradle原生方式,通过implementation platform("org.springframework.boot:spring-boot-dependencies:4.0.6")来导入BOM。Spring Boot官方推荐在新项目中采用这种方式,因为它不依赖额外插件,更加透明。

此外,Gradle用户应该关注版本目录(Version Catalog) 的用法。4.x的模块化使得每个功能模块有独立的starter坐标,使用版本目录可以在libs.versions.toml中统一定义所有Starter别名,避免在build.gradle中硬编码坐标。


4. 环境搭建

4.1 Maven方式一:继承spring-boot-starter-parent(推荐单模块项目)

这是最简单的方式,适用于单模块项目。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 继承 Spring Boot 4.x Parent,获得构建默认配置 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.8</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>single-module-app</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <name>single-module-app</name>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <!-- Spring Boot 4.x Web MVC Starter(替代3.x的starter-web) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>

        <!-- 测试依赖(4.x独立化的test-starter) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

关键点 :继承Parent后,spring-boot-starter-webmvc和spring-boot-starter-webmvc-test都不需要写<version>标签 。Parent自动导入了spring-boot-dependencies BOM,所有Spring Boot管理的依赖版本都由BOM决定。

4.2 Maven方式二:导入BOM(推荐多模块项目)

如果你的企业已有自己的父工程,不能继承spring-boot-starter-parent,可以使用BOM导入方式。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 企业自有父工程 -->
    <parent>
        <groupId>com.example</groupId>
        <artifactId>company-parent</artifactId>
        <version>2.0.0</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>standalone-app</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <properties>
        <java.version>21</java.version>
        <spring-boot.version>4.0.8</spring-boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- 导入 Spring Boot BOM,只管理版本,不引入依赖 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

注意 :使用这种方式时,需要手动配置spring-boot-maven-plugin的版本(通常放在父工程的pluginManagement中统一管理)。同时,Java编译版本、UTF-8编码等构建配置也需要手动声明,因为不再继承Spring Boot Parent。

4.3 Maven多模块项目最佳实践

这是企业项目中最重要的配置模式。假设你有一个多模块项目,结构如下:

复制代码
company-project/
├── pom.xml              (父POM:只做版本管理和插件管理)
├── common/              (通用模块:不含Spring Boot功能)
│   └── pom.xml
├── api/                 (API模块:DTO和接口定义)
│   └── pom.xml
└── web/                 (Web模块:Spring Boot应用)
    └── pom.xml

父POM(company-project/pom.xml)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>company-project</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>common</module>
        <module>api</module>
        <module>web</module>
    </modules>

    <properties>
        <java.version>21</java.version>
        <spring-boot.version>4.0.8</spring-boot.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- 导入 Spring Boot BOM -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- 内部模块版本管理 -->
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>common</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>api</artifactId>
                <version>${project.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <pluginManagement>
            <plugins>
                <!-- 插件放在 pluginManagement 中,子模块按需启用 -->
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <version>${spring-boot.version}</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.13.0</version>
                    <configuration>
                        <release>${java.version}</release>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

Web子模块(web/pom.xml)

xml 复制代码
<project ...>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>company-project</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>web</artifactId>

    <dependencies>
        <!-- 内部模块依赖 -->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>common</artifactId>
        </dependency>
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>api</artifactId>
        </dependency>

        <!-- Spring Boot Starter(版本由父POM的BOM管理) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- 只有web模块需要打包成可执行JAR -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

核心原则 :父POM用BOM定版本,子模块用Starter启功能,插件放在pluginManagement中按需启用。父POM不应该放在<plugins>中 ,否则所有子模块(包括common和api)都会执行repackage,导致"全家一起打包成Boot应用"的事故。

4.4 Gradle配置详解

build.gradle(Groovy DSL)

groovy 复制代码
plugins {
    id 'java'
    id 'org.springframework.boot' version '4.0.8'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '1.0.0-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // Spring Boot 4.x Web MVC Starter
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'

    // 测试 Starter
    testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

Gradle原生BOM导入方式(推荐)

如果你不想使用io.spring.dependency-management插件,可以使用Gradle原生的platform():

groovy 复制代码
plugins {
    id 'java'
    id 'org.springframework.boot' version '4.0.8'
}

dependencies {
    // 导入 Spring Boot BOM
    implementation platform('org.springframework.boot:spring-boot-dependencies:4.0.8')

    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
}

Gradle多模块项目配置

根项目settings.gradle:

groovy 复制代码
rootProject.name = 'company-project'
include 'common'
include 'api'
include 'web'

根项目build.gradle:

groovy 复制代码
plugins {
    id 'java' apply false
    id 'org.springframework.boot' version '4.0.8' apply false
}

subprojects {
    apply plugin: 'java'

    java {
        toolchain {
            languageVersion = JavaLanguageVersion.of(21)
        }
    }

    repositories {
        mavenCentral()
    }

    dependencies {
        implementation platform('org.springframework.boot:spring-boot-dependencies:4.0.8')
    }
}

web/build.gradle:

groovy 复制代码
plugins {
    id 'org.springframework.boot'
}

dependencies {
    implementation project(':common')
    implementation project(':api')
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
}

4.5 使用版本目录(Version Catalog)

Gradle 7.0+引入了版本目录特性,在Spring Boot 4.x的多模块项目中尤为有用。

gradle/libs.versions.toml:

toml 复制代码
[versions]
spring-boot = "4.0.8"
java = "21"

[libraries]
spring-boot-webmvc = { module = "org.springframework.boot:spring-boot-starter-webmvc" }
spring-boot-webmvc-test = { module = "org.springframework.boot:spring-boot-starter-webmvc-test" }

[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }

build.gradle中使用:

groovy 复制代码
plugins {
    alias(libs.plugins.spring.boot)
}

dependencies {
    implementation platform("org.springframework.boot:spring-boot-dependencies:${libs.versions.spring.boot.get()}")
    implementation libs.spring.boot.webmvc
    testImplementation libs.spring.boot.webmvc.test
}

5. 代码实战

5.1 完整的多模块项目示例

让我们创建一个完整的可运行多模块项目,包含通用工具模块、API模块和Web模块。

项目结构

复制代码
multi-module-demo/
├── pom.xml
├── common/
│   ├── pom.xml
│   └── src/main/java/com/example/common/
│       └── util/StringUtils.java
├── api/
│   ├── pom.xml
│   └── src/main/java/com/example/api/
│       └── dto/UserDTO.java
└── web/
    ├── pom.xml
    └── src/main/
        ├── java/com/example/web/
        │   ├── MultiModuleApplication.java
        │   └── controller/UserController.java
        └── resources/application.yml

common模块的StringUtils.java

java 复制代码
package com.example.common.util;

/**
 * 通用字符串工具类。
 * 该模块不依赖 Spring Boot,只包含纯 Java 逻辑。
 */
public final class StringUtils {

    private StringUtils() {}

    /**
     * 判断字符串是否为空白(null、空串、仅空格)。
     */
    public static boolean isBlank(String str) {
        return str == null || str.trim().isEmpty();
    }

    /**
     * 反转字符串,null 安全。
     */
    public static String reverse(String str) {
        if (str == null) return null;
        return new StringBuilder(str).reverse().toString();
    }
}

api模块的UserDTO.java

java 复制代码
package com.example.api.dto;

import java.time.LocalDateTime;

/**
 * 用户数据传输对象。
 * 该模块只包含 DTO 定义,被 web 模块引用。
 */
public class UserDTO {

    private Long id;
    private String name;
    private String email;
    private LocalDateTime createdAt;

    // 无参构造器
    public UserDTO() {}

    public UserDTO(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.createdAt = LocalDateTime.now();
    }

    // getter / setter 省略,实际代码中需完整实现
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}

web模块的启动类

java 复制代码
package com.example.web;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * 多模块 Web 应用启动入口。
 * 
 * 注意:@SpringBootApplication 的扫描范围是 com.example.web 及子包。
 * common 和 api 模块的类需要通过 @Import 或 @ComponentScan 显式引入。
 */
@SpringBootApplication
public class MultiModuleApplication {
    public static void main(String[] args) {
        SpringApplication.run(MultiModuleApplication.class, args);
    }
}

web模块的Controller

java 复制代码
package com.example.web.controller;

import com.example.api.dto.UserDTO;
import com.example.common.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 用户管理接口,演示多模块依赖。
 */
@RestController
@RequestMapping("/api/users")
public class UserController {

    private final ConcurrentHashMap<Long, UserDTO> store = new ConcurrentHashMap<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    /**
     * 创建用户。
     * POST /api/users
     * Body: {"name":"张三","email":"zhangsan@example.com"}
     */
    @PostMapping
    public UserDTO create(@RequestBody UserDTO user) {
        if (StringUtils.isBlank(user.getName())) {
            throw new IllegalArgumentException("用户名不能为空");
        }
        long id = idGenerator.getAndIncrement();
        UserDTO saved = new UserDTO(id, user.getName(), user.getEmail());
        store.put(id, saved);
        return saved;
    }

    /**
     * 获取所有用户。
     * GET /api/users
     */
    @GetMapping
    public List<UserDTO> list() {
        return List.copyOf(store.values());
    }

    /**
     * 根据 ID 获取用户。
     * GET /api/users/{id}
     */
    @GetMapping("/{id}")
    public UserDTO getById(@PathVariable Long id) {
        UserDTO user = store.get(id);
        if (user == null) {
            throw new IllegalArgumentException("用户不存在: " + id);
        }
        return user;
    }

    /**
     * 演示 StringUtils 的使用。
     * GET /api/users/reverse?input=hello
     */
    @GetMapping("/reverse")
    public String reverse(@RequestParam String input) {
        return StringUtils.reverse(input);
    }
}

5.2 运行与验证

编译整个项目

bash 复制代码
cd multi-module-demo
mvn clean install

启动Web模块

bash 复制代码
cd web
mvn spring-boot:run

测试接口

bash 复制代码
# 创建用户
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"张三","email":"zhangsan@example.com"}'
# 输出: {"id":1,"name":"张三","email":"zhangsan@example.com","createdAt":"2025-12-01T10:30:00"}

# 获取所有用户
curl http://localhost:8080/api/users
# 输出: [{"id":1,"name":"张三","email":"zhangsan@example.com","createdAt":"..."}]

# 字符串反转
curl "http://localhost:8080/api/users/reverse?input=hello"
# 输出: olleh

5.3 依赖冲突排查实战

假设项目中出现了一个依赖冲突场景:common模块引入了commons-lang3:3.12.0,而Spring Boot BOM管理的是3.14.0。运行mvn dependency:tree可以看到:

复制代码
[INFO] com.example:web:jar:1.0.0-SNAPSHOT
[INFO] +- com.example:common:jar:1.0.0-SNAPSHOT
[INFO] |  \- org.apache.commons:commons-lang3:jar:3.12.0:compile
[INFO] +- org.springframework.boot:spring-boot-starter-webmvc:jar:4.0.8:compile
[INFO] |  \- (org.apache.commons:commons-lang3:jar:3.14.0:compile - omitted for duplicate)

由于Maven的"最近优先"规则,commons-lang3:3.12.0被选中,与Spring Boot管理的版本不一致。解决方案是在父POM中覆盖BOM属性:

xml 复制代码
<properties>
    <commons-lang3.version>3.14.0</commons-lang3.version>
</properties>

或者在dependencyManagement中显式声明:

xml 复制代码
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.14.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

6. 底层源码剖析

6.1 spring-boot-starter-parent的继承机制

spring-boot-starter-parent的pom.xml本身非常简洁,它的核心作用是通过继承链将构建配置传递给子项目。

xml 复制代码
<!-- spring-boot-starter-parent-4.0.8.pom 核心内容 -->
<project>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>4.0.8</version>
        <relativePath>../spring-boot-dependencies</relativePath>
    </parent>

    <artifactId>spring-boot-starter-parent</artifactId>
    <packaging>pom</packaging>

    <properties>
        <java.version>17</java.version>
        <maven.compiler.release>${java.version}</maven.compiler.release>
        <resource.delimiter>@</resource.delimiter>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <pluginManagement>
            <plugins>
                <!-- 配置 spring-boot-maven-plugin 的默认执行 -->
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <executions>
                        <execution>
                            <id>repackage</id>
                            <goals>
                                <goal>repackage</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

关键点:spring-boot-starter-parent继承了spring-boot-dependencies,因此子项目继承Parent后自动获得了所有依赖版本管理。同时,Parent在pluginManagement中定义了spring-boot-maven-plugin的默认执行(repackage),子项目只需要在<plugins>中声明插件即可触发可执行JAR打包。

6.2 spring-boot-dependencies BOM的结构

spring-boot-dependencies是一个纯粹的BOM,它的结构如下:

xml 复制代码
<project>
    <artifactId>spring-boot-dependencies</artifactId>
    <packaging>pom</packaging>

    <properties>
        <spring-framework.version>7.0.9</spring-framework.version>
        <jackson-bom.version>3.0.0</jackson-bom.version>
        <tomcat.version>11.0.0</tomcat.version>
        <hikaricp.version>7.0.0</hikaricp.version>
        <!-- ... 数百个版本属性 -->
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- Spring Framework BOM -->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-framework-bom</artifactId>
                <version>${spring-framework.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- Jackson 3 BOM -->
            <dependency>
                <groupId>tools.jackson</groupId>
                <artifactId>jackson-bom</artifactId>
                <version>${jackson-bom.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- Spring Boot Starter 的版本声明 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-webmvc</artifactId>
                <version>${project.version}</version>
            </dependency>
            <!-- ... -->
        </dependencies>
    </dependencyManagement>
</project>

<dependencyManagement>中的依赖声明不会引入JAR包 ,只是告诉Maven"如果项目使用了这个依赖,应该用这个版本"。当子项目的<dependencies>中声明了spring-boot-starter-webmvc且没有指定版本时,Maven会从dependencyManagement中查找版本。

6.3 spring-boot-maven-plugin的repackage目标

spring-boot-maven-plugin的核心功能是repackage目标,它将Maven生成的普通JAR重新打包为可执行的Spring Boot JAR。在4.x中,repackage的执行流程与3.x基本一致:

java 复制代码
// 源码路径:org.springframework.boot.maven.RepackageMojo
public class RepackageMojo extends AbstractPackagerMojo {

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        // 1. 获取原始 JAR 文件
        File source = this.project.getArtifact().getFile();
        // 2. 创建可执行 JAR
        File target = getTargetFile();
        // 3. 执行重新打包
        repackage(source, target);
        // 4. 替换原始 JAR
        // ...
    }
}

4.x的关键变化在于分层打包(Layered JAR)的默认启用 。分层打包将应用代码和依赖库分离到不同的层中,Docker构建时可以复用依赖层,显著加快镜像构建速度。分层信息写入JAR中的layers.idx文件。


7. 常见坑点与生产问题排查

坑点1:spring-boot-starter-web 在4.x中已重命名

报错现象 :使用spring-boot-starter-web时IDE提示依赖已废弃,或无法找到某些自动配置类。

原因 :Spring Boot 4.0将spring-boot-starter-web重命名为spring-boot-starter-webmvc,对应的测试Starter也从spring-boot-starter-test变为spring-boot-starter-webmvc-test。

解决方案 :将依赖坐标改为spring-boot-starter-webmvc和spring-boot-starter-webmvc-test。

坑点2:多模块项目中父POM误用plugins导致repackage全家桶

报错现象 :common和api模块也被打包成可执行JAR,启动时报"no main manifest attribute"。

原因 :spring-boot-maven-plugin被放在父POM的<plugins>中(而非<pluginManagement>),导致所有子模块都继承了repackage的执行。

解决方案 :将插件移到父POM的<pluginManagement>中,只在需要打包成Boot应用的子模块(如web)的<plugins>中声明。

坑点3:BOM导入后仍需要手动指定版本

报错现象 :导入spring-boot-dependencies BOM后,依赖仍然报"version is missing"。

原因 :<scope>import</scope>必须配合<type>pom</type>使用,且必须放在<dependencyManagement>中。如果放在<dependencies>中,BOM本身会被当作依赖引入,但版本管理不生效。

解决方案 :确保BOM依赖声明在<dependencyManagement>中,包含<type>pom</type>和<scope>import</scope>。

坑点4:Jackson 3包名迁移导致自定义配置编译失败

报错现象 :import com.fasterxml.jackson.databind.ObjectMapper;编译失败。

原因 :Spring Boot 4.0默认使用Jackson 3,包名从com.fasterxml.jackson迁移到tools.jackson。

解决方案 :将自定义Jackson配置的import改为tools.jackson.databind.ObjectMapper。

坑点5:Gradle版本过低导致插件不兼容

报错现象 :Plugin [id: 'org.springframework.boot', version: '4.0.8'] was not found。

原因:Spring Boot 4.x的Gradle插件要求Gradle 7.6.4+或8.4+。

解决方案 :升级Gradle版本,或在gradle/wrapper/gradle-wrapper.properties中修改distributionUrl。

坑点6:第三方Starter未适配4.x模块化

报错现象:引入第三方Starter(如某些国产ORM框架的Spring Boot Starter)后,自动配置不生效。

原因 :第三方Starter可能依赖了spring-boot-autoconfigure的内部类,而这些类在4.x中变为package-private或移动了位置。

解决方案:检查第三方Starter是否已发布4.x兼容版本;如果还没有,暂时使用Classic Starter过渡,或联系维护者更新。


8. 本章总结

本节课围绕Spring Boot 4.x的项目构建和依赖管理,从Maven和Gradle两个维度构建了完整的知识体系。

Maven三层模型 :Parent管规则(构建配置、插件默认值),BOM管版本(spring-boot-dependencies),Starter管功能(引入依赖和自动配置)。单模块项目推荐继承spring-boot-starter-parent,多模块项目推荐父POM导入BOM + 子模块依赖Starter。

4.x模块化变化 :spring-boot-starter-web→spring-boot-starter-webmvc,Test Starter独立化为各模块的-test后缀,Classic Starter作为过渡方案保留。自动配置类从单一spring-boot-autoconfigure模块分散到各功能模块中。

Gradle配置 :Spring Boot 4.x插件要求Gradle 7.6.4+,推荐使用platform()导入BOM,版本目录(Version Catalog)在多模块项目中管理Starter坐标。

多模块最佳实践 :父POM用BOM定版本,子模块用Starter启功能,插件放在pluginManagement中按需启用。父POM不应放在<plugins>中,避免"全家桶repackage"事故。


9. 课后作业

基础题1

创建一个单模块Spring Boot 4.x项目,分别使用以下两种方式管理依赖版本:

  • 方式A:继承spring-boot-starter-parent
  • 方式B:导入spring-boot-dependencies BOM

对比两种方式的pom.xml差异,运行mvn dependency:tree,记录输出结果,解释为什么方式A可以省略spring-boot-maven-plugin的版本号。

基础题2

使用Gradle创建一个Spring Boot 4.x项目,分别使用io.spring.dependency-management插件和platform()导入BOM。观察gradle dependencies输出的差异,记录BOM是否被正确导入。

综合实战题

创建一个完整的多模块Maven项目,包含以下三个模块:

  1. common模块 :包含DateUtils工具类,提供format(LocalDateTime)方法返回yyyy-MM-dd HH:mm:ss格式字符串,不依赖Spring Boot。
  2. api模块 :包含OrderDTO类,字段包括orderId(String)、amount(BigDecimal)、createdAt(LocalDateTime),依赖common模块。
  3. web模块 :Spring Boot 4.x应用,依赖common和api模块,提供REST接口:
    • POST /api/orders:创建订单,返回OrderDTO
    • GET /api/orders:返回所有订单列表
    • GET /api/orders/{id}/time:返回订单创建时间的格式化字符串(使用DateUtils)

要求:

  • 父POM使用BOM导入方式管理Spring Boot版本
  • 父POM的插件配置放在pluginManagement中
  • 只有web模块声明spring-boot-maven-plugin在<plugins>中
  • 使用mvn clean install编译整个项目
  • 使用mvn dependency:tree检查依赖树,确保没有版本冲突
  • 启动web模块,使用curl测试所有接口

扩展挑战 :在父POM中覆盖commons-lang3的版本为3.14.0,在common模块中添加一个使用StringUtils.isBlank()的方法,验证版本覆盖是否生效。

🔗 SpringBoot 4.x 从入门到企业级实战系列课程

去订阅

相关推荐
譕痕1 小时前
Rabbit 实例
java·开发语言·java-rabbitmq
weixin_448771721 小时前
使用Aspose给PDF添加水印
java·pdf
\光辉岁月/1 小时前
7.system类
java·system类
SL_staff1 小时前
BI落地失败的3个技术断点:从数据接入、自助分析到服务闭环的工程实践
java·数据分析·数据可视化
大风起兮云飞扬丶1 小时前
Java——线程池
java·开发语言
文丑颜不良啊2 小时前
LeetCode276~300题目解析
java·leetcode
Vegetable_xu2 小时前
Spring Bean 循环依赖:为什么有时能过、有时启动就挂
java·spring·mybatis
avi91113 小时前
【AI教人】在Inspector显示所有Delegate-Odin Inspector之三
java·前端·算法·delegate·inspector·editor扩展·unity editor
青春不朽5123 小时前
Python 的 list append 为什么是均摊 O(1)?拆开过度分配那张表,看容量翻倍背后的暗账
java·python·list