系列导航:本文是《Spring框架第一性原理》系列的第4篇


引子:配置地狱的噩梦

场景重现:创建一个Spring Web应用(传统方式)

想象你是2013年的Java开发者,接到任务:创建一个简单的RESTful API服务。使用传统Spring框架,你需要经历以下"噩梦":

第一步:手动配置依赖(pom.xml,约50行)

<!-- 1. Spring核心依赖 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>4.3.9.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.3.9.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>4.3.9.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>4.3.9.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>4.3.9.RELEASE</version>
</dependency>

<!-- 2. Jackson JSON依赖 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.8.9</version>
</dependency>

<!-- 3. Servlet API -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>

<!-- 4. 日志依赖 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

<!-- 问题:-->
<!-- ❌ 依赖繁多:手动引入10+个依赖 -->
<!-- ❌ 版本冲突:spring-core必须和spring-webmvc版本一致 -->
<!-- ❌ 依赖遗漏:忘记jackson导致JSON序列化失败 -->

第二步:配置web.xml(约30行)

<!-- src/main/webapp/WEB-INF/web.xml -->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
         http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1">

    <!-- 1. 配置Spring上下文监听器 -->
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>

    <!-- 2. 配置Spring上下文配置文件位置 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>

    <!-- 3. 配置DispatcherServlet -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/dispatcher-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- 4. 配置Servlet映射 -->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!-- 5. 配置字符编码过滤器 -->
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>
            org.springframework.web.filter.CharacterEncodingFilter
        </filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

<!-- 问题:-->
<!-- ❌ XML冗长:30行配置只为启动Web应用 -->
<!-- ❌ 路径硬编码:配置文件路径写死 -->
<!-- ❌ 易出错:一个标签写错就启动失败 -->

第三步:配置applicationContext.xml(约40行)

<!-- src/main/webapp/WEB-INF/applicationContext.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 1. 启用注解扫描 -->
    <context:component-scan base-package="com.example">
        <context:exclude-filter type="annotation"
            expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

    <!-- 2. 加载properties文件 -->
    <context:property-placeholder location="classpath:application.properties"/>

    <!-- 3. 配置数据源 -->
    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="jdbcUrl" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="maximumPoolSize" value="20"/>
    </bean>

    <!-- 4. 配置JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 5. 配置事务管理器 -->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 6. 启用事务注解 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

<!-- 问题:-->
<!-- ❌ 配置繁琐:每个Bean都要显式配置 -->
<!-- ❌ 命名空间复杂:xsi:schemaLocation写错就无法启动 -->
<!-- ❌ 重复配置:每个项目都要写相同的配置 -->

第四步:配置dispatcher-servlet.xml(约30行)

<!-- src/main/webapp/WEB-INF/dispatcher-servlet.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/mvc
           http://www.springframework.org/schema/mvc/spring-mvc.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 1. 扫描Controller -->
    <context:component-scan base-package="com.example.controller"/>

    <!-- 2. 启用Spring MVC注解 -->
    <mvc:annotation-driven>
        <mvc:message-converters>
            <!-- 配置JSON转换器 -->
            <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                <property name="objectMapper">
                    <bean class="com.fasterxml.jackson.databind.ObjectMapper">
                        <property name="dateFormat">
                            <bean class="java.text.SimpleDateFormat">
                                <constructor-arg value="yyyy-MM-dd HH:mm:ss"/>
                            </bean>
                        </property>
                    </bean>
                </property>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <!-- 3. 配置静态资源处理 -->
    <mvc:default-servlet-handler/>

    <!-- 4. 配置拦截器 -->
    <mvc:interceptors>
        <bean class="com.example.interceptor.LogInterceptor"/>
    </mvc:interceptors>
</beans>

第五步:配置应用(application.properties)

# 数据库配置
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=123456

# 日志配置
logging.level.root=INFO
logging.level.com.example=DEBUG

第六步:编写业务代码

// Controller
@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getUser(id);
    }
}

// Service
@Service
public class UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public User getUser(Long id) {
        return jdbcTemplate.queryForObject(
            "SELECT * FROM users WHERE id = ?",
            new Object[]{id},
            new BeanPropertyRowMapper<>(User.class)
        );
    }
}

第七步:部署到Tomcat

# 1. 打包成WAR文件
mvn clean package

# 2. 将WAR文件复制到Tomcat webapps目录
cp target/myapp.war /usr/local/tomcat/webapps/

# 3. 启动Tomcat
/usr/local/tomcat/bin/startup.sh

# 4. 访问应用
curl http://localhost:8080/myapp/api/users/1

统计:传统Spring Web应用的复杂度

维度数量/行数说明
配置文件数量5个pom.xml、web.xml、applicationContext.xml、dispatcher-servlet.xml、application.properties
配置代码行数约200行纯配置代码,不含业务逻辑
依赖数量15+个需要手动管理版本
外部容器依赖Tomcat必须部署到Servlet容器
启动时间约30秒Tomcat启动 + Spring初始化
学习曲线陡峭XML配置、命名空间、Schema

问题总结

传统Spring Web应用的五大痛点:

1. 配置地狱(Configuration Hell)
   ├─ XML配置文件冗长(200+行)
   ├─ 命名空间复杂(记不住schemaLocation)
   ├─ 配置分散(5个配置文件)
   └─ 重复配置(每个项目都要写)

2. 依赖管理噩梦(Dependency Hell)
   ├─ 依赖繁多(15+个Maven依赖)
   ├─ 版本冲突(Spring各模块版本必须一致)
   ├─ 依赖遗漏(忘记引入导致运行时错误)
   └─ 传递依赖混乱(A依赖B,B依赖C的不同版本)

3. 容器部署复杂
   ├─ 必须依赖外部容器(Tomcat、Jetty)
   ├─ 打包成WAR文件
   ├─ 部署步骤繁琐
   └─ 环境不一致(开发环境 vs 生产环境)

4. 启动速度慢
   ├─ Tomcat启动耗时
   ├─ Spring容器初始化耗时
   └─ 总启动时间:30秒+

5. 学习成本高
   ├─ XML配置语法
   ├─ Spring命名空间
   ├─ 容器部署知识
   └─ 调试困难(配置错误定位难)

对比:Spring Boot的"魔法"

同样的需求,Spring Boot只需要这样:

第一步:创建项目(使用Spring Initializr)

