找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2219

积分

0

好友

297

主题
发表于 前天 01:21 | 查看: 16| 回复: 0

📌 系列说明:本文是《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

<!-- ❌ 传统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方式

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

Starter的定义

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

1.3 Starter的命名规范

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

格式: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命名(注意区别):

格式:{第三方名}-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:打开终端,进入项目根目录

cd your-spring-boot-project

步骤2:执行依赖树命令

mvn dependency:tree -Dincludes=org.springframework.boot:spring-boot-starter-web

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

mvn dependency:tree > dependencies.txt

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

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

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

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:运行测试

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 自动配置加载流程

┌─────────────────────────────────────────────────────────────────┐
│  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 格式(已废弃):

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

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

# 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包

# 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

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

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 系列注解进行条件判断:

@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父项目,包含两个子模块

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中创建父项目

File → New → Project → 选择 "Maven" → 点击 "Next"

步骤2:填写项目信息

字段
Name custom-starter-demo
GroupId com.example
ArtifactId custom-starter-demo
Version 1.0.0
Packaging pom(关键!父项目必须是pom类型)

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

创建完成后,将 pom.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>
      <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:在父项目下创建子模块

右键父项目 → 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 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:再次创建子模块

右键父项目 → 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 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:创建包结构

sms-spring-boot-starter/src/main/java/com/example/sms/

步骤2:创建 SmsProperties.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

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

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

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

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(核心)

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:创建自动配置注册文件(关键)

创建目录和文件:

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

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

# ===========================================
# 短信服务配置
# ===========================================
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

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:在父项目根目录执行构建

cd custom-starter-demo
mvn clean install

构建输出示例

[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:运行消费者项目

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

步骤3:测试接口

# 测试单条发送 ,或者可以直接在浏览器打开下面链接
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

预期日志输出

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

4.9 项目结构总览

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

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:访问自动配置报告
启动应用后,访问:

http://localhost:8080/actuator/conditions

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

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

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

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

步骤1:在自动配置类中设置断点
SmsAutoConfiguration.javasmsService 方法上设置断点。

步骤2:Debug模式启动消费者项目
步骤3:观察调用栈  

你会看到调用链:

SpringApplication.run()
 → AutoConfigurationImportSelector.selectImports()
   → getSuggestions()
     → SmsAutoConfiguration.smsService()

六、常见坑点与解决方案

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

现象:引入Starter后,Bean没有自动创建
排查步骤:  

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

解决方案

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

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

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

现象:在application.properties中输入 sms. 没有自动补全
原因:缺少配置处理器依赖或未重新编译
解决方案

<!-- 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仓库
解决方案

# 在父项目根目录执行
mvn clean install

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

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

现象:子模块无法继承父项目的依赖版本
原因:子模块未正确声明 <parent> 标签
解决方案

<!-- 子模块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 自动配置类设计原则

@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:

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

目录结构

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机制的最佳方式!

Java 是 Spring Boot 的底层基石,深入理解 JVM、类加载、字节码及 Spring 的 IoC/AOP 底层机制,才能真正驾驭 Starter 的自动装配逻辑。
后端 & 架构 领域中,Starter 设计本质是面向切面的模块化封装,其与分布式系统中的 Service Mesh、Sidecar 模式在抽象层级上高度同源。
数据库/中间件/技术栈 的集成能力,正是通过 Starter 将复杂中间件(如 Redis、Kafka)的接入成本降至最低——这也是云原生时代“基础设施即代码”理念在 Java 生态的典型落地。




上一篇:GDC 2026现场观察:展馆缩水、大厂退潮,游戏行业进入调整期
下一篇:OpenClaw与腾讯SkillHub技能社区争议始末:镜像站风波与开源赞助和解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-18 16:37 , Processed in 0.478957 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表