Spring Boot基础-2:Spring Boot 3.x 起步依赖(Starter)深度拆解:为什么引入一个依赖就够了?

📌 系列说明 :本文是《Spring Boot从入门到底层原理》20篇系列的第二篇

  • 前置知识:已完成第一篇《用5分钟搭建第一个REST API应用》,能独立创建并运行Spring Boot应用
  • 核心目标:彻底理解Starter工作机制,掌握依赖分析技能,能自定义Starter
  • 版本基准:Spring Boot 3.5.x + JDK 17+
  • 预计耗时 :阅读30分钟 + 实操60分钟, 代码亲自测试通过

一、什么是Starter?为什么它如此重要?

1.1 从痛点说起:传统Spring的依赖地狱

在Spring Boot出现之前,整合一个功能模块是这样的体验:

场景:整合Spring MVC + Jackson + Tomcat

xml 复制代码
<!-- ❌ 传统Spring方式:需要手动管理十几个依赖 -->
<dependencies>
    <!-- Spring核心 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>5.3.20</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.20</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>5.3.20</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.20</version>
    </dependency>
    
    <!-- JSON处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.13.3</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.13.3</version>
    </dependency>
    
    <!-- Servlet API -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
    </dependency>
    
    <!-- 内嵌Tomcat -->
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-core</artifactId>
        <version>9.0.65</version>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-el</artifactId>
        <version>9.0.65</version>
    </dependency>
    <!-- ... 还有更多 ... -->
</dependencies>

问题

  • 🔴 版本冲突:各依赖版本需要手动对齐,容易冲突
  • 🔴 配置繁琐:每个依赖都需要单独配置
  • 🔴 学习成本高:需要知道整合某个功能需要哪些依赖

1.2 Spring Boot的解决方案:Starter

Spring Boot方式

xml 复制代码
<!-- ✅ Spring Boot方式:一个依赖搞定 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Starter的定义

Starter是一组便捷的依赖描述符,它将某个功能场景所需的所有依赖打包在一起。引入一个Starter,就等于引入了该场景所需的完整依赖集合,且版本经过官方测试,确保兼容。

1.3 Starter的命名规范

Spring Boot官方Starter遵循严格的命名规范:

bash 复制代码
格式:spring-boot-starter-{功能名}

示例:
├── spring-boot-starter-web          # Web开发(Spring MVC + Tomcat)
├── spring-boot-starter-data-jpa     # JPA数据访问
├── spring-boot-starter-data-redis   # Redis集成
├── spring-boot-starter-security     # 安全认证
├── spring-boot-starter-test         # 测试支持
├── spring-boot-starter-actuator     # 生产监控
└── spring-boot-starter-validation   # 参数校验

第三方Starter命名(注意区别):

bash 复制代码
格式:{第三方名}-spring-boot-starter

示例:
├── mybatis-spring-boot-starter      # MyBatis官方Starter
├── druid-spring-boot-starter        # 阿里Druid连接池
└── redisson-spring-boot-starter     # Redisson客户端

⚠️ 重要 :官方Starter以spring-boot-starter-开头,第三方Starter以-spring-boot-starter结尾,这是区分两者的重要标志。


二、Starter依赖传递机制深度分析

2.1 实战:查看spring-boot-starter-web的依赖树

让我们用Maven命令揭开Starter的"黑盒":

步骤1:打开终端,进入项目根目录

bash 复制代码
cd your-spring-boot-project

步骤2:执行依赖树命令

bash 复制代码
mvn dependency:tree -Dincludes=org.springframework.boot:spring-boot-starter-web

步骤3:查看完整依赖树(推荐)

bash 复制代码
mvn dependency:tree > dependencies.txt

然后用文本编辑器打开dependencies.txt,你会看到类似以下内容:

ruby 复制代码
com.example:demo:jar:0.0.1-SNAPSHOT
└─ org.springframework.boot:spring-boot-starter-web:jar:3.2.5:compile
   ├─ org.springframework.boot:spring-boot-starter:jar:3.2.5:compile
   │  ├─ org.springframework.boot:spring-boot:jar:3.2.5:compile
   │  ├─ org.springframework.boot:spring-boot-autoconfigure:jar:3.2.5:compile
   │  ├─ org.springframework.boot:spring-boot-starter-logging:jar:3.2.5:compile
   │  │  ├─ ch.qos.logback:logback-classic:jar:1.4.14:compile
   │  │  ├─ org.apache.logging.log4j:log4j-to-slf4j:jar:2.21.1:compile
   │  │  └─ org.slf4j:jul-to-slf4j:jar:2.0.9:compile
   │  ├─ jakarta.annotation:jakarta.annotation-api:jar:2.1.1:compile
   │  └─ org.yaml:snakeyaml:jar:2.2:compile
   ├─ org.springframework.boot:spring-boot-starter-json:jar:3.2.5:compile
   │  ├─ com.fasterxml.jackson.core:jackson-databind:jar:2.15.4:compile
   │  ├─ com.fasterxml.jackson.core:jackson-annotations:jar:2.15.4:compile
   │  ├─ com.fasterxml.jackson.core:jackson-core:jar:2.15.4:compile
   │  └─ com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.15.4:compile
   ├─ org.springframework.boot:spring-boot-starter-tomcat:jar:3.2.5:compile
   │  ├─ org.apache.tomcat.embed:tomcat-embed-core:jar:10.1.19:compile
   │  ├─ org.apache.tomcat.embed:tomcat-embed-el:jar:10.1.19:compile
   │  └─ org.apache.tomcat.embed:tomcat-embed-websocket:jar:10.1.19:compile
   ├─ org.springframework:spring-web:jar:6.1.6:compile
   │  └─ org.springframework:spring-beans:jar:6.1.6:compile
   └─ org.springframework:spring-webmvc:jar:6.1.6:compile
      ├─ org.springframework:spring-aop:jar:6.1.6:compile
      ├─ org.springframework:spring-context:jar:6.1.6:compile
      └─ org.springframework:spring-expression:jar:6.1.6:compile