访问 https://start.spring.io/,选择:

  • Project: Maven
  • Language: Java
  • Spring Boot: 2.7.x
  • Dependencies: Spring Web, Spring Data JDBC, MySQL Driver

点击"Generate",下载项目压缩包,解压即可。

耗时:30秒

第二步:配置应用(application.yml)

# application.yml(仅10行)
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: 123456

server:
  port: 8080

logging:
  level:
    com.example: DEBUG

第三步:编写业务代码(完全相同)

// Controller
@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getUser(id);
    }
}

// Service
@Service
public class UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public User getUser(Long id) {
        return jdbcTemplate.queryForObject(
            "SELECT * FROM users WHERE id = ?",
            new Object[]{id},
            new BeanPropertyRowMapper<>(User.class)
        );
    }
}

// 启动类(新增,只需3行)
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

第四步:启动应用

# 方式1:IDE直接运行main方法
# 方式2:命令行启动
mvn spring-boot:run

# 方式3:打包成JAR,独立运行
mvn clean package
java -jar target/myapp.jar

# 访问应用
curl http://localhost:8080/api/users/1

# 启动时间:3-5秒

对比表格:传统Spring vs Spring Boot

维度传统SpringSpring Boot差异
配置文件数量5个1个(application.yml)减少80%
配置代码行数约200行约10行减少95%
依赖管理手动引入15+个依赖spring-boot-starter-web(1个)自动管理
外部容器必须(Tomcat等)可选(内嵌Tomcat)独立运行
打包方式WAR文件JAR文件更简单
启动时间约30秒3-5秒快6-10倍
部署步骤复杂(4步)简单(1步:java -jar)简化
学习成本高(XML、容器)低(约定优于配置)降低70%
开发效率提升5倍+

Spring Boot的"零配置"体验

Spring Boot如何做到零配置?

1. 自动配置(Auto-Configuration)
   ├─ 检测到spring-boot-starter-web → 自动配置DispatcherServlet
   ├─ 检测到MySQL驱动 → 自动配置DataSource
   ├─ 检测到JdbcTemplate → 自动配置事务管理器
   └─ 所有配置自动完成,开发者无需关心

2. Starter机制
   ├─ spring-boot-starter-web → 自动引入15+个依赖
   ├─ 依赖版本自动管理(Spring Boot统一管理)
   ├─ 依赖传递自动处理
   └─ 一个Starter = 一套完整解决方案

3. 内嵌容器
   ├─ Tomcat内嵌在应用中
   ├─ 打包成可执行JAR
   ├─ java -jar一键启动
   └─ 环境一致(开发 = 生产)

4. 约定优于配置(Convention Over Configuration)
   ├─ 默认端口:8080
   ├─ 默认配置文件:application.yml/properties
   ├─ 默认静态资源目录:static/
   └─ 开发者只需关注业务逻辑

第一性原理:为什么Spring Boot能做到这些?

核心问题拆解

传统Spring的配置复杂度本质上源于三个问题:

问题1:依赖管理复杂
└─ 根本原因:需要手动声明每个依赖及其版本
   └─ Spring Boot解决方案:Starter机制(依赖聚合 + 版本管理)

问题2:配置繁琐
└─ 根本原因:需要显式配置每个Bean和组件
   └─ Spring Boot解决方案:自动配置(条件装配 + 约定优于配置)

问题3:部署复杂
└─ 根本原因:依赖外部容器(Tomcat等)
   └─ Spring Boot解决方案:内嵌容器(可执行JAR)

Spring Boot的三大核心机制

机制1:Starter机制(依赖管理的抽象)

问题:传统Spring需要手动引入众多依赖,容易遗漏和版本冲突

Starter的本质:依赖的聚合 + 版本的统一管理

<!-- 传统Spring:手动引入15+个依赖 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>4.3.9.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>4.3.9.RELEASE</version>
</dependency>
<!-- ...还有13个依赖... -->

<!-- Spring Boot:只需1个Starter -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <!-- 版本由spring-boot-starter-parent统一管理,无需指定 -->
</dependency>

Starter的内部结构

<!-- spring-boot-starter-web的pom.xml内容 -->
<dependencies>
    <!-- 1. Spring Boot核心Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <!-- 2. Spring Boot JSON Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-json</artifactId>
    </dependency>

    <!-- 3. Spring Boot Tomcat Starter(内嵌容器)-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
    </dependency>

    <!-- 4. Spring Web MVC -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
    </dependency>
</dependencies>

<!-- Starter机制的价值:-->
<!-- ✅ 依赖聚合:1个Starter = 完整的依赖集合 -->
<!-- ✅ 版本管理:Spring Boot统一管理所有依赖版本 -->
<!-- ✅ 开箱即用:引入Starter后立即可用 -->

版本管理的秘密

<!-- 1. 项目继承spring-boot-starter-parent -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.10</version>
</parent>

<!-- 2. spring-boot-starter-parent继承spring-boot-dependencies -->
<!-- spring-boot-dependencies定义了300+个依赖的版本 -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.3.25</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.3.25</version>
        </dependency>
        <!-- ...还有298个依赖... -->
    </dependencies>
</dependencyManagement>

<!-- 3. 开发者只需引入Starter,无需指定版本 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <!-- 版本自动继承,无需指定 -->
</dependency>

<!-- 结果:-->
<!-- ✅ 无版本冲突:Spring Boot保证所有依赖版本兼容 -->
<!-- ✅ 简化配置:开发者无需关心版本号 -->
<!-- ✅ 统一升级:升级Spring Boot版本,所有依赖自动升级 -->

机制2:自动配置(配置的自动化)

问题:传统Spring需要显式配置每个Bean(DataSource、JdbcTemplate、TransactionManager等)

自动配置的本质:条件判断 + Bean自动注册

// 传统Spring:手动配置DataSource
@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource dataSource() {
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        ds.setUsername("root");
        ds.setPassword("123456");
        return ds;
    }
}

// Spring Boot:自动配置DataSource
// 开发者什么都不用写,Spring Boot自动完成
// 只需在application.yml配置数据库连接信息
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: 123456

自动配置的实现原理

// Spring Boot的DataSource自动配置类
@Configuration
@ConditionalOnClass(DataSource.class)  // 条件1:类路径存在DataSource类
@ConditionalOnMissingBean(DataSource.class)  // 条件2:容器中不存在DataSource Bean
@EnableConfigurationProperties(DataSourceProperties.class)  // 读取配置文件
public class DataSourceAutoConfiguration {

