Spring Boot 自动配置原理揭秘:从 @SpringBootApplication 到手写自定义 Starter

一、前言:为什么 Spring Boot 能做到"零配置"?

前面 15 节课,我们从 Java 基础走到 Maven 多模块。今天进入 Spring Boot 的核心:自动配置原理

你是否有这样的疑问:

  • 为什么引入 spring-boot-starter-web 就能自动启动 Tomcat?
  • 为什么配置了 spring.datasource.url 就能自动创建数据源?
  • 为什么自定义一个 @Configuration 就能覆盖默认配置?

答案就在 @EnableAutoConfiguration 的实现机制里。


二、@SpringBootApplication 拆解

java 复制代码
@SpringBootApplication(scanBasePackag

@SpringBootApplication 是一个组合注解,拆开来看:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration    // ① 标记为配置类
@EnableAutoConfiguration    // ② 启用自动配置(核心!)
@ComponentScan(excludeFilters = { ... })  // ③ 组件扫描
public @interface SpringBootApplication {
}

三个核心注解:

注解 作用 等价于
@SpringBootConfiguration 标记为配置类,允许用 @Bean 定义 Bean @Configuration
@EnableAutoConfiguration 启用自动配置机制 无直接等价
@ComponentScan 扫描 @Component@Service@Controller 手动写 <context:component-scan>

三、自动配置的核心机制

3.1 自动配置加载流程

plain 复制代码
SpringApplication.run()
    ↓
SpringFactoriesLoader.loadFactoryNames()
    ↓
读取 META-INF/spring.factories(2.7-)或 META-INF/spring/...imports(3.0+)
    ↓
加载所有 EnableAutoConfiguration 的实现类
    ↓
条件注解判断(@ConditionalOnClass, @ConditionalOnProperty 等)
    ↓
满足条件的配置类生效,注册 Bean 到容器

3.2 spring.factories 文件

Spring Boot 2.7+ 之前,自动配置类注册在 META-INF/spring.factories

properties 复制代码
# spring-boot-autoconfigure.jar 内部
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
...(上百个配置类)

Spring Boot 3.0+ 改为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Text 复制代码
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration

3.3 条件注解:决定配置类是否生效

自动配置不是无脑加载,而是通过条件注解判断是否生效:

条件注解 作用 示例
@ConditionalOnClass 类路径存在指定类时生效 Tomcat.class 存在 → 启用内嵌 Tomcat
@ConditionalOnMissingClass 类路径不存在指定类时生效 DispatcherServlet → 启用 WebFlux
@ConditionalOnBean 容器中存在指定 Bean 时生效 DataSource → 启用 JdbcTemplate
@ConditionalOnMissingBean 容器中不存在指定 Bean 时生效 无自定义 ObjectMapper → 启用默认 JSON 配置
@ConditionalOnProperty 配置文件中指定属性满足条件时生效 spring.datasource.url 存在 → 启用数据源
@ConditionalOnWebApplication 是 Web 应用时生效 ServletContext → 启用 Web 配置

WebMvcAutoConfiguration 为例:

java 复制代码
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean
    public InternalResourceViewResolver defaultViewResolver() {
        // 默认视图解析器
    }
    
    @Bean
    @ConditionalOnMissingBean
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
        // 默认请求处理适配器
    }
    
    // ... 其他默认配置
}

解读:

  • @ConditionalOnWebApplication(Type.SERVLET):是 Servlet Web 应用才生效
  • @ConditionalOnClass({Servlet.class, DispatcherServlet.class}):类路径有 Servlet 相关类才生效
  • @ConditionalOnMissingBean(WebMvcConfigurationSupport.class):用户没有自定义 WebMvc 配置才生效
  • @ConditionalOnMissingBean 的方法级别:用户没有自定义 Bean 才创建默认的

这就是"约定大于配置"的实现原理:

  • 你什么都不写 → 用 Spring Boot 的默认配置
  • 你自定义了 → 你的配置覆盖默认配置