2.2 依赖树解读:一个Starter背后隐藏了什么?

从上面的依赖树,我们可以看到spring-boot-starter-web实际引入了5个核心子Starter

bash 复制代码
spring-boot-starter-web
│
├── spring-boot-starter          # Spring Boot核心启动器
│   ├── spring-boot              # 核心库
│   ├── spring-boot-autoconfigure # 自动配置库
│   ├── spring-boot-starter-logging # 日志(Logback)
│   └── jakarta.annotation-api   # 注解API
│
├── spring-boot-starter-json     # JSON处理
│   └── jackson-*                # Jackson系列库
│
├── spring-boot-starter-tomcat   # 内嵌Tomcat
│   └── tomcat-embed-*           # Tomcat核心库
│
├── spring-web                   # Spring Web基础
│
└── spring-webmvc                # Spring MVC核心

2.3 动手实验:验证依赖传递效果

实验目标:证明引入一个Starter后,相关类确实可用

步骤1:创建测试类

src/test/java/com.example.democonsumer下创建DependencyTest.java

java 复制代码
package com.example.democonsumer;  
  
import com.fasterxml.jackson.databind.ObjectMapper;  
import org.apache.catalina.startup.Tomcat;  
import org.junit.jupiter.api.Test;  
import org.slf4j.Logger;  
  
import static org.junit.jupiter.api.Assertions.*;  
  
public class DependencyTest {  
  
    /**  
     * 测试1:验证Jackson依赖是否可用  
     * 预期:无需额外引入jackson依赖,ObjectMapper可直接使用  
     */  
    @Test  
    public void testJacksonAvailable() {  
        // 如果jackson依赖不存在,这行会报ClassNotFoundException  
        ObjectMapper mapper =  new ObjectMapper();  
        assertNotNull(mapper);  
        System.out.println("✅ Jackson 依赖已自动引入");  
    }  
  
    /**  
     * 测试2:验证Tomcat依赖是否可用  
     * 预期:Tomcat类可在classpath中找到  
     */  
    @Test  
    public void testTomcatAvailable() {  
        // 验证Tomcat核心类是否存在  
        Class<?> tomcatClass = Tomcat.class;  
        assertNotNull(tomcatClass);  
        System.out.println("✅ Tomcat 依赖已自动引入");  
    }  
  
    /**  
     * 测试3:验证Logback日志依赖是否可用  
     */  
    @Test  
    public void testLogbackAvailable() {  
        // 验证Logback类是否存在  
        Class<?> loggerClass = Logger.class;  
        assertNotNull(loggerClass);  
        System.out.println("✅ Logback 依赖已自动引入");  
    }  
}

步骤2:运行测试

bash 复制代码
mvn test -Dtest=DependencyTest
//或者直接在 IDEA 编辑器的代码左边右键 点击:run 'DependencyTest'

预期输出

复制代码
✅ Jackson 依赖已自动引入
✅ Tomcat 依赖已自动引入
✅ Logback 依赖已自动引入

💡 结论 :只需引入spring-boot-starter-web,Jackson、Tomcat、Logback等依赖都会自动传递,无需手动配置!


三、Starter自动配置原理(Spring Boot 3.x核心机制)

3.1 核心问题:引入依赖后,Bean是如何自动创建的?

引入Starter只是第一步,真正的魔法在于自动配置。让我们追踪整个过程:

3.2 Spring Boot 3.x 自动配置加载流程

java 复制代码
┌─────────────────────────────────────────────────────────────────┐
│  1. @SpringBootApplication 启动                                  │
│     ↓                                                            │
│  2. @EnableAutoConfiguration 生效                                │
│     ↓                                                            │
│  3. AutoConfigurationImportSelector 加载                         │
│     ↓                                                            │
│  4. 扫描 META-INF/spring/org.springframework.boot.autoconfigure. │
│     AutoConfiguration.imports 文件(Spring Boot 3.x 新方式)      │
│     ↓                                                            │
│  5. 读取所有自动配置类全限定名                                    │
│     ↓                                                            │
│  6. 根据 @Conditional 条件注解过滤                               │
│     ↓                                                            │
│  7. 加载符合条件的配置类,创建 Bean                               │
└─────────────────────────────────────────────────────────────────┘

3.3 关键变化:Spring Boot 2.x vs 3.x