    @Bean
    public DataSource dataSource(DataSourceProperties properties) {
        // 根据配置文件创建DataSource
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl(properties.getUrl());
        ds.setUsername(properties.getUsername());
        ds.setPassword(properties.getPassword());
        return ds;
    }
}

// 自动配置的工作流程:
1. 检测条件类路径是否存在DataSource类
   └─ 继续
   └─ 跳过此自动配置

2. 检测条件容器中是否已存在DataSource Bean
   └─ 自动创建
   └─ 跳过尊重开发者自定义

3. 读取配置从application.yml读取spring.datasource.*配置

4. 创建Bean根据配置自动创建DataSource Bean

// 结果:
// ✅ 开发者无需写配置类
// ✅ 配置外部化(application.yml)
// ✅ 支持自定义(开发者可以覆盖)

条件装配的完整体系

// Spring Boot提供的条件注解

@ConditionalOnClass         // 类路径存在指定类
@ConditionalOnMissingClass  // 类路径不存在指定类
@ConditionalOnBean          // 容器存在指定Bean
@ConditionalOnMissingBean   // 容器不存在指定Bean
@ConditionalOnProperty      // 配置文件存在指定属性
@ConditionalOnResource      // 类路径存在指定资源文件
@ConditionalOnWebApplication  // Web应用环境
@ConditionalOnNotWebApplication  // 非Web应用环境
@ConditionalOnExpression    // SpEL表达式为true
@ConditionalOnJava          // Java版本匹配

// 示例:只有在Web环境且存在Tomcat类时才配置
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat")
public class EmbeddedTomcatConfiguration {
    // 配置内嵌Tomcat
}

机制3:内嵌容器(部署的简化)

问题:传统Spring必须部署到外部容器(Tomcat、Jetty等)

内嵌容器的本质:将容器作为应用的一部分打包

// Spring Boot启动流程
public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
}

// SpringApplication.run()内部做了什么?
public ConfigurableApplicationContext run(String... args) {
    // 1. 创建Spring容器
    context = createApplicationContext();

    // 2. 刷新容器(加载Bean)
    refreshContext(context);

    // 3. 启动内嵌Tomcat(自动配置)
    // TomcatServletWebServerFactory.getWebServer()
    // └─ 创建Tomcat实例
    // └─ 配置端口、上下文路径
    // └─ 注册DispatcherServlet
    // └─ 启动Tomcat

    return context;
}

// 结果:
// ✅ 无需外部容器:Tomcat嵌入在应用中
// ✅ 打包成JAR:可执行JAR(java -jar启动)
// ✅ 环境一致:开发环境和生产环境完全相同

内嵌容器的实现原理

// 1. Spring Boot检测到spring-boot-starter-web
//    → 自动配置内嵌Tomcat

// 2. 自动配置类:ServletWebServerFactoryAutoConfiguration
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(ServletRequest.class)
public class ServletWebServerFactoryAutoConfiguration {

    @Configuration
    @ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat")
    public static class EmbeddedTomcat {

        @Bean
        public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
            return new TomcatServletWebServerFactory();
        }
    }
}

// 3. TomcatServletWebServerFactory创建Tomcat实例
public WebServer getWebServer(ServletContextInitializer... initializers) {
    // 创建Tomcat实例
    Tomcat tomcat = new Tomcat();

    // 配置端口
    Connector connector = new Connector();
    connector.setPort(getPort());  // 默认8080
    tomcat.getService().addConnector(connector);

    // 配置上下文
    Context context = tomcat.addContext("", getDocumentRoot());

    // 注册DispatcherServlet
    Wrapper dispatcherServlet = context.createWrapper();
    dispatcherServlet.setName("dispatcherServlet");
    dispatcherServlet.setServletClass(DispatcherServlet.class.getName());
    context.addChild(dispatcherServlet);

    // 启动Tomcat
    tomcat.start();

    return new TomcatWebServer(tomcat);
}

// 结果:
// ✅ Tomcat作为应用的一部分启动
// ✅ 无需部署到外部容器
// ✅ 应用和容器打包在一起

深度剖析:@SpringBootApplication注解

一个注解,三大功能

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

这个看似简单的注解,背后是三个注解的组合:

// @SpringBootApplication源码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration  // 功能1:标识这是Spring Boot配置类
@EnableAutoConfiguration  // 功能2:启用自动配置
@ComponentScan           // 功能3:启用组件扫描
public @interface SpringBootApplication {
    // ...
}

功能1:@SpringBootConfiguration

// @SpringBootConfiguration源码
@Configuration
public @interface SpringBootConfiguration {
    // 本质上就是@Configuration
    // 标识这是一个Spring配置类
}

// 等价于
@Configuration
public class Application {
    // 可以在这里定义Bean

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

功能2:@EnableAutoConfiguration(核心)

这是Spring Boot自动配置的核心,让我们深入剖析:

// @EnableAutoConfiguration源码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage  // 作用1:自动配置包
@Import(AutoConfigurationImportSelector.class)  // 作用2:导入自动配置选择器
public @interface EnableAutoConfiguration {
    // ...
}

作用1:@AutoConfigurationPackage

// @AutoConfigurationPackage源码
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
    // 将主配置类所在包及其子包注册到Spring容器
}

// Registrar的作用:
static class Registrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
                                       BeanDefinitionRegistry registry) {
        // 获取主配置类所在包
        // 例如:com.example.Application → com.example
        register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
    }
}

// 结果:
// ✅ Spring Boot知道从哪里扫描组件
// ✅ 默认扫描主配置类所在包及子包
// ✅ 无需手动配置@ComponentScan的basePackages

作用2:AutoConfigurationImportSelector(自动配置的核心)

这是Spring Boot自动配置的灵魂,让我们逐步拆解:

// AutoConfigurationImportSelector的核心方法
public class AutoConfigurationImportSelector implements DeferredImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        // 1. 检查自动配置是否启用
        if (!isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        }

        // 2. 加载自动配置类(核心)
        AutoConfigurationEntry autoConfigurationEntry =
            getAutoConfigurationEntry(annotationMetadata);

        // 3. 返回要导入的自动配置类名称
        return StringUtils.toStringArray(
            autoConfigurationEntry.getConfigurations()
        );
    }

    protected AutoConfigurationEntry getAutoConfigurationEntry(
            AnnotationMetadata annotationMetadata) {

        // 1. 加载所有自动配置类(从spring.factories文件)
        List<String> configurations = getCandidateConfigurations(annotationMetadata);
        // 例如:加载到130+个自动配置类

        // 2. 去重
        configurations = removeDuplicates(configurations);

        // 3. 排除不需要的(通过exclude属性)
        Set<String> exclusions = getExclusions(annotationMetadata);
        configurations.removeAll(exclusions);

        // 4. 过滤(通过条件注解@Conditional*)
        configurations = getConfigurationClassFilter().filter(configurations);
        // 例如:130+个 → 30+个(只有满足条件的才会被加载)

        // 5. 触发自动配置导入事件
        fireAutoConfigurationImportEvents(configurations, exclusions);

        return new AutoConfigurationEntry(configurations, exclusions);
    }
}

关键:从哪里加载自动配置类?

// getCandidateConfigurations方法
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata) {
    // 从META-INF/spring.factories文件加载
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
        getSpringFactoriesLoaderFactoryClass(),  // EnableAutoConfiguration.class
        getBeanClassLoader()
    );
    return configurations;
}

// SpringFactoriesLoader.loadFactoryNames的作用:
// 扫描所有jar包的META-INF/spring.factories文件
// 读取org.springframework.boot.autoconfigure.EnableAutoConfiguration配置项

spring.factories文件示例

# spring-boot-autoconfigure-2.7.10.jar中的spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
# ...还有100+个自动配置类

自动配置的完整流程图

@SpringBootApplication
    ↓
@EnableAutoConfiguration
    ↓
AutoConfigurationImportSelector
    ↓
扫描所有jar包的META-INF/spring.factories
    ↓
读取EnableAutoConfiguration配置项(130+个自动配置类)
    ↓
去重、排除
    ↓
过滤(通过@Conditional*条件注解)
    ↓
最终导入30+个自动配置类(满足条件的)
    ↓
每个自动配置类创建对应的Bean
    ↓
应用启动成功,所有组件自动配置完成

示例:DataSourceAutoConfiguration
    ↓
@ConditionalOnClass(DataSource.class)  // 检查类路径是否存在DataSource
    ↓ 是
@ConditionalOnMissingBean(DataSource.class)  // 检查容器是否已存在DataSource Bean
    ↓ 否(不存在)
@EnableConfigurationProperties(DataSourceProperties.class)  // 读取spring.datasource.*配置
    ↓
创建DataSource Bean(根据配置)
    ↓
注册到Spring容器

功能3:@ComponentScan

// @ComponentScan默认行为
@ComponentScan
public class Application {
    // 默认扫描主配置类所在包及其子包
    // 例如:com.example.Application
    // → 扫描 com.example 及其子包
}

// 可以自定义扫描范围
@SpringBootApplication
@ComponentScan(basePackages = {"com.example", "com.other"})
public class Application {
    // 扫描多个包
}

自动配置的条件装配体系

条件注解的作用

Spring Boot的自动配置依赖于一套完整的条件注解体系,只有满足条件的配置才会生效。

常用条件注解详解

1. @ConditionalOnClass / @ConditionalOnMissingClass

// 示例:只有当类路径存在DataSource类时才配置
@Configuration
@ConditionalOnClass(DataSource.class)
public class DataSourceAutoConfiguration {

    @Bean
    public DataSource dataSource() {
        return new HikariDataSource();
    }
}

// 使用场景:
// ✅ 引入了spring-boot-starter-jdbc → 类路径存在DataSource → 自动配置生效
// ❌ 没有引入JDBC相关依赖 → 类路径不存在DataSource → 自动配置跳过

// @ConditionalOnMissingClass(反向条件)
@Configuration
@ConditionalOnMissingClass("com.zaxxer.hikari.HikariDataSource")
public class BasicDataSourceConfiguration {
    // 只有当HikariDataSource不存在时才使用BasicDataSource
}

2. @ConditionalOnBean / @ConditionalOnMissingBean

// 示例:只有当容器中不存在DataSource Bean时才自动配置
@Configuration
@ConditionalOnMissingBean(DataSource.class)
public class DataSourceAutoConfiguration {

    @Bean
    public DataSource dataSource() {
        return new HikariDataSource();
    }
}

// 使用场景:
// 场景A:开发者没有自定义DataSource
//   → 容器中不存在DataSource Bean
//   → 自动配置生效,创建默认DataSource

// 场景B:开发者自定义了DataSource
@Configuration
public class MyDataSourceConfig {
    @Bean
    public DataSource dataSource() {
        // 自定义DataSource
        return new MyCustomDataSource();
    }
}
//   → 容器中已存在DataSource Bean
//   → 自动配置跳过,尊重开发者自定义

// 结果:
// ✅ 支持自定义覆盖
// ✅ 默认提供标准实现

3. @ConditionalOnProperty

// 示例:只有当配置文件中存在指定属性时才生效
@Configuration
@ConditionalOnProperty(
    name = "spring.datasource.enabled",  // 属性名
    havingValue = "true",                // 期望值
    matchIfMissing = true                // 属性不存在时的默认行为
)
public class DataSourceAutoConfiguration {

    @Bean
    public DataSource dataSource() {
        return new HikariDataSource();
    }
}

// 使用场景:
// 场景A:application.yml中配置了spring.datasource.enabled=true
//   → 自动配置生效

// 场景B:application.yml中配置了spring.datasource.enabled=false
//   → 自动配置跳过

// 场景C:application.yml中没有配置spring.datasource.enabled
//   → matchIfMissing=true → 自动配置生效(默认行为)

// 实战案例:控制功能开关
@Configuration
@ConditionalOnProperty(name = "feature.cache.enabled", havingValue = "true")
public class CacheAutoConfiguration {
    // 只有在配置文件中开启缓存功能时才自动配置
}

4. @ConditionalOnWebApplication / @ConditionalOnNotWebApplication

// 示例:只有在Web应用环境才配置
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
public class WebMvcAutoConfiguration {

    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }
}

// 使用场景:
// ✅ Web应用(引入了spring-boot-starter-web)→ 自动配置生效
// ❌ 非Web应用(如批处理任务)→ 自动配置跳过