四、手写一个自定义 Starter

理解了原理,我们来实战:写一个自己的 Starter,让其他项目引入就能自动配置。

场景:创建一个 hello-spring-boot-starter,引入后自动注入 HelloService,配置 hello.name 即可使用。

4.1 项目结构

plain 复制代码
hello-spring-boot-starter/
├── pom.xml
└── src/main/java/com/example/hello/
    ├── HelloService.java              ← 核心服务
    ├── HelloProperties.java           ← 配置属性绑定
    ├── HelloAutoConfiguration.java    ← 自动配置类
    └── HelloServiceAutoConfiguration.imports  ← 注册文件

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

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
    </parent>

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

    <dependencies>
        <!-- 自动配置核心依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        
        <!-- 配置属性提示(IDEA 中输入配置时有自动补全) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

4.3 HelloProperties.java(配置属性)

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

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

/**
 * Hello 配置属性
 * 绑定配置文件中的 hello.* 前缀属性
 */
@ConfigurationProperties(prefix = "hello")
public class HelloProperties {
    
    /** 问候语前缀,默认 "Hello" */
    private String prefix = "Hello";
    
    /** 目标名称 */
    private String name = "World";
    
    /** 是否开启问候功能,默认 true */
    private boolean enabled = true;

    // Getter / Setter
    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isEnabled() {
        return enabled;
    }

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

4.4 HelloService.java(核心服务)

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

/**
 * 问候服务
 */
public class HelloService {
    
    private final HelloProperties properties;
    
    public HelloService(HelloProperties properties) {
        this.properties = properties;
    }
    
    /**
     * 生成问候语
     */
    public String sayHello() {
        return properties.getPrefix() + ", " + properties.getName() + "!";
    }
    
    /**
     * 生成带时间的问候语
     */
    public String sayHelloWithTime() {
        return String.format("%s, %s! Now is %s",
            properties.getPrefix(),
            properties.getName(),
            java.time.LocalDateTime.now()
        );
    }
}

4.5 HelloAutoConfiguration.java(自动配置类)

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

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;

/**
 * Hello 自动配置类
 * 
 * 条件:
 * 1. 类路径存在 HelloService(@ConditionalOnClass)
 * 2. 配置 hello.enabled=true 或未配置(@ConditionalOnProperty)
 * 3. 用户未自定义 HelloService Bean(@ConditionalOnMissingBean)
 */
@Configuration
@EnableConfigurationProperties(HelloProperties.class)  // 启用配置属性绑定
@ConditionalOnClass(HelloService.class)                   // 类路径有 HelloService
@ConditionalOnProperty(prefix = "hello", name = "enabled", havingValue = "true", matchIfMissing = true)
public class HelloAutoConfiguration {
    
    @Bean
    @ConditionalOnMissingBean  // 用户没自定义时才创建
    public HelloService helloService(HelloProperties properties) {
        return new HelloService(properties);
    }
}

4.6 注册自动配置

src/main/resources/META-INF/spring/ 下创建文件:

org.springframework.boot.autoconfigure.AutoConfiguration.imports

Text 复制代码
com.example.hello.HelloAutoConfiguration

4.7 打包发布

bash 复制代码
mvn clean install

安装到本地 Maven 仓库,其他项目即可引用。


五、在项目中使用自定义 Starter

在 Day 15 的 ecommerce-web 模块中引入:

pom.xml 添加依赖:

xml 复制代码
<dependency>
    <groupId>com.example</groupId>
    <artifactId>hello-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

application.yml 配置:

yaml 复制代码
hello:
  prefix: "Hi"
  name: "Java全栈工程师"
  enabled: true

UserController.java 注入使用:

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

import com.example.api.dto.UserDTO;
import com.example.common.result.Result;
import com.example.hello.HelloService;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private HelloService helloService;  // 自动注入!
    