特性 Spring Boot 2.x Spring Boot 3.x
配置文件位置 META-INF/spring.factories META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
配置格式 Key-Value格式 逐行列出类名
处理类 SpringFactoriesLoader ImportCandidates

Spring Boot 2.x 格式(已废弃):

properties 复制代码
# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.demo.autoconfigure.DemoAutoConfiguration,\
com.example.demo.autoconfigure.AnotherAutoConfiguration

Spring Boot 3.x 格式(新标准):

shell 复制代码
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.demo.autoconfigure.DemoAutoConfiguration
com.example.demo.autoconfigure.AnotherAutoConfiguration

3.4 实战:查看spring-boot-autoconfigure中的自动配置类

步骤1:找到spring-boot-autoconfigure的JAR包

bash 复制代码
# Maven仓库位置(Linux/Mac)
~/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/3.5.11/

# Windows
C:\Users\你的用户名\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\3.5.11\

步骤2:用压缩软件打开JAR文件

步骤3:查看自动配置文件

路径:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

你会看到类似内容(部分):

kotlin 复制代码
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
...

3.5 条件注解:自动配置的"开关"

自动配置类不会无条件加载,而是通过@Conditional系列注解进行条件判断:

java 复制代码
@Configuration
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })  // 条件1:类路径存在这些类
@ConditionalOnWebApplication(type = Type.SERVLET)                // 条件2:是Web应用
@ConditionalOnMissingBean({ WebMvcConfigurationSupport.class })  // 条件3:用户没有自定义该Bean
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class WebMvcAutoConfiguration {
    
    @Bean
    public ViewResolver viewResolver() {
        // 只有满足以上所有条件,这个Bean才会被创建
        return new InternalResourceViewResolver();
    }
}

常用条件注解

注解 说明 使用场景
@ConditionalOnClass 类路径存在指定类 检查依赖是否存在
@ConditionalOnMissingBean 容器中不存在指定Bean 避免覆盖用户自定义配置
@ConditionalOnProperty 配置文件存在指定属性 根据配置开关功能
@ConditionalOnWebApplication 是Web应用 Web相关自动配置
@ConditionalOnBean 容器中存在指定Bean 依赖其他Bean存在

四、动手实战:自定义一个Starter

理论讲完了,现在让我们亲手创建一个自定义Starter,彻底理解其工作机制。

4.1 场景设计

我们要创建一个短信发送Starter,功能:

  • 自动配置短信服务Bean
  • 支持配置文件自定义参数
  • 其他项目引入后可直接使用

4.2 项目结构设计

我们将创建一个Maven父项目 ,包含两个子模块

bash 复制代码
custom-starter-demo/                    # 父项目(聚合项目)
├── pom.xml                             # 父项目POM,管理子模块
│
├── sms-spring-boot-starter/            # 子模块1:Starter模块(发布到Maven仓库)
│   └── pom.xml
│
└── demo-consumer/                      # 子模块2:使用Starter的演示项目
    └── pom.xml

💡 为什么需要父项目?

  • 统一版本管理:父项目可以统一管理所有子模块的依赖版本
  • 简化配置:公共配置在父项目定义,子模块继承
  • 一键构建 :在父项目执行mvn install,会自动构建所有子模块
  • 模块依赖:子模块之间可以方便地相互引用

4.3 第一步:创建Maven父项目

步骤1:在IDEA中创建父项目

arduino 复制代码
File → New → Project → 选择 "Maven" → 点击 "Next"

步骤2:填写项目信息

字段
Name custom-starter-demo
Location 选择你的工作目录
GroupId com.example
ArtifactId custom-starter-demo
Version 1.0.0
Packaging pom (关键!父项目必须是pom类型)

步骤3:配置父项目 pom.xml