// @ConditionalOnNotWebApplication(反向条件)
@Configuration
@ConditionalOnNotWebApplication
public class BatchAutoConfiguration {
    // 只有在非Web应用才配置批处理相关Bean
}

5. @ConditionalOnResource

// 示例:只有当类路径存在指定资源文件时才配置
@Configuration
@ConditionalOnResource(resources = "classpath:custom-config.xml")
public class CustomConfigAutoConfiguration {

    @Bean
    public CustomConfig customConfig() {
        // 加载custom-config.xml文件
        return new CustomConfig();
    }
}

// 使用场景:
// ✅ 类路径存在custom-config.xml → 自动配置生效
// ❌ 类路径不存在custom-config.xml → 自动配置跳过

6. @ConditionalOnExpression

// 示例:使用SpEL表达式控制条件
@Configuration
@ConditionalOnExpression("${spring.datasource.enabled:true} && '${spring.profiles.active}' == 'prod'")
public class ProductionDataSourceConfiguration {
    // 只有在生产环境且数据源启用时才配置
}

// 使用场景:
// ✅ 复杂条件判断
// ✅ 组合多个属性
// ❌ 性能略低(SpEL表达式解析)

7. @ConditionalOnJava

// 示例:只有在指定Java版本才配置
@Configuration
@ConditionalOnJava(JavaVersion.EIGHT)
public class Java8Configuration {
    // 只有在Java 8环境才配置
}

@Configuration
@ConditionalOnJava(range = Range.EQUAL_OR_NEWER, value = JavaVersion.ELEVEN)
public class Java11Configuration {
    // 只有在Java 11及以上版本才配置
}

条件注解的组合使用

// 实战案例:Redis自动配置
@Configuration
@ConditionalOnClass(RedisOperations.class)  // 条件1:类路径存在Redis类
@EnableConfigurationProperties(RedisProperties.class)  // 读取配置
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")  // 条件2:容器中不存在redisTemplate
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)  // 条件3:存在唯一的连接工厂
    public RedisTemplate<Object, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {

        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Configuration
    @ConditionalOnClass(StringRedisTemplate.class)  // 条件4:类路径存在StringRedisTemplate
    protected static class StringRedisTemplateConfiguration {

        @Bean
        @ConditionalOnMissingBean  // 条件5:容器中不存在StringRedisTemplate
        public StringRedisTemplate stringRedisTemplate(
                RedisConnectionFactory redisConnectionFactory) {

            StringRedisTemplate template = new StringRedisTemplate();
            template.setConnectionFactory(redisConnectionFactory);
            return template;
        }
    }
}

// 条件判断流程:
1. 检查类路径是否存在RedisOperations  继续
2. 检查容器中是否已存在redisTemplate  创建
3. 检查是否存在唯一的RedisConnectionFactory  继续
4. 创建RedisTemplate Bean
5. 检查类路径是否存在StringRedisTemplate  继续
6. 检查容器中是否已存在StringRedisTemplate  创建
7. 创建StringRedisTemplate Bean

// 结果:
// ✅ 自动配置Redis相关Bean
// ✅ 支持开发者自定义覆盖
// ✅ 只有在满足所有条件时才配置

配置优先级与外部化配置

配置文件的加载顺序

Spring Boot支持多种配置方式,优先级从高到低:

优先级(从高到低):

1. 命令行参数
   java -jar app.jar --server.port=9000

2. Java系统属性(System.getProperties())
   java -Dserver.port=9000 -jar app.jar

3. 操作系统环境变量
   export SERVER_PORT=9000

4. application-{profile}.yml(外部配置文件)
   /config/application-prod.yml(jar包同级config目录)
   /application-prod.yml(jar包同级目录)

5. application-{profile}.yml(内部配置文件)
   classpath:/config/application-prod.yml
   classpath:/application-prod.yml

6. application.yml(外部配置文件)
   /config/application.yml
   /application.yml

7. application.yml(内部配置文件)
   classpath:/config/application.yml
   classpath:/application.yml

8. @PropertySource指定的配置文件

9. 默认值(SpringApplication.setDefaultProperties)

配置文件示例

# application.yml(默认配置)
server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: 123456

logging:
  level:
    root: INFO
# application-dev.yml(开发环境配置)
server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb_dev
    username: root
    password: dev123456

logging:
  level:
    root: DEBUG
    com.example: TRACE
# application-prod.yml(生产环境配置)
server:
  port: 80

spring:
  datasource:
    url: jdbc:mysql://prod-db:3306/mydb_prod
    username: app_user
    password: ${DB_PASSWORD}  # 从环境变量读取

logging:
  level:
    root: WARN
    com.example: INFO

激活Profile

# 方式1:命令行参数
java -jar app.jar --spring.profiles.active=prod

# 方式2:环境变量
export SPRING_PROFILES_ACTIVE=prod
java -jar app.jar

# 方式3:配置文件
# application.yml
spring:
  profiles:
    active: prod

配置优先级实战示例

# application.yml
server:
  port: 8080  # 默认端口

# application-prod.yml
server:
  port: 80  # 生产环境端口
# 场景1:没有任何额外配置
java -jar app.jar
# 结果:端口=8080(application.yml)

# 场景2:激活prod环境
java -jar app.jar --spring.profiles.active=prod
# 结果:端口=80(application-prod.yml覆盖application.yml)

# 场景3:命令行指定端口
java -jar app.jar --spring.profiles.active=prod --server.port=9000
# 结果:端口=9000(命令行参数优先级最高)

# 场景4:环境变量 + 命令行
export SERVER_PORT=7000
java -jar app.jar --server.port=9000
# 结果:端口=9000(命令行参数优先级高于环境变量)

实战:手写一个自定义Starter

让我们通过手写一个Starter来深入理解Spring Boot的Starter机制。

需求:自定义短信发送Starter

目标:创建spring-boot-starter-sms,让开发者只需引入依赖和配置,即可使用短信发送功能。

第一步:创建Starter项目结构

spring-boot-starter-sms/
├── pom.xml
└── src/main/java/com/example/sms/
    ├── SmsProperties.java          # 配置属性类
    ├── SmsService.java             # 核心服务类
    └── SmsAutoConfiguration.java   # 自动配置类
└── src/main/resources/
    └── META-INF/
        └── spring.factories        # 自动配置注册文件

