📌 系列说明 :本文是《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>
⚠️ 关键点说明:
<packaging>pom</packaging>- 父项目必须是pom类型,不能是jar<modules>- 声明所有子模块,Maven会按顺序构建<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>
⚠️ 关键点说明:
<parent>- 声明父项目,继承父项目的配置和依赖管理- 依赖无需指定版本号 - 从父项目的
dependencyManagement继承<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>
⚠️ 关键点说明:
- 引入
sms-spring-boot-starter时无需指定版本号- 同一父项目下的子模块可以直接引用,Maven会自动处理
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.java的smsService方法上设置断点。
步骤2:Debug模式启动消费者项目
步骤3:观察调用栈
你会看到调用链:
scss
SpringApplication.run()
→ AutoConfigurationImportSelector.selectImports()
→ getSuggestions()
→ SmsAutoConfiguration.smsService()
六、常见坑点与解决方案
❌ 坑点1:自动配置类不生效
现象:引入Starter后,Bean没有自动创建
排查步骤:
- 检查
AutoConfiguration.imports文件路径是否正确 - 检查文件内容是否有空格或格式错误
- 检查
@Conditional条件是否满足 - 访问
/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
}
}
八、本篇延伸思考
💡 思考题:
- 为什么父项目的
<packaging>必须是pom而不是jar?- 如果要把Starter发布到公司私有Maven仓库,需要做什么配置?
@ConditionalOnMissingBean为什么要放在@Bean方法上而不是类上?- 如果用户想自定义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日
声明 :本文基于网络文档整理,如有疏漏,欢迎指正。转载请注明出处。
互动 :代码完全经过自测,如有任何问题?欢迎在评论区分享