创建完成后,将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>custom-starter-demo</artifactId>  
    <version>1.0.0</version>  
    <packaging>pom</packaging>  
    <name>Custom Starter Demo Parent</name>  
    <description>Spring Boot自定义Starter演示项目(父项目)</description>  
    <!-- 定义子模块列表 -->  
    <modules>  
        <module>sms-spring-boot-starter</module>  
        <module>demo-consumer</module>  
    </modules>  
    <!-- 统一属性管理 -->  
    <properties>  
        <java.version>17</java.version>  
        <maven.compiler.source>17</maven.compiler.source>  
        <maven.compiler.target>17</maven.compiler.target>  
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>  
        <spring-boot.version>3.5.11</spring-boot.version>  
    </properties>  
    
    <!-- 配置阿里云Maven镜像加速下载 -->  
    <repositories>  
        <repository>            
	        <id>aliyun-public</id>  
            <url>https://maven.aliyun.com/repository/public</url>  
            <releases>                
	            <enabled>true</enabled>  
            </releases>            
            <snapshots>                
	            <enabled>false</enabled>  
            </snapshots>        
        </repository>    
    </repositories>    
    <pluginRepositories>        
	    <pluginRepository>            
		    <id>aliyun-public</id>  
            <url>https://maven.aliyun.com/repository/public</url>  
            <releases>                
	            <enabled>true</enabled>  
            </releases>            
            <snapshots>                
	            <enabled>false</enabled>  
            </snapshots>        
        </pluginRepository>    
    </pluginRepositories> 
     
    <dependencyManagement>        
	    <dependencies>            <!-- Spring Boot依赖管理 -->  
            <dependency>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-dependencies</artifactId>  
                <version>${spring-boot.version}</version>  
                <type>pom</type>  
                <scope>import</scope>  
            </dependency>  
            <!-- 自定义Starter版本管理 -->  
            <dependency>  
                <groupId>com.example</groupId>  
                <artifactId>sms-spring-boot-starter</artifactId>  
                <version>${project.version}</version>  
            </dependency>        
        </dependencies>    
    </dependencyManagement>  
    <dependencies>        
	    <dependency>            
		    <groupId>org.projectlombok</groupId>  
            <artifactId>lombok</artifactId>  
            <version>1.18.44</version> <!-- Changed from 1.18.24 to 1.18.44 -->  
            <optional>true</optional>  
        </dependency>  
        <dependency>            
	        <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-test</artifactId>  
            <version>${spring-boot.version}</version>  
            <scope>test</scope>  
        </dependency>    
    </dependencies>  
    <build>        
	    <pluginManagement>            
	    <plugins>                
		    <plugin>                    
				    <groupId>org.springframework.boot</groupId>  
                    <artifactId>spring-boot-maven-plugin</artifactId>  
                    <version>${spring-boot.version}</version>  
                    <configuration>                        
	                    <excludes>                            
		                    <exclude>     
		                        <groupId>org.projectlombok</groupId>  
	                            <artifactId>lombok</artifactId>  
	                        </exclude>                        
	                    </excludes>
	                </configuration>                
	        </plugin>            
	    </plugins>        
	    </pluginManagement>        
	    <plugins>            
		    <plugin>                
			    <groupId>org.apache.maven.plugins</groupId>  
                <artifactId>maven-compiler-plugin</artifactId>  
                <version>3.13.0</version>  
                <configuration>                    
	                <parameters>true</parameters>  
	                <source>17</source>  
                    <target>17</target>  
                    <encoding>UTF-8</encoding>  
                    <annotationProcessorPaths>                        
	                    <path>
		                    <groupId>org.projectlombok</groupId>  
	                        <artifactId>lombok</artifactId>  
	                        <version>1.18.44</version>  
	                    </path>                    
                    </annotationProcessorPaths>                
	            </configuration>            
	        </plugin>        
	    </plugins>    
	</build>  
</project>

⚠️ 关键点说明

  1. <packaging>pom</packaging> - 父项目必须是pom类型,不能是jar
  2. <modules> - 声明所有子模块,Maven会按顺序构建
  3. <dependencyManagement> - 统一管理依赖版本,子模块继承

4.4 第二步:创建 sms-spring-boot-starter 子模块

步骤1:在父项目下创建子模块

sql 复制代码
右键父项目 → New → Module → 选择 "Maven" → 点击 "Next"

步骤2:填写模块信息

字段
Name sms-spring-boot-starter
GroupId com.example
ArtifactId sms-spring-boot-starter
Version 1.0.0
Packaging jar

步骤3:配置子模块 pom.xml

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>  
    <!-- 继承父项目 -->  
    <parent>  
        <groupId>com.example</groupId>  
        <artifactId>custom-starter-demo</artifactId>  
        <version>1.0.0</version>  
    </parent>  
    <!-- 项目信息 -->  
    <artifactId>sms-spring-boot-starter</artifactId>  
    <packaging>jar</packaging>  
    <name>SMS Spring Boot Starter</name>  
    <description>自定义短信服务Starter</description>  
  
    <dependencies>        <!-- Spring Boot自动配置依赖 -->  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-autoconfigure</artifactId>  
        </dependency>  
        <!-- Spring Boot核心(可选,根据需求) -->  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot</artifactId>  
            <optional>true</optional>  
        </dependency>  
        <!-- 配置处理器(生成配置元数据) -->  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-configuration-processor</artifactId>  
            <optional>true</optional>  
        </dependency>  
        <!-- 日志依赖 -->  
        <dependency>  
            <groupId>org.slf4j</groupId>  
            <artifactId>slf4j-api</artifactId>  
            <optional>true</optional>  
        </dependency>    </dependencies>  
</project>

⚠️ 关键点说明

  1. <parent> - 声明父项目,继承父项目的配置和依赖管理
  2. 依赖无需指定版本号 - 从父项目的dependencyManagement继承
  3. <optional>true</optional> - 标记为可选依赖,不会传递到使用方

4.5 第三步:创建 demo-consumer 子模块

步骤1:再次创建子模块

sql 复制代码
右键父项目 → New → Module → 选择 "Maven" → 点击 "Next"

步骤2:填写模块信息

字段
Name demo-consumer
GroupId com.example
ArtifactId demo-consumer
Version 1.0.0
Packaging jar