第二步: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
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>spring-boot-starter-sms</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>Spring Boot SMS Starter</name>
    <description>Spring Boot Starter for SMS</description>

    <dependencies>
        <!-- 1. Spring Boot自动配置依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.7.10</version>
        </dependency>

        <!-- 2. 配置元数据生成(可选,用于IDE提示)-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>2.7.10</version>
            <optional>true</optional>
        </dependency>

        <!-- 3. 短信SDK依赖(示例:阿里云短信)-->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.6.0</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
            <version>2.2.1</version>
        </dependency>
    </dependencies>
</project>

第三步:配置属性类(SmsProperties)

package com.example.sms;

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

/**
 * 短信配置属性类
 *
 * 对应配置文件中的sms.*配置项
 */
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {

    /**
     * 是否启用短信功能
     */
    private boolean enabled = true;

    /**
     * 短信服务商(aliyun、tencent等)
     */
    private String provider = "aliyun";

    /**
     * AccessKey ID
     */
    private String accessKeyId;

    /**
     * AccessKey Secret
     */
    private String accessKeySecret;

    /**
     * 短信签名
     */
    private String signName;

    /**
     * 短信模板编码
     */
    private String templateCode;

    /**
     * 区域(如cn-hangzhou)
     */
    private String regionId = "cn-hangzhou";

    /**
     * 连接超时时间(毫秒)
     */
    private int connectTimeout = 10000;

    /**
     * 读取超时时间(毫秒)
     */
    private int readTimeout = 10000;

    // getters and setters
    public boolean isEnabled() {
        return enabled;
    }

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

    public String getProvider() {
        return provider;
    }

    public void setProvider(String provider) {
        this.provider = provider;
    }

    public String getAccessKeyId() {
        return accessKeyId;
    }

    public void setAccessKeyId(String accessKeyId) {
        this.accessKeyId = accessKeyId;
    }

    public String getAccessKeySecret() {
        return accessKeySecret;
    }

    public void setAccessKeySecret(String accessKeySecret) {
        this.accessKeySecret = accessKeySecret;
    }

    public String getSignName() {
        return signName;
    }

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

    public String getTemplateCode() {
        return templateCode;
    }

    public void setTemplateCode(String templateCode) {
        this.templateCode = templateCode;
    }

    public String getRegionId() {
        return regionId;
    }

    public void setRegionId(String regionId) {
        this.regionId = regionId;
    }

    public int getConnectTimeout() {
        return connectTimeout;
    }

    public void setConnectTimeout(int connectTimeout) {
        this.connectTimeout = connectTimeout;
    }

    public int getReadTimeout() {
        return readTimeout;
    }

    public void setReadTimeout(int readTimeout) {
        this.readTimeout = readTimeout;
    }
}

第四步:核心服务类(SmsService)

package com.example.sms;

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

/**
 * 短信服务类
 */
public class SmsService {

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

    private final SmsProperties properties;
    private final IAcsClient acsClient;

    public SmsService(SmsProperties properties) {
        this.properties = properties;

        // 初始化阿里云短信客户端
        IClientProfile profile = DefaultProfile.getProfile(
            properties.getRegionId(),
            properties.getAccessKeyId(),
            properties.getAccessKeySecret()
        );

        this.acsClient = new DefaultAcsClient(profile);

        log.info("SmsService initialized with provider: {}", properties.getProvider());
    }

    /**
     * 发送短信
     *
     * @param phone 手机号
     * @param params 模板参数
     * @return 是否发送成功
     */
    public boolean sendSms(String phone, Map<String, String> params) {
        if (!properties.isEnabled()) {
            log.warn("SMS service is disabled");
            return false;
        }

        try {
            // 构建请求
            SendSmsRequest request = new SendSmsRequest();
            request.setPhoneNumbers(phone);
            request.setSignName(properties.getSignName());
            request.setTemplateCode(properties.getTemplateCode());

            // 模板参数(JSON格式)
            if (params != null && !params.isEmpty()) {
                StringBuilder json = new StringBuilder("{");
                params.forEach((key, value) -> {
                    json.append("\"").append(key).append("\":\"").append(value).append("\",");
                });
                json.deleteCharAt(json.length() - 1);  // 删除最后一个逗号
                json.append("}");
                request.setTemplateParam(json.toString());
            }

            // 发送短信
            SendSmsResponse response = acsClient.getAcsResponse(request);

            if ("OK".equals(response.getCode())) {
                log.info("SMS sent successfully to {}", phone);
                return true;
            } else {
                log.error("Failed to send SMS to {}: {} - {}",
                    phone, response.getCode(), response.getMessage());
                return false;
            }

        } catch (Exception e) {
            log.error("Error sending SMS to {}", phone, e);
            return false;
        }
    }

    /**
     * 发送验证码短信(简化方法)
     *
     * @param phone 手机号
     * @param code 验证码
     * @return 是否发送成功
     */
    public boolean sendVerificationCode(String phone, String code) {
        return sendSms(phone, Map.of("code", code));
    }
}

第五步:自动配置类(SmsAutoConfiguration)

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;

/**
 * 短信自动配置类
 *
 * 条件:
 * 1. 类路径存在IAcsClient类(引入了阿里云SDK)
 * 2. 配置文件中sms.enabled=true(默认为true)
 * 3. 容器中不存在SmsService Bean(支持自定义)
 */
@Configuration
@ConditionalOnClass(name = "com.aliyuncs.IAcsClient")  // 条件1:类路径存在阿里云SDK
@ConditionalOnProperty(
    prefix = "sms",
    name = "enabled",
    havingValue = "true",
    matchIfMissing = true  // 配置不存在时默认为true
)  // 条件2:配置启用
@EnableConfigurationProperties(SmsProperties.class)  // 启用配置属性
public class SmsAutoConfiguration {

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

    @Bean
    @ConditionalOnMissingBean  // 条件3:容器中不存在SmsService(支持自定义)
    public SmsService smsService(SmsProperties properties) {
        log.info("Auto-configuring SmsService with properties: {}", properties);
        return new SmsService(properties);
    }
}

第六步:注册自动配置类(spring.factories)

# src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.sms.SmsAutoConfiguration

第七步:配置元数据(可选,用于IDE提示)