    @GetMapping("/{id}")
    public Result<UserDTO> getUser(@PathVariable Long id) {
        // 使用自定义 Starter 的服务
        System.out.println(helloService.sayHello());
        System.out.println(helloService.sayHelloWithTime());
        
        UserDTO user = userService.getUserById(id);
        return Result.success(user);
    }
}

启动后访问 http://localhost:8080/api/users/1,控制台输出:

plain 复制代码
Hi, Java全栈工程师!
Hi, Java全栈工程师! Now is 2026-06-13T22:45:30.123

六、自动配置源码调试技巧

6.1 查看生效的自动配置

application.yml 中开启调试:

yaml 复制代码
debug: true

启动后控制台输出:

plain 复制代码
============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:      ← 生效的自动配置
-----------------
   WebMvcAutoConfiguration matched:
      - @ConditionalOnClass found required classes '...' (OnClassCondition)
      - @ConditionalOnMissingBean (types: WebMvcConfigurationSupport; SearchStrategy: all) did not find any (OnBeanCondition)

Negative matches:      ← 未生效的自动配置
-----------------
   DataSourceAutoConfiguration:
      Did not match:
         - @ConditionalOnClass (required DataSource, found none) (OnClassCondition)

6.2 排除特定自动配置

java 复制代码
@SpringBootApplication(
    scanBasePackages = "com.example",
    exclude = { DataSourceAutoConfiguration.class }  // 排除数据源自动配置
)

或在 application.yml

yaml 复制代码
spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
概念 作用 关键点
@SpringBootApplication 组合注解:配置类 + 自动配置 + 组件扫描 拆开理解三个子注解
@EnableAutoConfiguration 启用自动配置机制 读取 spring.factories / imports 文件
spring.factories / .imports 注册自动配置类 Spring Boot 3.0+ 改用 imports 文件
@ConditionalOnXxx 条件注解,控制配置类是否生效 组合使用实现"约定大于配置"
@EnableConfigurationProperties 启用配置属性绑定 配合 @ConfigurationProperties
自定义 Starter 封装通用功能,一键引入 自动配置类 + properties + 注册文件

八、面试题自检

问题 答案
@SpringBootApplication 由哪些注解组成? @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan
自动配置类从哪里加载? META-INF/spring.factories(2.7-)或 META-INF/spring/...AutoConfiguration.imports(3.0+)
@ConditionalOnClass 作用? 类路径存在指定类时配置生效
@ConditionalOnMissingBean 作用? 容器中不存在指定 Bean 时创建默认 Bean
如何自定义 Starter? ① 写服务类 ② 写 Properties ③ 写 AutoConfiguration ④ 注册到 imports 文件
如何排除自动配置? @SpringBootApplication(exclude=...)spring.autoconfigure.exclude

最大感悟: Spring Boot 的"零配置"不是真的没有配置,而是把配置藏在了自动配置类里。理解这套机制后,不仅能用好框架,还能封装自己的 Starter 复用代码。

相关推荐
人道领域1 小时前
【LeetCode刷题日记】47.全排列Ⅱ
java·开发语言·算法·leetcode
周杰伦fans1 小时前
续集:工作空间一切换,我的插件菜单就消失?——MenuBar与Ribbon的自动重载方案
后端·ribbon·c#
是苏浙2 小时前
Java实现链表1
java·开发语言
未若君雅裁2 小时前
上传数据安全:对称加密、非对称加密、签名与重放防护
java·安全
可乐ea2 小时前
【Spring Boot + MyBatis|第7篇】JWT 登录认证与拦截器实现
java·spring boot·后端·mybatis·状态模式
步步为营DotNet2 小时前
借助 C# 14 特性强化 .NET 后端数据验证的深度实践
java·c#·.net
西安邮电大学2 小时前
有关栈的经典算法题
java·后端·其他·算法·面试
手握风云-3 小时前
ProtoBuf:从序列化原理到高性能架构底座(一)
java·网络·架构
摇滚侠3 小时前
SpringMVC 入门到实战 配置类替换 XML 配置文件 86-91
xml·java·后端·spring·maven·intellij-idea