步骤3:配置子模块 pom.xml

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>  
    <!-- 继承父项目 -->  
    <parent>  
        <groupId>com.example</groupId>  
        <artifactId>custom-starter-demo</artifactId>  
        <version>1.0.0</version>  
    </parent>  
  
    <artifactId>demo-consumer</artifactId>  
    <packaging>jar</packaging>  
    <name>demo-consumer</name>  
    <description>demo-consumer</description>  
  
    <dependencies>  
        <!-- Spring Boot Web Starter -->  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-web</artifactId>  
        </dependency>  
        <!-- 引入我们的自定义Starter(同一父项目下的子模块) -->  
        <dependency>  
            <groupId>com.example</groupId>  
            <artifactId>sms-spring-boot-starter</artifactId>  
        </dependency>  
        <!-- Spring Boot Actuator(用于查看自动配置报告) -->  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-actuator</artifactId>  
        </dependency>  
        <!-- Spring Boot DevTools(开发热部署) -->  
        <dependency>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-devtools</artifactId>  
            <scope>runtime</scope>  
            <optional>true</optional>  
        </dependency>  
    </dependencies>  
    <build>        
	    <plugins>            
		    <plugin>                
			    <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-maven-plugin</artifactId>  
            </plugin>        
        </plugins>   
    </build>  
</project>

⚠️ 关键点说明

  1. 引入sms-spring-boot-starter无需指定版本号
  2. 同一父项目下的子模块可以直接引用,Maven会自动处理
  3. spring-boot-maven-plugin用于打包可执行JAR

4.6 第四步:创建Starter核心代码

现在在sms-spring-boot-starter模块下创建Java代码。

步骤1:创建包结构

css 复制代码
sms-spring-boot-starter/src/main/java/com/example/sms/

步骤2:创建 SmsProperties.java

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

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * 短信服务配置属性
 * 可通过 application.properties 或 application.yml 配置
 */
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {

    private String provider = "aliyun";
    private String apiKey = "";
    private String apiSecret = "";
    private String signName = "我的应用";
    private boolean enabled = true;

    // Getter 和 Setter
    public String getProvider() { return provider; }
    public void setProvider(String provider) { this.provider = provider; }

    public String getApiKey() { return apiKey; }
    public void setApiKey(String apiKey) { this.apiKey = apiKey; }

    public String getApiSecret() { return apiSecret; }
    public void setApiSecret(String apiSecret) { this.apiSecret = apiSecret; }

    public String getSignName() { return signName; }
    public void setSignName(String signName) { this.signName = signName; }

    public boolean isEnabled() { return enabled; }
    public void setEnabled(boolean enabled) { this.enabled = enabled; }

    @Override
    public String toString() {
        return "SmsProperties{" +
                "provider='" + provider + '\'' +
                ", apiKey='" + apiKey + '\'' +
                ", apiSecret='***'" +
                ", signName='" + signName + '\'' +
                ", enabled=" + enabled +
                '}';
    }
}

步骤3:创建 SmsService.java

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

import java.util.List;

/**
 * 短信服务接口
 */
public interface SmsService {

    SendResult send(String phone, String message);

    List<SendResult> batchSend(List<String> phones, String message);
}

步骤4:创建 SendResult.java

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

/**
 * 短信发送结果
 */
public class SendResult {

    private boolean success;
    private String messageId;
    private String phone;
    private String errorMessage;

    public SendResult() {}

    public SendResult(boolean success, String phone) {
        this.success = success;
        this.phone = phone;
    }

    public SendResult(boolean success, String messageId, String phone, String errorMessage) {
        this.success = success;
        this.messageId = messageId;
        this.phone = phone;
        this.errorMessage = errorMessage;
    }

    // Getter 和 Setter
    public boolean isSuccess() { return success; }
    public void setSuccess(boolean success) { this.success = success; }

    public String getMessageId() { return messageId; }
    public void setMessageId(String messageId) { this.messageId = messageId; }

    public String getPhone() { return phone; }
    public void setPhone(String phone) { this.phone = phone; }

    public String getErrorMessage() { return errorMessage; }
    public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }

    @Override
    public String toString() {
        return "SendResult{" +
                "success=" + success +
                ", messageId='" + messageId + '\'' +
                ", phone='" + phone + '\'' +
                ", errorMessage='" + errorMessage + '\'' +
                '}';
    }
}

步骤5:创建 DefaultSmsService.java

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * 默认短信服务实现
 */
public class DefaultSmsService implements SmsService {

    private static final Logger log = LoggerFactory.getLogger(DefaultSmsService.class);

    private final SmsProperties properties;

    public DefaultSmsService(SmsProperties properties) {
        this.properties = properties;
        log.info("===========================================");
        log.info("初始化短信服务");
        log.info("提供商:{}", properties.getProvider());
        log.info("签名:{}", properties.getSignName());
        log.info("启用状态:{}", properties.isEnabled());
        log.info("===========================================");
    }

    @Override
    public SendResult send(String phone, String message) {
        log.info("【短信发送】开始发送短信");
        log.info("目标手机号:{}", phone);
        log.info("短信内容:{}", message);
        
        String messageId = UUID.randomUUID().toString();
        SendResult result = new SendResult(true, messageId, phone, null);
        
        log.info("【短信发送】发送成功,消息ID:{}", messageId);
        return result;
    }