// src/main/resources/META-INF/spring-configuration-metadata.json
{
  "groups": [
    {
      "name": "sms",
      "type": "com.example.sms.SmsProperties",
      "sourceType": "com.example.sms.SmsProperties"
    }
  ],
  "properties": [
    {
      "name": "sms.enabled",
      "type": "java.lang.Boolean",
      "description": "Whether to enable SMS service.",
      "defaultValue": true
    },
    {
      "name": "sms.provider",
      "type": "java.lang.String",
      "description": "SMS provider (aliyun, tencent, etc.).",
      "defaultValue": "aliyun"
    },
    {
      "name": "sms.access-key-id",
      "type": "java.lang.String",
      "description": "AccessKey ID for SMS service."
    },
    {
      "name": "sms.access-key-secret",
      "type": "java.lang.String",
      "description": "AccessKey Secret for SMS service."
    },
    {
      "name": "sms.sign-name",
      "type": "java.lang.String",
      "description": "SMS signature name."
    },
    {
      "name": "sms.template-code",
      "type": "java.lang.String",
      "description": "SMS template code."
    },
    {
      "name": "sms.region-id",
      "type": "java.lang.String",
      "description": "Region ID (e.g., cn-hangzhou).",
      "defaultValue": "cn-hangzhou"
    },
    {
      "name": "sms.connect-timeout",
      "type": "java.lang.Integer",
      "description": "Connect timeout in milliseconds.",
      "defaultValue": 10000
    },
    {
      "name": "sms.read-timeout",
      "type": "java.lang.Integer",
      "description": "Read timeout in milliseconds.",
      "defaultValue": 10000
    }
  ]
}

第八步:发布Starter

# 1. 打包
mvn clean package

# 2. 安装到本地Maven仓库(测试用)
mvn clean install

# 3. 发布到Maven中央仓库(生产用)
mvn clean deploy

第九步:使用自定义Starter

1. 引入依赖

<dependency>
    <groupId>com.example</groupId>
    <artifactId>spring-boot-starter-sms</artifactId>
    <version>1.0.0</version>
</dependency>

2. 配置application.yml

sms:
  enabled: true
  provider: aliyun
  access-key-id: YOUR_ACCESS_KEY_ID
  access-key-secret: YOUR_ACCESS_KEY_SECRET
  sign-name: 你的签名
  template-code: SMS_123456789
  region-id: cn-hangzhou
  connect-timeout: 10000
  read-timeout: 10000

3. 使用SmsService

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

    @Autowired
    private SmsService smsService;  // 自动注入

    @PostMapping("/send-code")
    public String sendVerificationCode(@RequestParam String phone) {
        // 生成6位验证码
        String code = String.format("%06d", (int)(Math.random() * 1000000));

        // 发送短信
        boolean success = smsService.sendVerificationCode(phone, code);

        if (success) {
            return "验证码发送成功";
        } else {
            return "验证码发送失败";
        }
    }
}

4. 启动应用

# 启动应用
mvn spring-boot:run

# 测试接口
curl -X POST http://localhost:8080/api/sms/send-code?phone=13800138000

# 输出:
# 验证码发送成功

5. 日志输出

2025-11-03 18:00:00.123  INFO --- [main] c.e.sms.SmsAutoConfiguration        : Auto-configuring SmsService with properties: SmsProperties{enabled=true, provider='aliyun', ...}
2025-11-03 18:00:00.456  INFO --- [main] c.e.sms.SmsService                  : SmsService initialized with provider: aliyun
2025-11-03 18:00:01.789  INFO --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http)
2025-11-03 18:00:01.890  INFO --- [main] c.e.Application                     : Started Application in 2.345 seconds

2025-11-03 18:05:30.123  INFO --- [http-nio-8080-exec-1] c.e.sms.SmsService                  : SMS sent successfully to 13800138000

Starter工作流程总结

用户引入spring-boot-starter-sms
    ↓
Spring Boot启动
    ↓
扫描所有jar包的META-INF/spring.factories
    ↓
发现SmsAutoConfiguration
    ↓
检查条件1:类路径存在IAcsClient?
    ↓ 是
检查条件2:sms.enabled=true?
    ↓ 是(默认true)
检查条件3:容器中不存在SmsService?
    ↓ 是(开发者未自定义)
加载SmsProperties(读取sms.*配置)
    ↓
创建SmsService Bean
    ↓
注册到Spring容器
    ↓
应用启动成功
    ↓
开发者@Autowired注入SmsService
    ↓
调用smsService.sendVerificationCode()
    ↓
短信发送成功

关键点:
✅ 开发者只需引入依赖+配置,无需写任何Spring配置代码
✅ Starter内部完成所有配置工作
✅ 支持自定义覆盖(通过@ConditionalOnMissingBean)
✅ 配置外部化(application.yml)
✅ IDE智能提示(spring-configuration-metadata.json)

Spring Boot最佳实践

1. Starter命名规范

官方Starter:spring-boot-starter-*
  ├─ spring-boot-starter-web
  ├─ spring-boot-starter-data-jpa
  └─ spring-boot-starter-redis

第三方Starter:*-spring-boot-starter
  ├─ mybatis-spring-boot-starter
  ├─ druid-spring-boot-starter
  └─ sms-spring-boot-starter(我们自定义的)

命名原则:
✅ 官方Starter:前缀spring-boot-starter
✅ 第三方Starter:后缀spring-boot-starter
❌ 不要违反命名规范

2. 配置属性前缀规范

# ✅ 推荐:使用短小、有意义的前缀
sms:
  enabled: true
  provider: aliyun

# ✅ 推荐:多级配置使用层级结构
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: 123456

# ❌ 不推荐:前缀过长
com.example.project.sms:
  enabled: true

# ❌ 不推荐:配置扁平化(不使用层级)
sms-enabled: true
sms-provider: aliyun

3. 条件注解使用建议

// ✅ 推荐:使用@ConditionalOnMissingBean支持自定义
@Bean
@ConditionalOnMissingBean
public SmsService smsService(SmsProperties properties) {
    return new SmsService(properties);
}

// ✅ 推荐:使用@ConditionalOnProperty控制功能开关
@Configuration
@ConditionalOnProperty(name = "sms.enabled", havingValue = "true", matchIfMissing = true)
public class SmsAutoConfiguration {
    // ...
}