    @Override
    public List<SendResult> batchSend(List<String> phones, String message) {
        log.info("【批量短信】开始批量发送,总数:{}", phones.size());
        
        List<SendResult> results = new ArrayList<>();
        for (String phone : phones) {
            results.add(send(phone, message));
        }
        
        log.info("【批量短信】发送完成,成功:{}/{}", 
                results.stream().filter(SendResult::isSuccess).count(), 
                results.size());
        return results;
    }
}

步骤6:创建 SmsTemplateManager.java

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 短信模板管理器
 */
public class SmsTemplateManager {

    private static final Logger log = LoggerFactory.getLogger(SmsTemplateManager.class);

    private final Map<String, String> templates = new HashMap<>();

    public SmsTemplateManager() {
        registerDefaultTemplates();
        log.info("短信模板管理器初始化完成,已加载 {} 个默认模板", templates.size());
    }

    private void registerDefaultTemplates() {
        templates.put("verify_code", "您的验证码是:${code},${minute}分钟内有效");
        templates.put("notify", "【${sign}】${content}");
        templates.put("marketing", "【${sign}】${title},${content} 退订回T");
        templates.put("logistics", "【${sign}】您的快递已${status},单号:${trackingNo}");
    }

    public String render(String templateName, Map<String, Object> params) {
        String template = templates.get(templateName);
        if (template == null) {
            throw new IllegalArgumentException("模板不存在:" + templateName);
        }
        
        String result = template;
        if (params != null) {
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                result = result.replace("${" + entry.getKey() + "}", String.valueOf(entry.getValue()));
            }
        }
        
        return result;
    }

    public void registerTemplate(String name, String content) {
        templates.put(name, content);
    }

    public Set<String> getTemplateNames() {
        return templates.keySet();
    }

    public boolean hasTemplate(String templateName) {
        return templates.containsKey(templateName);
    }
}

步骤7:创建 SmsAutoConfiguration.java(核心)

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 短信服务自动配置类
 */
@Configuration
@ConditionalOnClass(SmsService.class)
@EnableConfigurationProperties(SmsProperties.class)
@ConditionalOnProperty(prefix = "sms", name = "enabled", havingValue = "true", matchIfMissing = true)
public class SmsAutoConfiguration {

    private static final Logger log = LoggerFactory.getLogger(SmsAutoConfiguration.class);

    @Bean
    @ConditionalOnMissingBean(SmsService.class)
    public SmsService smsService(SmsProperties properties) {
        log.info("===========================================");
        log.info("【自动配置】正在创建 SmsService Bean");
        log.info("配置信息:{}", properties);
        log.info("===========================================");
        return new DefaultSmsService(properties);
    }

    @Bean
    @ConditionalOnMissingBean(name = "smsTemplateManager")
    public SmsTemplateManager smsTemplateManager() {
        log.info("【自动配置】正在创建 SmsTemplateManager Bean");
        return new SmsTemplateManager();
    }
}

步骤8:创建自动配置注册文件(关键)

创建目录和文件:

bash 复制代码
sms-spring-boot-starter/src/main/resources/META-INF/spring/

文件:org.springframework.boot.autoconfigure.AutoConfiguration.imports

内容:

复制代码
com.example.sms.SmsAutoConfiguration

⚠️ 重要提示

  • 文件路径必须完全正确(Spring Boot 3.x 新格式)
  • 每行一个自动配置类的全限定名
  • 不要有额外的空格或空行
  • 文件编码使用 UTF-8

4.7 第五步:创建消费者项目代码

现在在demo-consumer模块下创建使用Starter的代码。

步骤1:创建启动类

路径:demo-consumer/src/main/java/com/example/consumer/DemoConsumerApplication.java

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

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

@SpringBootApplication
public class DemoConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoConsumerApplication.class, args);
    }
}

步骤2:创建配置文件

路径:demo-consumer/src/main/resources/application.properties

properties 复制代码
# ===========================================
# 短信服务配置
# ===========================================
sms.provider=aliyun
sms.api-key=test-api-key-12345
sms.api-secret=test-api-secret-67890
sms.sign-name=测试应用
sms.enabled=true

# ===========================================
# 服务器配置
# ===========================================
server.port=8080

# ===========================================
# Actuator 监控配置
# ===========================================
management.endpoints.web.exposure.include=*

# ===========================================
# 日志配置
# ===========================================
logging.level.com.example.sms=DEBUG
logging.level.com.example.consumer=DEBUG

步骤3:创建 SmsController.java

路径:demo-consumer/src/main/java/com/example/consumer/controller/SmsController.java

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

import com.example.sms.SendResult;
import com.example.sms.SmsService;
import com.example.sms.SmsTemplateManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/sms")
public class SmsController {

    @Autowired
    private SmsService smsService;

    @Autowired
    private SmsTemplateManager templateManager;

    @GetMapping("/send")
    public SendResult sendSms(@RequestParam String phone, 
                              @RequestParam String message) {
        return smsService.send(phone, message);
    }

    @PostMapping("/batch")
    public List<SendResult> batchSend(@RequestBody BatchSendRequest request) {
        return smsService.batchSend(request.getPhones(), request.getMessage());
    }

    @GetMapping("/template/verify")
    public SendResult sendVerifyCode(@RequestParam String phone, 
                                     @RequestParam String code) {
        Map<String, Object> params = new HashMap<>();
        params.put("code", code);
        params.put("minute", 5);
        
        String message = templateManager.render("verify_code", params);
        return smsService.send(phone, message);
    }

    @GetMapping("/templates")
    public List<String> getTemplates() {
        return templateManager.getTemplateNames().stream().toList();
    }

    public static class BatchSendRequest {
        private List<String> phones;
        private String message;

        public List<String> getPhones() { return phones; }
        public void setPhones(List<String> phones) { this.phones = phones; }

        public String getMessage() { return message; }
        public void setMessage(String message) { this.message = message; }
    }
}

4.8 第六步:构建并运行项目

步骤1:在父项目根目录执行构建

bash 复制代码
cd custom-starter-demo
mvn clean install

构建输出示例

ini 复制代码
[INFO] Reactor Build Order:
[INFO] 
[INFO] Custom Starter Demo Parent                     [pom]
[INFO] SMS Spring Boot Starter                        [jar]
[INFO] Demo Consumer                                  [jar]
[INFO] 
[INFO] --- maven-clean-plugin:3.2.0:clean (default-clean) ---
[INFO] 
[INFO] --- maven-install-plugin:3.1.1:install (default-install) ---
[INFO] Installing ... to local repository
[INFO] BUILD SUCCESS

💡 Reactor Build Order :Maven会按正确顺序构建子模块,先构建sms-spring-boot-starter,再构建demo-consumer

步骤2:运行消费者项目

bash 复制代码
cd demo-consumer
mvn spring-boot:run
//或者参考 我githup 源代码里面的README.md文件的说明跑,直接在IDEA里面launch也是可以的

步骤3:测试接口

bash 复制代码
# 测试单条发送 ,或者可以直接在浏览器打开下面链接
curl "http://localhost:8080/api/sms/send?phone=13800138000&message=Hello"

# 测试模板发送,也可以可以直接在浏览器打开下面链接
curl "http://localhost:8080/api/sms/template/verify?phone=13800138000&code=123456"

# 查看自动配置报告  也可以可以直接在浏览器打开下面链接
curl http://localhost:8080/actuator/conditions

预期日志输出

markdown 复制代码
===========================================
初始化短信服务
提供商:aliyun
签名:测试应用
启用状态:true
===========================================
【自动配置】正在创建 SmsService Bean
【自动配置】正在创建 SmsTemplateManager Bean
短信模板管理器初始化完成,已加载 4 个默认模板

4.9 项目结构总览

构建完成后的完整项目结构:

bash 复制代码
custom-starter-demo/
├── pom.xml                              # 父项目POM
│
├── sms-spring-boot-starter/
│   ├── pom.xml                          # Starter模块POM
│   └── src/
│       ├── main/
│       │   ├── java/com/example/sms/
│       │   │   ├── SmsProperties.java
│       │   │   ├── SmsService.java
│       │   │   ├── SendResult.java
│       │   │   ├── DefaultSmsService.java
│       │   │   ├── SmsTemplateManager.java
│       │   │   └── SmsAutoConfiguration.java
│       │   └── resources/META-INF/spring/
│       │       └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│       └── test/
│
└── demo-consumer/
    ├── pom.xml                          # 消费者模块POM
    └── src/
        ├── main/
        │   ├── java/com/example/consumer/
        │   │   ├── DemoConsumerApplication.java
        │   │   └── controller/
        │   │       └── SmsController.java
        │   └── resources/
        │       └── application.properties
        └── test/

五、验证自动配置是否生效

5.1 使用Actuator查看自动配置报告

步骤1:访问自动配置报告

启动应用后,访问:

bash 复制代码
http://localhost:8080/actuator/conditions

步骤2:查看响应内容(部分)

json 复制代码
{
  "contexts": {
    "application": {
      "positiveMatches": {
        "SmsAutoConfiguration": {
          "condition": "OnPropertyCondition",
          "matched": true,
          "details": {
            "sms.enabled": {
              "found": "true",
              "value": "true"
            }
          }
        }
      },
      "negativeMatches": {
        // 未匹配的配置类
      }
    }
  }
}

💡 作用:可以清晰看到哪些自动配置类生效了,哪些没有,以及原因是什么。

5.2 调试技巧:断点跟踪自动配置加载

步骤1:在自动配置类中设置断点

SmsAutoConfiguration.javasmsService方法上设置断点。

步骤2:Debug模式启动消费者项目

步骤3:观察调用栈

你会看到调用链:

scss 复制代码
SpringApplication.run()
  → AutoConfigurationImportSelector.selectImports()
    → getSuggestions()
      → SmsAutoConfiguration.smsService()

六、常见坑点与解决方案

❌ 坑点1:自动配置类不生效

现象:引入Starter后,Bean没有自动创建

排查步骤

  1. 检查AutoConfiguration.imports文件路径是否正确
  2. 检查文件内容是否有空格或格式错误
  3. 检查@Conditional条件是否满足
  4. 访问/actuator/conditions查看匹配报告

解决方案

bash 复制代码
# 确保文件路径正确(Spring Boot 3.x)
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