// ✅ 推荐:组合使用多个条件注解
@Configuration
@ConditionalOnClass(IAcsClient.class)
@ConditionalOnProperty(name = "sms.enabled", havingValue = "true")
@ConditionalOnMissingBean(SmsService.class)
public class SmsAutoConfiguration {
    // ...
}

// ❌ 不推荐:条件过于宽松(可能导致误配置)
@Configuration
public class SmsAutoConfiguration {
    // 没有任何条件,总是会配置
}

4. 配置类设计建议

// ✅ 推荐:配置类分离关注点
@Configuration
public class DataSourceAutoConfiguration {
    // 只负责DataSource相关配置
}

@Configuration
public class JdbcTemplateAutoConfiguration {
    // 只负责JdbcTemplate相关配置
}

// ❌ 不推荐:所有配置都放在一个类
@Configuration
public class DatabaseAutoConfiguration {
    // 混杂了DataSource、JdbcTemplate、事务管理器等
}

5. 日志输出规范

// ✅ 推荐:关键步骤输出INFO日志
public SmsService(SmsProperties properties) {
    this.properties = properties;
    // ...
    log.info("SmsService initialized with provider: {}", properties.getProvider());
}

// ✅ 推荐:配置详情输出DEBUG日志
@Bean
public SmsService smsService(SmsProperties properties) {
    log.debug("Creating SmsService with properties: {}", properties);
    return new SmsService(properties);
}

// ✅ 推荐:错误情况输出ERROR日志
public boolean sendSms(String phone, Map<String, String> params) {
    try {
        // ...
    } catch (Exception e) {
        log.error("Error sending SMS to {}", phone, e);
        return false;
    }
}

// ❌ 不推荐:过度日志(影响性能)
public boolean sendSms(String phone, Map<String, String> params) {
    log.info("开始发送短信");
    log.info("手机号: {}", phone);
    log.info("参数: {}", params);
    log.info("构建请求");
    // ...每一步都输出日志
}

总结:Spring Boot的核心价值

从配置地狱到约定优于配置

传统Spring的痛点:
├─ 配置文件冗长(200+行XML)
├─ 依赖管理复杂(15+个依赖,版本冲突)
├─ 部署繁琐(WAR包、外部容器)
├─ 启动缓慢(30秒+)
└─ 学习成本高(XML、容器、部署)

Spring Boot的解决方案:
├─ 自动配置:零配置启动
├─ Starter机制:一键引入完整依赖
├─ 内嵌容器:独立运行JAR
├─ 快速启动:3-5秒
└─ 约定优于配置:降低学习成本

核心价值:
✅ 开发效率提升5倍+
✅ 配置代码减少95%
✅ 部署复杂度降低80%
✅ 学习成本降低70%

Spring Boot三大核心机制总结

1. Starter机制(依赖管理)

问题:手动管理依赖繁琐、容易出错
解决:
  └─ 依赖聚合:1个Starter = 完整依赖集合
  └─ 版本管理:Spring Boot统一管理版本
  └─ 开箱即用:引入即可使用

2. 自动配置(配置自动化)

问题:显式配置每个Bean繁琐、重复
解决:
  └─ 条件装配:按需配置(@Conditional*)
  └─ 约定优于配置:默认值 + 可覆盖
  └─ 配置外部化:application.yml

3. 内嵌容器(部署简化)

问题:依赖外部容器、部署复杂
解决:
  └─ 容器内嵌:Tomcat打包进应用
  └─ 可执行JAR:java -jar一键启动
  └─ 环境一致:开发 = 生产

第一性原理思考

Spring Boot的本质:
  └─ 问题:Spring配置复杂
     └─ 根本原因1:依赖管理手动化
        └─ 解决方案:Starter机制
     └─ 根本原因2:Bean配置显式化
        └─ 解决方案:自动配置
     └─ 根本原因3:部署依赖外部容器
        └─ 解决方案:内嵌容器

约定优于配置的哲学:
  └─ 提供合理的默认值(80%场景适用)
  └─ 支持自定义覆盖(20%特殊场景)
  └─ 降低决策负担(开发者专注业务)

给从业者的建议

L1(初级):会用Spring Boot
├─ 掌握常用Starter(web、data-jpa、redis)
├─ 理解application.yml配置
├─ 能独立搭建Spring Boot项目
└─ 熟悉常用注解(@RestController、@Service)

L2(中级):理解原理
├─ 理解自动配置机制
├─ 掌握条件注解体系
├─ 能阅读spring.factories文件
├─ 能自定义Starter
└─ 掌握配置优先级

L3(高级):深度定制
├─ 阅读Spring Boot源码
├─ 能设计复杂的自动配置
├─ 掌握SpringApplication启动流程
├─ 能优化启动性能
└─ 能解决复杂配置问题

学习路径:
从使用入手 → 理解原理 → 深度定制

结语

Spring Boot通过"约定优于配置"的设计哲学,将开发者从繁琐的配置工作中解放出来,让我们能够专注于业务逻辑的实现。

核心要点回顾

  1. Starter机制:依赖管理的抽象,一键引入完整依赖
  2. 自动配置:条件装配 + 约定优于配置,零配置启动
  3. 内嵌容器:独立运行JAR,简化部署
  4. 配置优先级:命令行 > 环境变量 > 配置文件,灵活配置
  5. 自定义Starter:通过spring.factories + 自动配置类,扩展Spring Boot

关键收获

  • 从第一性原理理解Spring Boot的设计动机
  • 掌握自动配置的实现原理和条件注解体系
  • 学会手写自定义Starter,扩展Spring Boot能力
  • 理解约定优于配置的设计哲学

下一篇预告第5篇:Spring Cloud微服务架构

  • 从单体应用到微服务的演进
  • Spring Cloud组件体系深度剖析
  • 微服务拆分的第一性原理

系列文章

  1. 为什么我们需要Spring框架?
  2. IoC容器:从手动new到自动装配的演进
  3. AOP:从代码重复到面向切面编程
  4. Spring Boot:约定优于配置的威力(本文)
  5. Spring Cloud:从单体到微服务的架构演进(待发布)
  6. Spring源码深度解析(待发布)

🤔 思考题

  1. 如果让你设计一个自定义Starter,你会选择什么功能?
  2. Spring Boot的自动配置是如何做到"约定优于配置,但不强制"的?
  3. 为什么内嵌容器能显著提升开发效率?

欢迎在评论区分享你的思考和经验!

📚 推荐阅读