# 确保内容为类的全限定名,每行一个
com.example.sms.SmsAutoConfiguration

❌ 坑点2:配置属性没有IDE提示

现象 :在application.properties中输入sms.没有自动补全

原因:缺少配置处理器依赖或未重新编译

解决方案

xml 复制代码
<!-- pom.xml中添加 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

然后执行mvn clean install重新编译。

❌ 坑点3:子模块依赖找不到

现象demo-consumer模块编译时报sms-spring-boot-starter找不到

原因:Starter模块未安装到本地Maven仓库

解决方案

bash 复制代码
# 在父项目根目录执行
mvn clean install

# 确保先构建Starter模块,再构建Consumer模块
# 或使用Reactor构建(推荐)
mvn clean install -pl sms-spring-boot-starter,demo-consumer -am

❌ 坑点4:父项目配置未生效

现象:子模块无法继承父项目的依赖版本

原因 :子模块未正确声明<parent>标签

解决方案

xml 复制代码
<!-- 子模块pom.xml必须包含 -->
<parent>
    <groupId>com.example</groupId>
    <artifactId>custom-starter-demo</artifactId>
    <version>1.0.0</version>
</parent>

七、最佳实践总结

7.1 Maven多模块项目规范

实践 说明
父项目packaging 必须是pom类型
子模块声明 父项目<modules>中声明所有子模块
依赖版本管理 在父项目<dependencyManagement>中统一定义
子模块引用 继承父项目后,依赖无需指定版本号
构建顺序 Maven Reactor会自动处理依赖顺序

7.2 Starter开发规范

实践 说明
命名规范 第三方Starter:{name}-spring-boot-starter
配置前缀 使用@ConfigurationProperties(prefix = "{name}")
条件注解 必须使用@ConditionalOnMissingBean给用户留空间
配置元数据 必须包含spring-boot-configuration-processor
自动配置文件 Spring Boot 3.x使用AutoConfiguration.imports

7.3 自动配置类设计原则

java 复制代码
@Configuration
@ConditionalOnClass(...)           // 1. 检查依赖是否存在
@EnableConfigurationProperties(...) // 2. 启用配置绑定
@ConditionalOnProperty(...)        // 3. 检查配置是否开启
public class XxxAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(...) // 4. 避免覆盖用户Bean
    public XxxService xxxService() {
        // 5. 创建Bean
    }
}

八、本篇延伸思考

💡 思考题

  1. 为什么父项目的<packaging>必须是pom而不是jar
  2. 如果要把Starter发布到公司私有Maven仓库,需要做什么配置?
  3. @ConditionalOnMissingBean为什么要放在@Bean方法上而不是类上?
  4. 如果用户想自定义SmsService,应该如何做?

九、下篇预告

第3篇:《配置文件全攻略:application.properties vs application.yml 与多环境切换》

将深入探讨:

  • 配置文件的优先级规则(11层优先级详解)
  • @ConfigurationProperties高级用法
  • 多环境配置(@Profile)最佳实践
  • 配置加密与安全存储

附录:完整代码仓库

所有示例代码已上传GitHub:

ruby 复制代码
主仓库:https://github.com/beatafu/spring-boot-learning.git
本教程:https://github.com/beatafu/spring-boot-learning/tree/main/02-starter-deep-dive

目录结构

bash 复制代码
02-starter-deep-dive/
├── pom.xml                          # 父项目POM
├── sms-spring-boot-starter/         # 自定义Starter源码
│   ├── pom.xml
│   └── src/
└── demo-consumer/                   # 使用Starter的演示项目
    ├── pom.xml
    └── src/

🎉 恭喜你完成第二篇! 现在你已经:

  • ✅ 理解Starter的依赖传递机制
  • ✅ 掌握自动配置的核心原理
  • 能创建Maven多模块项目
  • ✅ 能独立开发自定义Starter
  • ✅ 学会使用Actuator排查配置问题

建议:动手完成自定义Starter的全部步骤,这是理解Spring Boot机制的最佳方式!

作者 :架构师Beata
日期 :2026年3月15日
声明 :本文基于网络文档整理,如有疏漏,欢迎指正。转载请注明出处。
互动代码完全经过自测,如有任何问题?欢迎在评论区分享

相关推荐
享棣1 小时前
Win11 安装 Nacos 2.0.4 完整版文档 文档说明
后端
90后的晨仔1 小时前
windows安装 openclaw 报错
后端
AMoon丶1 小时前
Golang--多种数据结构详解
linux·c语言·开发语言·数据结构·c++·后端·golang
深蓝轨迹2 小时前
SpringBoot YAML配置文件全解析:语法+读取+高级用法
java·spring boot·后端·学习
颜酱2 小时前
最小生成树(MST)核心原理 + Kruskal & Prim 算法
javascript·后端·算法
深蓝轨迹2 小时前
乐观锁 vs 悲观锁 含面试模板
java·spring boot·笔记·后端·学习·mysql·面试
tant1an4 小时前
Spring Boot 基础入门:从核心配置到 SSMP 整合实战
java·数据库·spring boot·sql·spring
用户7344028193424 小时前
SpringBoot —— 实现邮件、短信的发送功能
后端