【阶段 10】进阶原理:源码、自定义 Starter 与 WebFlux

13次阅读
没有评论

版本约定:Spring Boot 3.2.x / JDK 17 / Maven
3.9.x。本篇默认你已经完成阶段 1(Spring Core)与阶段 2(Spring Boot
基础),能熟练使用
@Component@Autowired@Configurationapplication.yml
等基础知识。


1. 导语

前九个阶段,你学会了「怎么用」Spring Boot:写
Controller、配数据源、加事务注解、打日志、调缓存。但到了真实生产环境,你会遇到一类全新的问题:自动配置不生效却不知道为什么、循环依赖导致启动报错却找不到原因、@Transactional
明明加了却不回滚、同一个类里两个方法互相调用事务像没加一样、接口高并发扛不住却不知道是不是该换
WebFlux。这些问题有一个共同点——它们全都发生在「框架替你做完的那部分」里,你用应用层的知识根本看不见。

这一阶段就是带你「看透原理」。你会从 main 方法一路追到
Spring 容器最核心的
refresh(),读懂自动配置的筛选机制,搞明白为什么循环依赖需要「三级缓存」而不是两级,弄清楚
AOP 与事务本质上是同一个东西——动态代理 +
环绕通知,再亲手写一个能被其他项目直接引用的自定义
Starter,最后入门响应式编程 WebFlux,知道它和 MVC 到底该怎么选。

学完这一篇,你就不再是「会写 Spring Boot 代码的人」,而是「能解释
Spring Boot 为什么这样工作的人」。这也是面试题「Bean
生命周期」「三级缓存」「@Transactional 原理」的高频考点所在。


2. 学习目标与前置要求

学完你能

  • 画出 SpringApplication.run()refresh()
    再到内嵌 Tomcat 启动的完整时序图,并说出每一步做了什么
  • 说清楚自动配置的核心类 AutoConfigurationImportSelector
    与配置文件 AutoConfiguration.imports 的关系,会用
    --debug 排查「配置不生效」
  • 讲透三级缓存:一级存什么、二级存什么、三级存什么,以及「为什么必须是三级而非两级」
  • 从源码角度解释 JDK 动态代理与 CGLIB 的区别、Spring Boot 3.x
    默认选谁,以及 @Transactional 为什么是环绕通知
  • 独立开发一个符合命名规范、带 @Conditional
    组合、可被其他项目开箱即用的自定义 Starter
  • 用 Reactor 写响应式接口,理解背压,并正确判断「WebFlux vs Spring
    MVC」的选型边界

前置依赖

依赖 内容 若未掌握
阶段 1 IoC、Bean、依赖注入、AOP 基础概念 先复习阶段 1 的「Bean 与依赖注入」「AOP 入门」两章
阶段 2 @SpringBootApplicationapplication.yml、自动装配的「黑盒」用法 先看阶段 2 的「自动配置初体验」

本篇不需要你预先读过 Spring
源码,但建议你有一份可断点的本地工程(下文会给出),边看边跟踪比干读文字有效十倍。


3. 环境准备

本篇大量内容需要「边读源码边验证」,请准备一个最小工程。

1. 精确版本

组件 版本
JDK 17(LTS)
Spring Boot 3.2.5
Maven 3.9.x
IDE IntelliJ IDEA 2023+(能下载源码、打断点、Ctrl+Click
跳进框架类)

2. 初始化命令

# 使用 Spring Initializr 快速生成(也可在 IDEA 里 New Project → Spring Initializr)
curl https://start.spring.io/starter.zip 
  -d dependencies=web,lombok 
  -d javaVersion=17 
  -d bootVersion=3.2.5 
  -d groupId=com.example 
  -d artifactId=advanced-principles 
  -d name=advanced-principles 
  -o advanced-principles.zip

unzip advanced-principles.zip && cd advanced-principles

3. 生成的 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>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>advanced-principles</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>advanced-principles</name>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 阶段 6 之后会用到 WebFlux,这里先不引入,避免两套栈冲突 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

4. 让 IDEA 能跳进源码(关键)

断点跟踪源码前,先确认框架源码可读:

  1. pom.xmlCtrl+Click 任意
    SpringApplication 类名,IDEA 会自动下载
    spring-boot-3.2.5-sources.jar
  2. 若跳进的是 .class 反编译代码(泛型丢失、注释缺失),在
    Maven 面板勾选 Download Sources and
    Documentation
  3. 确认
    Preferences → Build → Build Tools → Maven → Importing
    里勾选了 Sources

提示:本篇会标注「源码位置」,如
SpringApplication#run(String...),你可以在 IDEA 里用
Navigate → ClassCtrl+N)直接定位。


4.1 Spring Boot 启动流程源码

本章目标:把 SpringApplication.run()
从「一行黑盒」拆成「七步可跟踪的流水线」,并回答面试高频问题「refreshContext
做了什么」「内嵌 Tomcat 是什么时候启动的」。

1.1 从 main 方法到 run()
的完整链路

先看最熟悉的入口:

package com.example.advancedprinciples;

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

@SpringBootApplication
public class AdvancedPrinciplesApplication {

    public static void main(String[] args) {
        // 这行是「一切的原点」,背后是一个庞大的启动流水线
        SpringApplication.run(AdvancedPrinciplesApplication.class, args);
    }
}

SpringApplication.run()
是一个静态便捷方法,它内部其实做了两件事:先
new SpringApplication(...) 构造对象,再调用实例方法
run(args)。完整时序如下:

SpringApplication.run(主类, args)
│
├─ ① new SpringApplication(主类)              // 构造阶段
│     ├─ 推断应用类型 WebApplicationType        // SERVLET / REACTIVE / NONE
│     ├─ 加载 ApplicationContextInitializer    // 上下文初始化器
│     └─ 加载 ApplicationListener              // 应用监听器
│
└─ ② springApplication.run(args)              // 运行阶段
      ├─ 创建 StopWatch 并启动                   // 计时
      ├─ 触发 SpringApplicationRunListeners.starting()   // 广播「启动开始」事件
      ├─ prepareEnvironment()                   // ③ 准备 Environment(读配置)
      ├─ createApplicationContext()             // ④ 创建 ApplicationContext
      ├─ prepareContext(...)                    // ⑤ 准备上下文(注册主类、执行 initializer)
      ├─ refreshContext(context)                // ⑥ 核心:Spring 最复杂的方法
      │     └─ AbstractApplicationContext.refresh()  // 12 步(见 1.4)
      ├─ afterRefresh(context)                  // 空实现,留给子类扩展
      ├─ listeners.ready(...)                   // 广播「应用就绪」事件
      └─ callRunners(...)                       // ⑦ 执行 ApplicationRunner / CommandLineRunner

下面我们把每一步讲透。

1.2 构造阶段:new
SpringApplication() 做了什么

源码位置:SpringApplication#SpringApplication(ResourceLoader, Class<?>...)

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // ① 推断应用类型:根据 classpath 上有无 DispatcherServlet / WebFlux 相关类
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // ② 从 spring.factories / imports 里加载初始化器与监听器
    this.bootstrapRegistryInitializers = new ArrayList<>(
            getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    // ③ 推断 main 方法所在的类(用于日志打印「Started XxxApplication」)
    this.mainApplicationClass = deduceMainApplicationClass();
}

关键点:deduceFromClasspath()
怎么判断应用类型?

源码位置:WebApplicationType#deduceFromClasspath()

static WebApplicationType deduceFromClasspath() {
    // REACTIVE:有 WebFlux 的 DispatcherHandler 类,且没有 Servlet 相关类
    if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null)
            && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
            && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
        return WebApplicationType.REACTIVE;
    }
    // 如果都没有,就是非 Web 应用(比如纯命令行/批处理)
    for (String className : SERVLET_INDICATOR_CLASSES) {
        if (!ClassUtils.isPresent(className, null)) {
            return WebApplicationType.NONE;
        }
    }
    // 默认:Servlet Web 应用(最常见)
    return WebApplicationType.SERVLET;
}

它不做任何「神秘魔法」,就是用 ClassUtils.isPresent()
判断 classpath 里有没有对应的类。这也解释了为什么第 6 章会强调「WebFlux
和 WebMVC
不要同时引入」——两个都引入时,deduceFromClasspath() 会退回
SERVLET 类型,响应式栈根本不会被启用。

本章小结(1.2):构造阶段不创建任何
Bean,只是「判断类型 +
收集工具人(initializer、listener)」,为后续流水线备料。

1.3 run() 方法的分步拆解

源码位置:SpringApplication#run(String...)。我们按执行顺序走读(已精简,保留主干)。

public ConfigurableApplicationContext run(String... args) {
    // 计时器:启动耗时统计,最后会打出一行「Started ... in X seconds」
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();

    // 准备 BootstrapContext(引导上下文,Spring Boot 2.4+ 引入,用于早期 Bean)
    DefaultBootstrapContext bootstrapContext = createBootstrapContext();
    ConfigurableApplicationContext context = null;

    // 设置 java.awt.headless 属性(无界面服务器环境必须)
    configureHeadlessProperty();

    // ① 获取运行监听器,并广播「starting」事件
    //    SpringApplicationRunListener 默认是 EventPublishingRunListener,负责把事件转发给 ApplicationListener
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass);

    try {
        // ② 解析命令行参数,封装为 ApplicationArguments
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

        // ③ 准备 Environment:加载 application.yml / 命令行 / 环境变量 / 系统属性
        ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);

        // 打印 Banner(那个大大的 SPRING 字样)
        Banner printedBanner = printBanner(environment);

        // ④ 根据 webApplicationType 创建对应的 ApplicationContext
        //    SERVLET → AnnotationConfigServletWebServerApplicationContext
        //    REACTIVE → AnnotationConfigReactiveWebServerApplicationContext
        context = createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);

        // ⑤ 准备上下文:设置 Environment、注册主配置类、执行所有 ApplicationContextInitializer
        prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

        // ⑥ 核心:刷新上下文(见 1.4)
        refreshContext(context);

        // ⑦ refresh 之后:留给子类的空钩子
        afterRefresh(context, applicationArguments);

        stopWatch.stop();

        // 广播「应用就绪」事件(这是最常用的监听点,可在此做预热、清理缓存等)
        listeners.ready(context, timeTakenToStartup);

        // ⑧ 执行 ApplicationRunner 和 CommandLineRunner
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, listeners);
        throw new IllegalStateException(ex);
    }
    ...
    return context;
}

这里有一个特别容易混淆的点prepareContext()
里有一个 load()
调用,会把主配置类(@SpringBootApplication
注解的那个类)注册成 BeanDefinition。也就是说,你的主类在
refresh()
之前,就已经以「定义」的形式存在了,但真正「实例化 + 注入 + 代理」发生在
refresh() 里。

本章小结(1.3)run()
是一条七段流水线:构造 → 监听器 → 环境 → 上下文 → prepare → refresh →
runners。前五步都是「准备食材」,第六步 refresh()
才是「开火炒菜」。

1.4 refreshContext:Spring
最核心的方法

refreshContext(context) 最终会调到
AbstractApplicationContext#refresh(),这是整个 Spring
框架最复杂、最值得背下来的方法。Spring Boot
并没有重写它,只是提前准备好了一个合适的 ApplicationContext
实现类,然后照常调用。

源码位置:AbstractApplicationContext#refresh()(Spring
Framework 6.x)。下面是 12 步的精简走读:

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // ① 准备刷新:记录启动时间、校验必填属性、初始化事件源集合
        prepareRefresh();

        // ② 获取 BeanFactory(Spring Boot 场景下此时其实早已创建好,这里做一个「刷新前处理」)
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

        // ③ 准备 BeanFactory:设置类加载器、SpEL 解析器、注册一些内置 BeanPostProcessor
        prepareBeanFactory(beanFactory);

        try {
            // ④ 子类扩展点:留给特定上下文实现注册特殊 BeanPostProcessor(Servlet 场景会注册 ServletContextAwareProcessor)
            postProcessBeanFactory(beanFactory);

            // ⑤ 执行 BeanFactoryPostProcessor(含 ConfigurationClassPostProcessor)
            //    这里完成:@Configuration 解析、@Bean 方法注册、@ComponentScan 扫描、
            //    @Import 处理(自动配置的入口 AutoConfigurationImportSelector 就在这里被触发!)
            invokeBeanFactoryPostProcessors(beanFactory);

            // ⑥ 注册 BeanPostProcessor(只是注册,不执行;执行时机在实例化 Bean 时)
            registerBeanPostProcessors(beanFactory);

            // ⑦ 初始化消息源(国际化 MessageSource)
            initMessageSource();

            // ⑧ 初始化事件广播器(ApplicationEventMulticaster)
            initApplicationEventMulticaster();

            // ⑨ 刷新钩子:ServletWebServerApplicationContext 在这里创建并启动内嵌 Tomcat(见 1.5)
            onRefresh();

            // ⑩ 注册监听器(ApplicationListener)
            registerListeners();

            // ⑪ 实例化所有非懒加载的单例 Bean(依赖注入、AOP 代理都发生在这里)
            finishBeanFactoryInitialization(beanFactory);

            // ⑫ 完成刷新:初始化 LifecycleProcessor、发布 ContextRefreshedEvent
            finishRefresh();
        }
        catch (BeansException ex) {
            // 销毁已创建的 Bean,避免残留
            destroyBeans();
            cancelRefresh(ex);
            throw ex;
        }
        finally {
            resetCommonCaches();
        }
    }
}

你必须记住的 4 个关键步骤

步骤 方法 核心作用 面试常问点
invokeBeanFactoryPostProcessors 解析 @Configuration、扫描组件、处理
@Import
自动配置的入口就在这
registerBeanPostProcessors 注册 BeanPostProcessor(AOP 代理创建器) AOP 为什么能生效
onRefresh Servlet 容器启动 Tomcat 内嵌 Tomcat 的启动时机
finishBeanFactoryInitialization 实例化所有单例 Bean,完成依赖注入与代理 Bean 生命周期、循环依赖都在这里

为什么「背 refresh() 的 12
步」值得
:几乎所有疑难杂症(Bean 没创建、AOP
没生效、配置没加载、监听器没触发)都能在这 12
步里定位到具体的「某一步」。面试问「refresh 做了什么」,你能按顺序讲出
12 步并强调 ⑤⑥⑨⑪,基本就过关了。

本章小结(1.4)refresh() 是 Spring
容器「从定义到实例」的完整生命周期入口,12 步里有 4 步是关键——⑤
处理配置类与自动装配、⑥ 注册代理创建器、⑨ 启动 Tomcat、⑪
实例化所有单例。

1.5 内嵌 Tomcat
的启动时机(高频易错点)

很多人以为「Tomcat 在 main 里就启动了」或者「Tomcat
在所有 Bean
创建完之后才启动」,其实都不准确。真实的启动分两段,分别发生在
refresh() 的第 ⑨ 步和第 ⑫ 步。

第一段:onRefresh() 创建并初始化
Tomcat

源码位置:ServletWebServerApplicationContext#onRefresh()
createWebServer()
TomcatServletWebServerFactory#getWebServer()

// ServletWebServerApplicationContext
@Override
protected void onRefresh() {
    super.onRefresh();
    try {
        // 关键:在 refresh 的第 ⑨ 步就创建 WebServer(Tomcat)
        createWebServer();
    }
    catch (Throwable ex) {
        throw new ApplicationContextException("Unable to start web server", ex);
    }
}

createWebServer() 最终会
new TomcatWebServer(...),其构造器里调用
initialize()

// TomcatWebServer
private void initialize() throws WebServerException {
    synchronized (this.monitor) {
        try {
            ...
            // 启动 Tomcat 引擎,触发初始化监听器;注意:此时「移除了 Connector」,还没绑定端口!
            this.tomcat.start();
            ...
        }
        catch (Exception ex) {
            throw new WebServerException("Unable to start embedded Tomcat", ex);
        }
    }
}

注意注释里的关键信息:tomcat.start() 启动的是
Tomcat 引擎(Engine/Host/Context),用来加载
Servlet、Filter 等,但此时 Connector(也就是 8080
端口)还没真正绑定
。这是为了防止「Bean
还没创建完,外部请求就打进来了」。

第二段:finishRefresh()
真正绑定端口

源码位置:AbstractApplicationContext#finishRefresh()
LifecycleProcessor#onRefresh()
WebServerStartStopLifecycle#start()
TomcatWebServer#start()

// TomcatWebServer
@Override
public void start() throws WebServerException {
    synchronized (this.monitor) {
        if (this.started) {
            return;
        }
        try {
            // 把之前移除的 Connector 重新加回来 → 绑定 8080 端口,开始接收请求
            addPreviouslyRemovedConnectors();
            Connector connector = this.tomcat.getConnector();
            if (connector != null && this.autoStart) {
                performDeferredLoadOnStartup();
            }
            checkThatConnectorsHaveStarted();
            this.started = true;
            logger.info("Tomcat started on port(s): " + getPortsDescription(true));
        }
        catch (ConnectorStartFailedException ex) {
            stopSilently();
            throw ex;
        }
    }
}

TomcatWebServer 实现了 SmartLifecycle
接口,所以它会被注册成一个「生命周期 Bean」。在
finishRefresh() 里,LifecycleProcessor
会遍历所有 SmartLifecycle Bean 并调用它们的
start(),此时 Tomcat 才真正绑定端口、开始监听 8080。

完整时序(内嵌 Tomcat 部分)

refresh()
  ├─ ⑨ onRefresh()
  │     └─ createWebServer() → new TomcatWebServer() → initialize()
  │           └─ tomcat.start()   // 引擎启动,但移除 Connector,端口未绑定
  ├─ ⑪ finishBeanFactoryInitialization()   // 所有单例 Bean 创建完成
  └─ ⑫ finishRefresh()
        └─ LifecycleProcessor.onRefresh() → TomcatWebServer.start()
              └─ addPreviouslyRemovedConnectors()  // 绑定 8080,开始接收请求

为什么设计成两段:先启动引擎(⑨)是为了让后续 Bean
创建过程中,能拿到一个「已经初始化好的 ServletContext」去注册
Servlet/Filter;最后绑定端口(⑫)是为了保证「端口真正对外可用时,容器里的
Bean 已经全部就绪」,避免「请求打进来了但 Controller
还没创建」的尴尬窗口。

本章小结(1.5):内嵌 Tomcat
启动分两段——onRefresh()
初始化引擎(不绑端口),finishRefresh() 通过
LifecycleProcessor 绑定端口,保证「对外可用 =
容器就绪」。

1.6 断点跟踪实战

光读代码不够,按下述步骤亲手断点走一遍:

  1. SpringApplication#run(String...)
    的第一行打断点,Debug 启动。
  2. Step OverprepareEnvironment(...),观察
    environment 里已经读到了 application.yml
    的配置。
  3. 进入 refreshContext(...) 再进入
    AbstractApplicationContext#refresh(),逐行
    Step Over
  4. onRefresh()Step Into,一路跟到
    TomcatWebServer#initialize(),看
    tomcat.start()
  5. 回到 finishRefresh()Step Into
    TomcatWebServer#start(),此时控制台会打印
    Tomcat started on port(s): 8080
  6. 最后看 listeners.ready(...) 触发
    ApplicationReadyEvent

提示:启动流程里框架代码很多,建议用 IDEA 的 Step Out /
断点条件过滤
,不要一路 Step Into
到底,否则会陷进反射细节里。跟着上面的「关键 4 步」走即可。


4.2 自动配置源码剖析

本章目标:搞懂 @SpringBootApplication
背后「自动配置」的三段链路——入口注解、选择器、条件过滤,并掌握用
--debug 排查「为什么不生效」的武器。

2.1
自动配置的入口:@SpringBootApplication

先看这个你写了无数遍的注解,它其实是一个「三合一」的复合注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration        // ① 标记这是配置类(等价于 @Configuration)
@EnableAutoConfiguration        // ② 开启自动配置(本节的绝对主角)
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
                                // ③ 组件扫描,但排除自动配置类(避免被扫两次)
public @interface SpringBootApplication {
    ...
}

三个部分的分工:

注解 作用 对应阶段
@SpringBootConfiguration 让主类成为配置类,可用 @Bean 定义 Bean 阶段 1 已学
@EnableAutoConfiguration 触发自动配置机制 本章重点
@ComponentScan 扫描主类所在包及其子包的 @Component 阶段 2 已学

我们只看 @EnableAutoConfiguration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage              // 记录「主类所在包」,用于 @Entity 扫描等
@Import(AutoConfigurationImportSelector.class)   // 核心:导入一个「选择器」
public @interface EnableAutoConfiguration {
    ...
}

@Import 是 Spring
的通用机制:可以导入普通配置类、ImportSelectorImportBeanDefinitionRegistrar
三种。这里导入的是 AutoConfigurationImportSelector——它会在
refresh() 的第 ⑤ 步
invokeBeanFactoryPostProcessors() 中被处理。

本章小结(2.1):自动配置的开关是
@EnableAutoConfiguration,它通过
@Import(AutoConfigurationImportSelector.class)
引入一个「选择器」,由选择器决定导入哪些自动配置类。

2.2
AutoConfigurationImportSelector 源码走读

源码位置:AutoConfigurationImportSelector。它实现了
DeferredImportSelector(延迟导入选择器)、BeanClassLoaderAwareEnvironmentAware
等接口。

为什么是 DeferredImportSelector(延迟导入)?

普通的 ImportSelector 会立即返回要导入的类;而
DeferredImportSelector
会等「所有用户配置类都处理完」之后,最后才处理。这保证了:你的
@Configuration@Bean
先注册,自动配置类后注册,这样自动配置里的
@ConditionalOnMissingBean
才能正确判断「用户是不是已经自己定义了同类型
Bean」。这个「用户优先」的顺序是理解自动配置可被覆盖的关键。

核心方法 getAutoConfigurationEntry()
的走读(精简):

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    // ① 拿到 @EnableAutoConfiguration 注解的属性(exclude、excludeName)
    AnnotationAttributes attributes = getAttributes(annotationMetadata);

    // ② 核心:从 META-INF/spring/...AutoConfiguration.imports 文件读出「候选自动配置类」全限定名
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);

    // ③ 去重
    configurations = removeDuplicates(configurations);

    // ④ 处理手动排除的类(@EnableAutoConfiguration(exclude = ...) 或 spring.autoconfigure.exclude 配置)
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    configurations.removeAll(exclusions);

    // ⑤ 过滤:过滤掉 @AutoConfigureAfter/@AutoConfigureBefore 依赖关系不满足的(在 3.0 后基本靠 @AutoConfiguration 的顺序声明)
    configurations = getConfigurationClassFilter().filter(configurations);

    // ⑥ 触发 AutoConfigurationImportEvent(供 Actuator 等监听)
    fireAutoConfigurationImportEvents(configurations, exclusions);

    // ⑦ 返回「类全限定名 + 排除集合」
    return new AutoConfigurationEntry(configurations, exclusions);
}

getCandidateConfigurations() 内部(Spring Boot 3.x
版本):

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    // 关键:从指定的 imports 文件位置读取(3.x 已从 spring.factories 迁移到 .imports 文件)
    List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
            .getCandidates();
    Assert.notEmpty(configurations,
            "No auto configuration classes found in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.");
    return configurations;
}

ImportCandidates.load() 会去 classpath 里找到所有名为
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
的文件,把它们的内容(每行一个类全限定名)合并成一个候选列表。这个列表在
Spring Boot 3.2.x
里约有两百多个类(DataSourceAutoConfigurationRedisAutoConfigurationSecurityAutoConfiguration
等)。

读到候选类之后呢?
这些候选类会作为「待导入的配置类」进入容器,但并不是全部生效——每个自动配置类上都挂着一堆
@ConditionalOnXxx
注解,注册时会逐个判断,不满足条件的直接跳过。这就是 2.4 节的内容。

本章小结(2.2)AutoConfigurationImportSelector
负责「读 imports 文件 → 拿到候选列表 → 排除/排序 →
交给容器逐个加载」,它本身不创建 Bean,只是决定「把哪些配置类交给 Spring
处理」。

2.3
AutoConfiguration.imports 文件(3.x 与 2.x 的重大差异)

这是 Spring Boot 3.x
的一个重大变更,面试和排查都常考:

版本 配置文件 键/格式
Spring Boot 2.6 及以前 META-INF/spring.factories org.springframework.boot.autoconfigure.EnableAutoConfiguration=
后跟逗号分隔的类名
Spring Boot 2.7 ~ 3.x META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 每行一个类全限定名

新的 .imports 文件示例(节选自
spring-boot-autoconfigure 包):

# 文件位置: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.orm.jpa.HibernateJpaAutoConfiguration
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
...

为什么要从 spring.factories 迁移出来? 旧版
spring.factories 是一个「万能文件」,所有 SPI
都往里面塞,键多了之后文件巨大、加载慢、容易冲突;新版的
.imports 文件「一文件一职责」,只在需要时按
ImportCandidates.load()
按需读取,加载更高效、语义更清晰。

同时,Spring Boot 3.x 引入了 @AutoConfiguration
注解来标记自动配置类:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration(proxyBeanMethods = false)   // 内部就是一个 @Configuration
public @interface AutoConfiguration {

    // 声明「在哪个自动配置类之后」加载,取代旧的 @AutoConfigureAfter 的组合方式
    @AliasFor(annotation = Configuration.class)
    String value() default "";

    String[] after() default {};

    String[] before() default {};
}

注意 proxyBeanMethods = false:自动配置类默认不启用
CGLIB 代理(不拦截 @Bean 方法互相调用),因为自动配置类里的
@Bean
方法通常不会互相调用,省掉代理能提升启动性能。这是一个很细节的性能优化点。

本章小结(2.3):Spring Boot 3.x 用
AutoConfiguration.imports 文件(每行一个类)取代了旧的
spring.factories,并用
@AutoConfigurationproxyBeanMethods = false)标记自动配置类。

2.4 @Conditional
家族:自动配置的「筛选器」

候选类有 200 多个,但你的项目不会同时用
MySQL、Redis、MongoDB、RabbitMQ……所以每个自动配置类上都有一组「生效条件」,只有全部满足才会真正注册其内部的
@Bean。这些条件注解统称 @Conditional
家族:

注解 生效条件 典型用途
@ConditionalOnClass classpath 存在指定类 RedisTemplate 才配 Redis
@ConditionalOnMissingClass classpath 不存在指定类 无某依赖时走降级逻辑
@ConditionalOnBean 容器已有指定 Bean 依赖别的 Bean 先创建
@ConditionalOnMissingBean 容器没有指定 Bean 用户没自定义才用默认值
@ConditionalOnProperty 配置项满足条件 spring.xxx.enabled=true
@ConditionalOnWebApplication 当前是 Web 应用 只配 Web 相关 Bean
@ConditionalOnExpression SpEL 表达式为 true 复杂组合条件

看一个真实例子——DataSourceAutoConfiguration
的开头(简化):

@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })   // 有数据源相关类
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")       // 不是响应式数据源
@EnableConfigurationProperties(DataSourceProperties.class)               // 绑定 spring.datasource.* 配置
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, ... })
public class DataSourceAutoConfiguration {
    // 内部还有一堆 @ConditionalOnMissingBean 的 @Bean 方法
}

@ConditionalOnMissingBean
是「用户可覆盖默认配置」的基石
。它的判断时机是「当前
BeanDefinition 注册阶段」,即:如果用户已经通过
@Component@Bean@Import
等方式把同类型 Bean
注册过了,自动配置就跳过。正因为「用户配置先处理(DeferredImportSelector)→
自动配置后处理」,这个判断才能成立。

本章小结(2.4)@Conditional
家族是自动配置的筛选器,@ConditionalOnClass
解决「有无依赖」,@ConditionalOnMissingBean
解决「用户是否已自定义」,两者是 200
多个候选类能「精准生效」的核心。

2.5
自动配置报告:–debug 排查「为什么不生效」

当某个自动配置「感觉应该生效却没生效」时,别猜,直接看报告:

# 打包后运行
java -jar target/advanced-principles-0.0.1-SNAPSHOT.jar --debug

# 或开发期直接在 IDEA 里加启动参数
# Run Configuration → Program arguments → 填入:--debug

启动日志里会多出两大段:

============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:            ← 已生效的自动配置
-----------------
   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'javax.sql.DataSource', ...
   WebMvcAutoConfiguration matched:
      - @ConditionalOnClass found required classes ...

Negative matches:            ← 未生效的自动配置 + 原因
-----------------
   RedisAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'org.springframework.data.redis.core.RedisTemplate'
   KafkaAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'org.springframework.kafka.core.KafkaTemplate'

怎么用

  1. 你要的自动配置没有出现在 Positive matches
    大概率是 @ConditionalOnClass 不满足(缺依赖)。
  2. 出现在 Negative matches → 看
    Did not match:
    下面列出的具体原因,通常是「缺了某个类」或「某个属性为 false」。
  3. 你自定义的配置被覆盖 → 看它是否因为
    @ConditionalOnMissingBean 没匹配(说明容器里已有同类型
    Bean)。

2.6
手写一个最小自动配置(为第 5 章打基础)

在正式写完整 Starter
之前,先用手写最小自动配置理解机制。假设我们要自动注册一个
UserService

package com.example.advancedprinciples.autoconfigure;

// 自动配置类:@AutoConfiguration 标记,proxyBeanMethods=false 关闭代理
@AutoConfiguration
// 只有当 UserService 相关类存在时才生效
@ConditionalOnClass(UserService.class)
// 绑定配置属性(可选,这里先演示无属性版本)
public class UserServiceAutoConfiguration {

    // 用户没自己定义 UserService 时才注册
    @Bean
    @ConditionalOnMissingBean
    public UserService userService() {
        // 生产级:这里应做参数校验、日志等,此处最小示例从简
        return new UserService();
    }
}

配套的 imports 文件:

# src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.advancedprinciples.autoconfigure.UserServiceAutoConfiguration

启动后,若容器里没有
UserService,它会被自动注册;一旦你在某处自己
@Bean 了一个 UserService,这个自动配置就会因为
@ConditionalOnMissingBean
而跳过。这个「先自动、可覆盖」的模式,就是所有官方 Starter
的底层逻辑。

本章小结(2.6):最小自动配置 =
@AutoConfiguration 类 + @Conditional 过滤 +
imports 文件注册 + @ConditionalOnMissingBean
留覆盖口。这套模板在第 5 章会扩展成完整 Starter。


4.3 Bean 生命周期与循环依赖

本章目标:讲透 Spring
的三级缓存——每一级存什么、循环依赖如何被解决、以及面试必考的「为什么是三级而不是两级」。这是本篇最核心、最需要吃透的一章。

3.1 Bean 的完整生命周期

要理解循环依赖,先要知道一个 Bean
从「定义」到「可被注入」要经历哪些阶段:

BeanDefinition(定义,来自 @Component 扫描 / @Bean 注册)
   │
   ▼
① 实例化 instantiate            createBeanInstance()
   │    通过构造器 new 出对象,此时属性都是 null
   ▼
② 属性填充 populate             populateBean()
   │    给 @Autowired / @Resource 字段、setter 注入依赖
   ▼
③ 初始化 initialize             initializeBean()
   │    执行 Aware 回调 → BeanPostProcessor#postProcessBeforeInitialization
   │    → InitializingBean#afterPropertiesSet → @PostConstruct 自定义 init 方法
   │    → BeanPostProcessor#postProcessAfterInitialization(AOP 代理在这里创建!)
   ▼
④ 就绪的单例 Bean               放入一级缓存 singletonObjects,可被正常使用

关键洞察:循环依赖问题的本质,是「实例化(①)和属性填充(②)之间」存在一个时间窗口。在这个窗口里,Bean
A 的对象引用已经存在(new
出来了),但属性还没填充完。如果此时 B 需要引用
A,Spring 就得想办法「提前暴露」A
的这个半成品引用——三级缓存就是干这个的。

本章小结(3.1):Bean 生命周期四阶段「实例化 →
属性填充 → 初始化 →
就绪」,循环依赖发生在「实例化完成、属性未填充完」的窗口期。

3.2 三级缓存是什么

三级缓存定义在 DefaultSingletonBeanRegistry 里,是三个
Map

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

    /** 一级缓存:存「完整可用的单例 Bean」(经过实例化+填充+初始化+可能的 AOP 代理) */
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    /** 二级缓存:存「提前暴露的半成品 Bean」(已实例化,未填充完,可能是代理) */
    private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

    /** 三级缓存:存「能生产半成品 Bean 的工厂 ObjectFactory */
    private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

    /** 正在创建中的 Bean 名称集合(标记「这个 Bean 正在创建,还没完工」) */
    private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));
}
缓存 变量名 存的是什么 存进去的时机
一级 singletonObjects 完整可用的单例 Bean Bean 初始化完成后
二级 earlySingletonObjects 提前暴露的半成品 Bean(可能是代理对象) 从三级缓存「取出来」时
三级 singletonFactories 能生产半成品 Bean 的 ObjectFactory 工厂 Bean 实例化之后、属性填充之前

为什么三级缓存里存的是「工厂」而不是「对象」?
这是理解整章的关键,我们放到 3.4
节详细讲。先记住结论:三级缓存存工厂,是为了延迟决定「到底要返回原始对象,还是返回
AOP 代理对象」。

3.3 循环依赖如何被解决

假设 UserServiceOrderService
互相注入(这是一个真实业务里常见的反模式,本章用它来演示机制):

package com.example.advancedprinciples.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {

    // 字段注入:先演示字段注入能被三级缓存解决(构造器注入见 3.5)
    private final OrderService orderService;

    public void createUser(Long userId) {
        log.info("创建用户 {}", userId);
    }
}
package com.example.advancedprinciples.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {

    private final UserService userService;

    public void createOrder(Long orderId) {
        log.info("创建订单 {}", orderId);
    }
}

注意:这里故意用了 @RequiredArgsConstructor(Lombok
生成的构造器注入),其实是构造器注入。构造器注入的循环依赖无法被三级缓存解决,会直接抛
BeanCurrentlyInCreationException。为了演示「能被解决的循环依赖」,应该用字段注入@Autowired
字段)或 setter 注入。下面修正为字段注入版本:

package com.example.advancedprinciples.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class UserService {

    // 字段注入:循环依赖的演示场景专用。
    // 生产环境请优先构造器注入(见 3.5 说明),这里仅用于观察三级缓存机制。
    @Autowired
    private OrderService orderService;

    public void createUser(Long userId) {
        log.info("创建用户 {}", userId);
    }
}
package com.example.advancedprinciples.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class OrderService {

    @Autowired
    private UserService userService;

    public void createOrder(Long orderId) {
        log.info("创建订单 {}", orderId);
    }
}

启动后不会报错。我们跟踪
UserService → OrderService → UserService 的创建过程:

创建 UserService
  ① 实例化 UserService(new 出对象,orderService 字段还是 null)
  ② 把 UserService 的 ObjectFactory 放入三级缓存 singletonFactories
  ③ 填充 UserService.orderService  → 需要创建 OrderService
        ├─ 实例化 OrderService
        ├─ 把 OrderService 的 ObjectFactory 放入三级缓存
        ├─ 填充 OrderService.userService → 需要 UserService
        │      └─ getSingleton("userService")
        │           ├─ 一级缓存没有(还没创建完)
        │           ├─ 二级缓存没有
        │           └─ 三级缓存有!→ 执行 ObjectFactory.getObject() 得到 UserService 半成品
        │              (若需要 AOP,这里可能返回代理对象)
        │              → 把结果放入二级缓存 earlySingletonObjects
        │              → 从三级缓存移除
        │      → OrderService.userService 注入完成
        ├─ OrderService 初始化完成 → 放入一级缓存 singletonObjects
  ④ UserService.orderService 注入完成
  ⑤ UserService 初始化完成 → 放入一级缓存 singletonObjects
  ⑥ 清理:把 UserService 从二级缓存移除(最终只保留一级缓存)

对应的核心方法 getSingleton()
走读(源码位置:DefaultSingletonBeanRegistry#getSingleton(String, boolean)):

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    // 第一层:从一级缓存拿完整 Bean
    Object singletonObject = this.singletonObjects.get(beanName);
    // 一级没有,且该 Bean 正在创建中(说明发生了循环依赖)
    if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
        // 第二层:从二级缓存拿「提前暴露的半成品」
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            synchronized (this.singletonObjects) {
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                        // 第三层:从三级缓存拿「工厂」,现场生产半成品
                        ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();  // 可能触发 AOP 代理创建
                            // 生产出来后,升级到二级缓存,并从三级缓存移除(工厂只执行一次)
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }
    return singletonObject;
}

注意这里体现了三级缓存的一个升级规则:Bean
从三级缓存「生产出来」后会立即升级到二级缓存,并且从三级缓存移除。这保证
ObjectFactory.getObject() 只会被调用一次——否则每次拿 A
的提前引用都重新执行工厂,可能产生多个不同的代理对象,导致「注入到 B 的
A」和「最终就绪的 A」不是同一个对象。

本章小结(3.3):循环依赖通过「实例化后立刻把工厂放进三级缓存
→ 其他 Bean 需要时从三级缓存取半成品并升级到二级 →
全部完成后升入一级」来解决,且工厂只执行一次。

3.4
为什么是三级而非两级(面试必考核心)

这是全篇最需要讲透的问题。先给出一句话答案

三级缓存里存的是工厂而不是对象,因为
Bean 可能在后面被 AOP
代理;只有用工厂,才能在循环依赖发生时「现场决定」返回原始对象还是代理对象,从而保证「提前暴露的半成品」和「最终就绪的
Bean」是同一个(代理)对象。

如果只用两级缓存(去掉三级,二级直接存半成品对象)会怎样?

假设 UserService 既参与了循环依赖,又要被 AOP
代理(比如它上面有 @Transactional 或自定义切面):

  1. UserService
    实例化后,如果两级缓存直接存原始对象,那么
    OrderService 注入的 userService
    就是原始对象,没经过 AOP 代理。
  2. UserService 初始化完成、创建 AOP
    代理后,一级缓存里放的是代理对象
  3. 结果:OrderService.userService 是原始对象,容器里的
    userService
    是代理对象,两个不是同一个对象,事务、切面逻辑在
    OrderService 注入的那个引用上全部失效。

三级缓存怎么解决这个问题?

关键在 ObjectFactory.getObject()
的实现。实例化后放进三级缓存的那个工厂,内部会调用
getEarlyBeanReference()

// AbstractAutowireCapableBeanFactory#doCreateBean 中,实例化后立即注册工厂
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

// 工厂内部
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    Object exposedObject = bean;
    if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
        for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {
            // AbstractAutoProxyCreator 会在这里判断:要不要创建代理?
            // 要 → 返回代理对象;不要 → 返回原始对象
            exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);
        }
    }
    return exposedObject;
}

AbstractAutoProxyCreator#getEarlyBeanReference()
会做「提前创建代理」的判断。所以流程变成:

  1. UserService 实例化后,放进三级缓存的工厂「有 AOP
    能力」。
  2. OrderService 需要 userService
    时,调用工厂,工厂发现 UserService
    需要代理,当场创建一个代理对象返回,并放进二级缓存。
  3. 后面 UserService 初始化完成,真正做 AOP
    时,AbstractAutoProxyCreator#postProcessAfterInitialization
    会发现「早先已经为它创建过代理了」,复用二级缓存里那个代理对象(通过
    earlyProxyReferences 标记),而不是再创建一个新的。
  4. 最终一级缓存里的 userService
    OrderService 注入的 userService
    同一个代理对象

用一张表总结:

方案 循环依赖时注入的对象 最终就绪的对象 是否一致
两级(二级直接存原始对象) 原始对象 代理对象 不一致 → 事务失效
三级(三级存工厂,工厂决定是否代理) 代理对象(工厂现场生成) 同一个代理对象(复用) 一致

为什么不能只把「是否代理」提前到实例化阶段就决定?
因为 Bean 是否需要
AOP,取决于它「初始化完成后」是否符合切点匹配条件(@Transactional、切面表达式等);而循环依赖发生时,Bean
还没初始化完,Spring 无法在「实例化阶段」就 100%
确定要不要代理。所以它用一个「工厂」把「判断 +
创建」这个动作延迟到了「真正被需要」的那一刻,那一刻能拿到的信息更充分。

本章小结(3.4):三级缓存是「为 AOP
代理服务」的。三级存工厂,是为了在循环依赖发生时「延迟并一次性」地决定返回原始对象还是代理对象,保证提前暴露的对象与最终对象是同一个代理,从而让事务和切面在循环依赖场景下依然有效。

3.5
构造器注入为什么「暴露」循环依赖

上面的例子用字段注入能解决循环依赖。如果你用构造器注入(阶段 1
强烈推荐的方式),循环依赖会直接启动失败:

package com.example.advancedprinciples.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {

    private final OrderService orderService;   // 构造器注入

    public void createUser(Long userId) {
        log.info("创建用户 {}", userId);
    }
}
package com.example.advancedprinciples.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {

    private final UserService userService;   // 构造器注入

    public void createOrder(Long orderId) {
        log.info("创建订单 {}", orderId);
    }
}

启动报错:

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  orderService defined in file [.../OrderService.class]
↑     ↓
|  userService defined in file [.../UserService.class]
└─────┘

为什么构造器注入无法被三级缓存解决? 回到 3.1
的生命周期:

  • 字段注入发生在「② 属性填充」阶段,此时 Bean 已经 new
    出来了(实例化完成),所以能提前暴露引用。
  • 构造器注入发生在「①
    实例化」阶段——new UserService(orderService)
    必须先拿到 orderService 才能完成 new。而此刻
    UserService
    的对象还不存在,根本没东西可往三级缓存里放,循环依赖无解。

这是特性,不是
bug
。构造器注入让循环依赖「无处遁形、立即报错」,逼迫你在设计阶段就发现「两个
Service
互相依赖」这种糟糕设计,而不是用字段注入把循环依赖藏起来、留到线上变成难以排查的问题。所以阶段
1
推荐的构造器注入,与本章的字段注入演示并不矛盾:生产用构造器注入(暴露问题),理解原理时用字段注入(观察三级缓存)

本章小结(3.5):构造器注入发生在实例化阶段,对象尚未诞生无法提前暴露,因此循环依赖直接报错——这正是它作为「设计问题探测器」的价值。

3.6
生产级案例:如何正确拆解循环依赖

既然循环依赖是设计问题,正确做法是「拆掉」而不是「容忍」。三种手段:

手段一:提取第三个类(最常见)。把互相引用的逻辑下沉到一个新的
Service:

package com.example.advancedprinciples.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class OrderUserFacadeService {

    private final UserService userService;
    private final OrderService orderService;

    public void createOrderAndUser(Long userId, Long orderId) {
        // 组合两个服务,而不是让两个服务互相引用
        userService.createUser(userId);
        orderService.createOrder(orderId);
        log.info("组合服务完成:用户 {} 与订单 {}", userId, orderId);
    }
}

这样 UserServiceOrderService
之间不再互相依赖,各自只依赖自己需要的,Facade
统一编排。

手段二:用 @Lazy
打破构造期依赖
(治标,谨慎使用):

@Service
@RequiredArgsConstructor
public class UserService {

    // @Lazy 让注入的是一个「代理占位符」,真正用到时才去容器里取,从而打破构造期的循环
    @Lazy
    private final OrderService orderService;
}

@Lazy 会让 Spring
注入一个延迟代理,构造时不需要真实对象,等第一次调用
orderService.xxx()
时才真正解析。这能解决启动报错,但只是把「循环依赖」变成了「延迟解析」,设计问题还在,只适合临时救急。

手段三:用事件驱动解耦UserService
创建用户后发布事件,OrderService
监听事件,两者完全解耦:

package com.example.advancedprinciples.event;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

// 领域事件:用户已创建
@Getter
@RequiredArgsConstructor
public class UserCreatedEvent {
    private final Long userId;
}
@Service
@RequiredArgsConstructor
public class UserService {

    private final ApplicationEventPublisher publisher;

    public void createUser(Long userId) {
        // 业务落库……
        // 发布事件,不直接依赖 OrderService
        publisher.publishEvent(new UserCreatedEvent(userId));
    }
}
@Service
@RequiredArgsConstructor
public class OrderService {

    // 监听用户创建事件,两者零直接依赖
    @EventListener
    public void onUserCreated(UserCreatedEvent event) {
        // 用户创建后,为新用户初始化订单相关逻辑
    }
}

事件驱动是拆循环依赖最优雅的方式之一,但它引入了「异步/同步语义」与「事务边界」的复杂性(@EventListener
默认同步、同事务,改成 @Async
则异步、跨事务),需要按业务谨慎选择。

本章小结(3.6):循环依赖应「拆」而非「忍」,手段依次为:提取
Facade
类(推荐)、事件驱动解耦(优雅但有事务语义成本)、@Lazy(临时救急)。


4.4 AOP 与事务底层

本章目标:从源码角度讲清 AOP 的两种代理(JDK 动态代理 vs
CGLIB)、代理对象是在哪一步被创建的,以及 @Transactional
本质是「环绕通知」。同时用源码解释阶段 1 遗留的「自调用失效」之谜。

4.1 AOP 的核心概念快速回顾

AOP(Aspect-Oriented
Programming,面向切面编程)把「日志、事务、权限」这类横切关注点从业务代码里剥离出来。核心术语:

术语 含义
切面 Aspect 横切关注点的模块化,一个类 + @Aspect 注解
通知 Advice 切面在「何时」执行,如
@Before@After@Around@AfterReturning@AfterThrowing
切点 Pointcut 「哪些方法」要被拦截,用 execution(...) 表达式匹配
连接点 JoinPoint 程序执行的某个点,如方法调用
织入 Weaving 把切面应用到目标对象、生成代理对象的过程

Spring AOP
的底层是动态代理:它不修改你的原始类,而是生成一个「代理对象」包裹原始对象。调用方拿到的是代理对象,代理对象在执行目标方法前后插入通知逻辑。Spring
AOP 的织入发生在运行期(区别于 AspectJ
的编译期/类加载期织入)。

4.2 JDK 动态代理

JDK 动态代理基于
java.lang.reflect.Proxy必须基于接口。它会在运行期生成一个「实现同一接口」的代理类。

最小可用示例

package com.example.advancedprinciples.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 业务接口:JDK 动态代理的前提是必须有接口
public interface UserService {
    void createUser(Long userId);
}
package com.example.advancedprinciples.proxy;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class UserServiceImpl implements UserService {

    @Override
    public void createUser(Long userId) {
        // 模拟业务逻辑
        log.info("业务方法执行:创建用户 {}", userId);
    }
}
package com.example.advancedprinciples.proxy;

import lombok.extern.slf4j.Slf4j;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

@Slf4j
public class JdkProxyDemo {

    public static void main(String[] args) {
        // 目标对象:原始实现
        UserService target = new UserServiceImpl();

        // 生成代理对象:Proxy.newProxyInstance(类加载器, 目标接口数组, 调用处理器)
        UserService proxy = (UserService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxyObj, Method method, Object[] methodArgs) throws Throwable {
                        // 前置通知
                        log.info("【代理】方法 {} 执行前,记录日志", method.getName());
                        // 调用真实目标对象的方法
                        Object result = method.invoke(target, methodArgs);
                        // 后置通知
                        log.info("【代理】方法 {} 执行后,记录日志", method.getName());
                        return result;
                    }
                });

        // 调用方拿到的 proxy 是一个「实现 UserService 接口」的全新对象
        proxy.createUser(1001L);
    }
}

运行输出:

【代理】方法 createUser 执行前,记录日志
业务方法执行:创建用户 1001
【代理】方法 createUser 执行后,记录日志

JDK 动态代理的本质:生成一个 $Proxy0
类,它实现了 UserService 接口,内部持有一个
InvocationHandler。任何接口方法调用都会转交给
InvocationHandler.invoke(),由它在调用真实对象前后加料。局限:目标类必须实现接口;如果目标类没有接口,JDK
动态代理无能为力。

4.3 CGLIB 代理

CGLIB(Code Generation
Library)基于继承:它生成目标类的子类,通过重写父类方法来实现拦截,不需要接口

package com.example.advancedprinciples.proxy;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

@Slf4j
public class CglibProxyDemo {

    // 目标类:注意「没有接口」,JDK 动态代理做不了,CGLIB 可以做
    public static class OrderService {
        public void createOrder(Long orderId) {
            log.info("业务方法执行:创建订单 {}", orderId);
        }
    }

    public static void main(String[] args) {
        // Enhancer 是 CGLIB 的入口,用来生成子类
        Enhancer enhancer = new Enhancer();
        // 指定父类:生成的子类会继承 OrderService
        enhancer.setSuperclass(OrderService.class);
        // 设置方法拦截器
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] methodArgs, MethodProxy methodProxy) throws Throwable {
                log.info("【CGLIB】方法 {} 执行前", method.getName());
                // 调用父类(原始)方法
                Object result = methodProxy.invokeSuper(obj, methodArgs);
                log.info("【CGLIB】方法 {} 执行后", method.getName());
                return result;
            }
        });

        // 生成代理对象:它是 OrderService 的子类
        OrderService proxy = (OrderService) enhancer.create();
        proxy.createOrder(2001L);
    }
}

运行输出:

【CGLIB】方法 createOrder 执行前
业务方法执行:创建订单 2001
【CGLIB】方法 createOrder 执行后

CGLIB 的本质:生成
OrderService$$SpringCGLIB$$0 子类,重写
createOrder 方法,方法体里先回调
MethodInterceptor.intercept()局限:不能代理
final 类(无法被继承)和 final
方法(无法被重写)。

4.4 JDK vs CGLIB
对照(面试高频)

维度 JDK 动态代理 CGLIB 代理
原理 基于接口,生成接口的实现类 基于继承,生成目标类的子类
是否要求接口 必须实现接口 无需接口
能否代理 final 类/方法 可以(不涉及继承) 不能(final 无法继承/重写)
创建代理的速度 快(纯反射,无需生成字节码的额外开销大) 慢(要生成并加载子类字节码)
运行速度 较慢(反射调用 Method.invoke 较快(直接子类方法调用)
包名 java.lang.reflect.Proxy Spring 内嵌 org.springframework.cglib

Spring Boot 3.x 默认用哪个?

答案是 CGLIB。从 Spring Framework 5.x 开始,CGLIB
不再作为可选依赖,而是被重新打包内嵌
spring-core(包名
org.springframework.cglib)。在 Spring Boot
里,spring.aop.proxy-target-class 默认值为
true,意味着即使目标类实现了接口,也默认用
CGLIB
(除非你显式配置
spring.aop.proxy-target-class=false)。

# application.yml(可选)—— 强制回到「有接口用 JDK,无接口用 CGLIB」的老策略
spring:
  aop:
    proxy-target-class: false   # 默认 true,即默认 CGLIB

为什么默认转向 CGLIB? 三个原因:

  1. 统一行为:不再因为「有没有接口」而导致代理策略不同,行为更可预期。
  2. 性能:CGLIB 运行期是直接子类调用,比 JDK
    反射调用更快。
  3. 功能:JDK 代理只能代理接口方法,CGLIB
    能代理类里的所有非 final 方法,覆盖更全。

怎么确认运行时到底用了哪个代理? 在任意被代理的 Bean
上打断点或打印类名:

@RestController
@RequiredArgsConstructor
public class ProxyCheckController {

    private final UserService userService;

    @GetMapping("/proxy/type")
    public String proxyType() {
        // 若输出含 "$$SpringCGLIB$$" 说明是 CGLIB;含 "$Proxy" 说明是 JDK 动态代理
        return userService.getClass().getName();
    }
}

典型输出:com.example.advancedprinciples.service.UserServiceImpl$$SpringCGLIB$$0,确认是
CGLIB。

本章小结(4.4):JDK 动态代理要求接口、CGLIB
基于继承;Spring Boot 3.x 默认
CGLIB(proxy-target-class=true),可通过类名里的
$$SpringCGLIB$$$Proxy
判断实际用的哪种。

4.5 BeanPostProcessor
与代理创建时机

代理对象不是凭空出现的,它的创建有一个明确的时机和「责任人」。

责任人AbstractAutoProxyCreator(其实现类
AnnotationAwareAspectJAutoProxyCreator),它实现了
SmartInstantiationAwareBeanPostProcessor——本质是一个
BeanPostProcessor

创建时机:回到 3.1 的生命周期,AOP 代理创建发生在「③
初始化」阶段的 postProcessAfterInitialization()

initializeBean()
  ├─ applyBeanPostProcessorsBeforeInitialization(bean)   // @PostConstruct 之前的后处理器
  ├─ invokeInitMethods(bean)                            // afterPropertiesSet + init 方法
  └─ applyBeanPostProcessorsAfterInitialization(bean)   // ← AOP 代理在这里创建
        └─ AbstractAutoProxyCreator#postProcessAfterInitialization
              ├─ wrapIfNecessary(bean, beanName, cacheKey)
              │     ├─ 判断 bean 是否命中切点(@Transactional / @Aspect 切面)
              │     ├─ 命中 → 创建代理(JDK 或 CGLIB)
              │     └─ 未命中 → 返回原始 bean
              └─ 返回代理对象替换原始 bean(放入一级缓存的是代理对象)

关键点:Spring
容器一级缓存里最终放的是代理对象,原始对象被代理对象内部引用。这就是为什么你在
Controller 里 @Autowired 注入的
UserService,拿到的 getClass()
$$SpringCGLIB$$ 子类——注入的是代理。

判断「要不要代理」的依据AbstractAutoProxyCreator#getAdvicesAndAdvisorsForBean()
会收集所有 Advisor(切面 + 事务拦截器都会生成
Advisor),用切点表达式去匹配当前 bean
的类和方法;有匹配才代理。这就是为什么「加了 @Transactional
才被代理、没加就不代理」。

本章小结(4.5):AOP 代理由
BeanPostProcessorAbstractAutoProxyCreator)在
initializeBean
postProcessAfterInitialization 阶段创建,命中切点的 Bean
会被代理对象替换。

4.6 @Transactional
是环绕通知(事务底层核心)

很多人把 @Transactional
当成「一个会魔法的事务开关」,其实它的底层是 AOP
的环绕通知(Around Advice)
,完整链路如下:

调用 orderService.createOrder()  [orderService 是代理对象]
   │
   ▼
代理拦截(TransactionInterceptor,一个 MethodInterceptor = 环绕通知)
   │
   ▼
invokeWithinTransaction()
   ├─ 获取 PlatformTransactionManager(如 DataSourceTransactionManager)
   ├─ 获取连接 Connection,setAutoCommit(false)     ← 开启事务
   ├─ 执行目标方法(反射调用真实对象的方法)
   │     ├─ 正常返回 → commit()                     ← 提交
   │     └─ 抛 RuntimeException → rollback()         ← 回滚(注意:默认只回滚 RuntimeException 和 Error)
   └─ 释放连接,恢复 autoCommit

这些「事务能力」是怎么被装配起来的? 关键在于
@EnableTransactionManagement(Spring Boot 通过
TransactionAutoConfiguration 自动开启)导入的配置类
ProxyTransactionManagementConfiguration

// 精简示意:ProxyTransactionManagementConfiguration 注册了三个关键 Bean
@Configuration(proxyBeanMethods = false)
public class ProxyTransactionManagementConfiguration {

    // ① 事务拦截器:真正的「环绕通知」实现,负责开启/提交/回滚
    @Bean
    public TransactionInterceptor transactionInterceptor(TransactionAttributeSource txAttrSource, ...) {
        TransactionInterceptor interceptor = new TransactionInterceptor();
        interceptor.setTransactionAttributeSource(txAttrSource);
        interceptor.setTransactionManager(txManager);
        return interceptor;
    }

    // ② Advisor:把「事务拦截器」和「切点」绑定在一起
    @Bean
    public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(...) {
        BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
        advisor.setTransactionAttributeSource(txAttrSource);
        advisor.setAdvice(transactionInterceptor(...));
        return advisor;
    }
}

TransactionInterceptor 实现了
MethodInterceptor 接口,而 MethodInterceptor
正是环绕通知的接口形态(@Around
切面的底层也是它)。所以:

@Transactional =
一个「切点」(@Transactional 注解 + 事务属性) +
一个「环绕通知」(TransactionInterceptor) +
一个「Advisor」把它们绑在一起,最终由
AbstractAutoProxyCreator 织入成代理对象。

TransactionInterceptor
的核心走读
(源码位置:TransactionInterceptor#invoke
TransactionAspectSupport#invokeWithinTransaction):

// TransactionInterceptor 是环绕通知,核心就是 invoke 方法
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
    Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
    // 委托给父类的 invokeWithinTransaction,完成「开事务 → 执行 → 提交/回滚」的环绕逻辑
    return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
        @Override
        public Object proceedWithInvocation() throws Throwable {
            return invocation.proceed();   // 这里才真正执行目标方法
        }
        ...
    });
}

invokeWithinTransaction
里的伪代码逻辑(这是理解「事务环绕」最直观的方式):

protected Object invokeWithinTransaction(Method method, Class<?> targetClass, InvocationCallback invocation) {
    // 1. 解析事务属性(传播行为、隔离级别、回滚规则)
    TransactionAttributeSource tas = getTransactionAttributeSource();
    TransactionAttribute txAttr = tas.getTransactionAttribute(method, targetClass);

    // 2. 获取事务管理器(根据数据源/事务类型选择)
    TransactionManager tm = determineTransactionManager(txAttr);

    if (txAttr == null || !(tm instanceof PlatformTransactionManager)) {
        // 没有事务属性 → 直接执行目标方法
        return invocation.proceedWithInvocation();
    }

    // 3. 开启事务(或加入已有事务,取决于传播行为)
    TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

    Object retVal;
    try {
        // 4. 执行目标方法(环绕通知的「中间」)
        retVal = invocation.proceedWithInvocation();
    }
    catch (Throwable ex) {
        // 5. 异常 → 判断是否回滚(默认 RuntimeException/Error 回滚)
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
    }
    finally {
        // 6. 清理线程绑定的连接等资源
        cleanupTransactionInfo(txInfo);
    }
    // 7. 正常返回 → 提交
    commitTransactionAfterReturning(txInfo);
    return retVal;
}

这就是「环绕通知」的完整形态:前(开事务)→ 中(执行目标)→
后(提交)→ 异常(回滚)

几个必须记住的事务细节

  1. 默认只回滚 RuntimeException 和
    Error
    ,受检异常(Checked
    Exception)不回滚。若要回滚受检异常,需显式
    @Transactional(rollbackFor = Exception.class)
  2. 传播行为默认
    REQUIRED
    :有事务则加入,没有则新建。
  3. 事务的「开/关」依赖线程绑定:事务管理器用
    ThreadLocalTransactionSynchronizationManager)把连接绑到当前线程,所以同一线程内多个
    @Transactional
    方法能共享同一个事务。这也解释了「事务方法里开新线程,新线程里不会自动继承事务」。

本章小结(4.6)@Transactional =
环绕通知(TransactionInterceptor)。它拦截方法调用,执行「开事务
→ 调目标方法 → 提交/回滚」的环绕逻辑,默认只回滚
RuntimeException/Error,且依赖
ThreadLocal 绑定连接。

4.7
自调用失效的源码解释(呼应阶段 1)

阶段 1 里你学过「同类里 A 方法调用带 @Transactional 的 B
方法,事务失效」。现在从源码角度彻底解释清楚。

问题复现

package com.example.advancedprinciples.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentMapper paymentMapper;

    // 没有 @Transactional:从外部调用这个方法
    public void process() {
        // 自调用:this.pay() 直接调用,绕过了代理对象
        this.pay(1001L);
    }

    // 有 @Transactional:期望这里开启事务
    @Transactional
    public void pay(Long paymentId) {
        paymentMapper.updateStatus(paymentId, "PAID");
    }
}

失效原因(一句话):代理是「外层对象」,而
this 永远指向「原始对象」。this.pay()
是在原始对象内部直接调用,根本不经过代理对象,TransactionInterceptor
压根没机会执行。

图解

外部调用:controller.paymentService.process()
   │
   ▼
[代理对象 PaymentService$$CGLIB$$0]
   │ process()  → 内部执行 this.pay()
   │               │
   │               ▼
   │        [原始对象 PaymentService]   ← this 指向这里
   │               │ pay() 直接执行,没有经过代理的 invoke 拦截
   │               ▼
   │         paymentMapper.updateStatus(...)  ← 事务拦截器从未介入,连接没开事务
   ▼
process() 返回(整个过程没有事务)

对照正常链路:controller 直接调用
paymentService.pay() 时,paymentService
是注入的代理对象pay() 调用会先进入
TransactionInterceptor.invoke() 开启事务。而
this.pay() 绕过了这一步。

三种解法

解法一:把方法拆到另一个 Bean(推荐):

@Service
@RequiredArgsConstructor
public class PaymentProcessService {

    private final PaymentPayService paymentPayService;

    public void process() {
        // 通过注入的代理对象调用,事务生效
        paymentPayService.pay(1001L);
    }
}

@Service
@RequiredArgsConstructor
public class PaymentPayService {

    private final PaymentMapper paymentMapper;

    @Transactional
    public void pay(Long paymentId) {
        paymentMapper.updateStatus(paymentId, "PAID");
    }
}

解法二:通过 ApplicationContext
获取代理再调用

@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentMapper paymentMapper;
    private final ApplicationContext applicationContext;

    public void process() {
        // 从容器里拿到的是「代理对象」,用它调用即可触发拦截
        PaymentService proxy = applicationContext.getBean(PaymentService.class);
        proxy.pay(1001L);
    }

    @Transactional
    public void pay(Long paymentId) {
        paymentMapper.updateStatus(paymentId, "PAID");
    }
}

解法三:使用 AopContext 暴露当前代理(需开启
exposeProxy,较少用):

// 需在配置里开启:@EnableAspectJAutoProxy(exposeProxy = true)
@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentMapper paymentMapper;

    public void process() {
        // AopContext.currentProxy() 拿到当前线程的代理对象
        ((PaymentService) AopContext.currentProxy()).pay(1001L);
    }

    @Transactional
    public void pay(Long paymentId) {
        paymentMapper.updateStatus(paymentId, "PAID");
    }
}

本章小结(4.7):自调用失效的根因是「this
指向原始对象、不指向代理」,代理拦截器(事务/切面)在自调用路径上不生效。解法是「让调用经过代理」——拆
Bean(推荐)、容器取代理、或 AopContext


4.5 自定义 Starter

本章目标:开发一个完整的、可被其他项目直接引用的「分布式 ID 生成器
Starter」,把第 2 章的自动配置机制落地成真实组件,并掌握命名规范与
@Conditional 组合。

5.1 Starter 是什么与命名规范

Starter 的本质:一个「普通的 Maven
依赖」+「自动配置能力」。它把「某个功能所需的依赖、配置属性、自动装配逻辑」打包成一个
jar,别的项目只要引入这个 jar,功能就自动生效,无需手写任何配置。

命名规范(硬性)

类型 命名规则 示例
官方 Starter spring-boot-starter-* spring-boot-starter-webspring-boot-starter-data-redis
第三方/自定义 Starter *-spring-boot-starter mybatis-spring-boot-startersnowflake-spring-boot-starter

命名不能搞反:官方用「前缀
spring-boot-starter」,第三方用「后缀
-spring-boot-starter」。这是社区约定,也是避免和官方命名冲突的重要手段。

一个规范 Starter 的标准结构

snowflake-spring-boot-starter/
├── pom.xml
└── src/main/
    ├── java/com/example/snowflake/
    │   ├── SnowflakeIdGenerator.java          # 核心功能类
    │   ├── IdProperties.java                   # 配置属性类(@ConfigurationProperties)
    │   ├── IdAutoConfiguration.java            # 自动配置类(@AutoConfiguration)
    │   └── IdService.java                      # 对外提供的门面服务(可选)
    └── resources/META-INF/spring/
        └── org.springframework.boot.autoconfigure.AutoConfiguration.imports   # 注册自动配置类

5.2 分布式 ID
生成器:雪花算法(Snowflake)

雪花算法是分布式 ID 的经典方案,生成的 ID 是一个 64 位
long,结构如下:

0 | 0000000000 0000000000 0000000000 0000000000 0 | 00000 | 00000 | 000000000000
└─ 1 位符号位(固定 0)
     └─ 41 位时间戳(毫秒,可用约 69 年)
                          └─ 5 位数据中心 ID(0~31)
                                 └─ 5 位机器 ID(0~31)
                                              └─ 12 位序列号(同毫秒内 0~4095)

优点:趋势递增(利于索引)、高性能(纯内存位运算,无网络开销)、分布式唯一(靠「数据中心
ID + 机器 ID」区分节点)。

核心实现(生产级,含时钟回拨处理)

package com.example.snowflake;

import lombok.extern.slf4j.Slf4j;

/**
 * 雪花算法 ID 生成器(生产级实现)。
 *
 * 64 位结构:1 位符号位 + 41 位时间戳 + 5 位数据中心 + 5 位机器 + 12 位序列号。
 * 关键点:处理了「时钟回拨」问题——服务器时钟被回拨会导致生成重复 ID,必须防御。
 */
@Slf4j
public class SnowflakeIdGenerator {

    // 各部分的位数
    private static final long SEQUENCE_BITS = 12L;          // 序列号 12 位
    private static final long WORKER_ID_BITS = 5L;          // 机器 ID 5 位
    private static final long DATACENTER_ID_BITS = 5L;      // 数据中心 ID 5 位

    // 最大值(用于做位运算掩码)
    private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);              // 4095
    private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);            // 31
    private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);    // 31

    // 各部分的左移位数(拼接 64 位时用)
    private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;                              // 12
    private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;         // 17
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS; // 22

    // 自定义纪元(2024-01-01 00:00:00 的毫秒时间戳),让时间戳部分能用更久
    private static final long EPOCH = 1704067200000L;

    private final long workerId;
    private final long datacenterId;

    private long sequence = 0L;          // 当前序列号
    private long lastTimestamp = -1L;    // 上一次生成 ID 的时间戳

    /**
     * 构造器:校验 workerId  datacenterId 的合法性(必须在校验范围 0~31)。
     * 这里做参数校验,保证「配置错了立刻失败」,而不是运行时产生重复 ID
     */
    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId < 0 || workerId > MAX_WORKER_ID) {
            throw new IllegalArgumentException(
                    String.format("workerId 必须在 [0, %d] 之间,当前值:%d", MAX_WORKER_ID, workerId));
        }
        if (datacenterId < 0 || datacenterId > MAX_DATACENTER_ID) {
            throw new IllegalArgumentException(
                    String.format("datacenterId 必须在 [0, %d] 之间,当前值:%d", MAX_DATACENTER_ID, datacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
        log.info("SnowflakeIdGenerator 初始化完成,workerId={}, datacenterId={}", workerId, datacenterId);
    }

    /**
     * 生成下一个 ID。用 synchronized 保证线程安全(序列号在同一毫秒内是共享状态)。
     * 也可以改用无锁的 AtomicLong + CAS,但雪花算法里锁的临界区极短,synchronized 足够且更清晰。
     */
    public synchronized long nextId() {
        long timestamp = currentTimeMillis();

        // 时钟回拨检测:如果当前时间小于上次时间,说明服务器时钟被回拨了
        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            log.error("检测到时钟回拨,回拨了 {} 毫秒", offset);
            throw new IllegalStateException(
                    String.format("时钟回拨 %d 毫秒,拒绝生成 ID 以避免重复", offset));
        }

        if (timestamp == lastTimestamp) {
            // 同一毫秒内:序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            if (sequence == 0) {
                // 序列号用完了(同一毫秒生成了 4096 个),自旋等待下一毫秒
                timestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            // 新的毫秒:序列号归零
            sequence = 0L;
        }

        lastTimestamp = timestamp;

        // 拼接 64 位 ID:时间戳 + 数据中心 + 机器 + 序列号
        return ((timestamp - EPOCH) << TIMESTAMP_SHIFT)
                | (datacenterId << DATACENTER_ID_SHIFT)
                | (workerId << WORKER_ID_SHIFT)
                | sequence;
    }

    /** 自旋等待到下一毫秒 */
    private long waitNextMillis(long lastTimestamp) {
        long timestamp = currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = currentTimeMillis();
        }
        return timestamp;
    }

    private long currentTimeMillis() {
        return System.currentTimeMillis();
    }
}

关键点提示

  • 时钟回拨是雪花算法最大的坑:NTP
    校时、虚拟机迁移都可能导致系统时间倒退,若不做防御就会生成重复
    ID。上面的实现检测到回拨直接抛异常(宁可失败,不可重复),这是生产级必须有的保护。
  • 序列号溢出自旋:同一毫秒内超过 4096 个 ID
    时,sequence 归零后必须等到下一毫秒,否则同一毫秒会生成重复
    ID。
  • synchronized
    足够
    :临界区只有几行位运算,锁开销极小,无需过度设计。

5.3 配置属性类 IdProperties

package com.example.snowflake;

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

/**
 * 绑定 application.yml  snowflake.* 的配置项。
 * 前缀统一用 snowflake,避免与其他 starter 冲突。
 */
@Data
@ConfigurationProperties(prefix = "snowflake")
public class IdProperties {

    /** 是否启用雪花 ID 生成器,默认 true(配合 @ConditionalOnProperty 使用) */
    private boolean enabled = true;

    /** 机器 ID(0~31),不同机器/实例必须不同 */
    private long workerId = 1L;

    /** 数据中心 ID(0~31),不同机房必须不同 */
    private long datacenterId = 1L;
}

5.4 自动配置类
IdAutoConfiguration

package com.example.snowflake;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfiguration;
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;

/**
 * 雪花 ID 生成器的自动配置类。
 *
 * @AutoConfiguration         标记为自动配置类(proxyBeanMethods=false,性能更好)
 * @ConditionalOnClass        只有 classpath 存在 SnowflakeIdGenerator 才生效
 * @ConditionalOnProperty     只有 snowflake.enabled=true(默认)才生效,允许用户一键关闭
 * @EnableConfigurationProperties  IdProperties 注册进容器并绑定配置
 */
@Slf4j
@AutoConfiguration
@ConditionalOnClass(SnowflakeIdGenerator.class)
@ConditionalOnProperty(prefix = "snowflake", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(IdProperties.class)
public class IdAutoConfiguration {

    /**
     * 注册 ID 生成器。
     * @ConditionalOnMissingBean 用户自己定义了 SnowflakeIdGenerator 时,不覆盖用户的。
     * 这是「默认配置可被用户覆盖」的关键。
     */
    @Bean
    @ConditionalOnMissingBean
    public SnowflakeIdGenerator snowflakeIdGenerator(IdProperties properties) {
        log.info("自动配置 SnowflakeIdGenerator,workerId={}, datacenterId={}",
                properties.getWorkerId(), properties.getDatacenterId());
        return new SnowflakeIdGenerator(properties.getWorkerId(), properties.getDatacenterId());
    }

    /**
     * 对外提供的门面服务:封装 nextId(),返回 String 类型(避免前端 JS  long 精度丢失)。
     */
    @Bean
    @ConditionalOnMissingBean
    public IdService idService(SnowflakeIdGenerator generator) {
        return new IdService(generator);
    }
}

@Conditional 组合的设计思路(这是 Starter
的「开关三件套」):

  1. @ConditionalOnClass —— 保证依赖存在(防止类缺失导致
    ClassNotFoundException)。
  2. @ConditionalOnProperty ——
    给用户一个配置开关(snowflake.enabled=false
    一键关闭)。
  3. @ConditionalOnMissingBean ——
    给用户「自定义覆盖」的口子。

5.5 对外门面服务 IdService

package com.example.snowflake;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/**
 * 对外提供的门面服务:业务方直接注入 IdService 即可。
 * 返回 String 而不是 long:前端 JavaScript  Number 只能精确表示到 2^53,
 * 雪花 ID(64 位)超过后会丢精度,故用 String 传输。
 */
@Slf4j
@RequiredArgsConstructor
public class IdService {

    private final SnowflakeIdGenerator generator;

    /** 生成一个 IDString 形式,避免前端精度丢失) */
    public String nextId() {
        return String.valueOf(generator.nextId());
    }

    /** 批量生成 ID,返回 long 数组(内部使用时用 long 更高效) */
    public long[] nextIds(int count) {
        if (count <= 0) {
            throw new IllegalArgumentException("批量生成数量必须大于 0,当前值:" + count);
        }
        long[] ids = new long[count];
        for (int i = 0; i < count; i++) {
            ids[i] = generator.nextId();
        }
        return ids;
    }
}

5.6 注册自动配置类(imports
文件)

# src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
# 每行一个自动配置类全限定名(Spring Boot 3.x 的注册方式)
com.example.snowflake.IdAutoConfiguration

5.7 Starter 的 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>snowflake-spring-boot-starter</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <name>snowflake-spring-boot-starter</name>

    <properties>
        <java.version>17</java.version>
        <spring-boot.version>3.2.5</spring-boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- 用 BOM 统一管理 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>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 只依赖「自动配置」所需的最小集合,不引入 spring-boot-starter-web 等重依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <!-- @ConfigurationProperties 的注解处理器:生成配置元数据,让 IDE 有提示 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

关键:Starter 的 pom 里只依赖
spring-boot-autoconfigure
,绝不引入
spring-boot-starter-web 这种重依赖。Starter
要轻量、不替用户做技术选型,否则用户引入你的 ID
生成器,连带引入一堆不想要的依赖。

5.8 在业务项目中使用 Starter

第 1 步:本地安装 Starter(在 Starter
项目根目录执行):

mvn clean install

第 2 步:业务项目引入依赖

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

第 3 步:配置(可选)

# application.yml
snowflake:
  enabled: true          # 是否启用(默认 true)
  worker-id: 1           # 机器 ID,多实例部署时每个实例必须不同
  datacenter-id: 2       # 数据中心 ID

第 4 步:业务代码直接注入使用

package com.example.advancedprinciples.controller;

import com.example.snowflake.IdService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

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

@RestController
@RequiredArgsConstructor
public class IdController {

    private final IdService idService;   // 直接注入 Starter 提供的 Bean

    @GetMapping("/id")
    public Map<String, Object> nextId() {
        // 生产级:返回结构统一,包含生成的 ID
        Map<String, Object> result = new HashMap<>();
        result.put("id", idService.nextId());
        result.put("timestamp", System.currentTimeMillis());
        return result;
    }
}

启动日志会打印
自动配置 SnowflakeIdGenerator,workerId=1, datacenterId=2,说明
Starter 的自动配置已生效。访问 http://localhost:8080/id
即可拿到雪花 ID。

5.9 Starter 最佳实践清单

实践 说明
命名用 *-spring-boot-starter 第三方约定,别和官方 spring-boot-starter-* 冲突
轻依赖 只依赖 spring-boot-autoconfigure,不替用户做选型
配置属性有默认值 用户零配置也能用,需要时再覆盖
@ConditionalOnMissingBean 留口 允许用户用自己的 Bean 覆盖默认实现
@ConditionalOnProperty 提供开关 xxx.enabled 一键关闭,方便排障和降级
提供 spring-boot-configuration-processor 生成配置元数据,IDE 输入 snowflake. 时有自动提示
构造器参数校验 非法配置「启动即失败」,而不是运行时才爆

本章小结(5.9):自定义 Starter = 功能类 +
@ConfigurationProperties 属性类 +
@AutoConfiguration 配置类 + imports
文件注册,配合「开关三件套」@ConditionalOnClass/@ConditionalOnProperty/@ConditionalOnMissingBean,做到开箱即用、可配置、可覆盖。


4.6 WebFlux 响应式编程

本章目标:理解响应式编程与 Reactor 的
Mono/Flux、背压机制,会用 WebFlux
写响应式接口,并建立「WebFlux vs MVC
选型」的正确判断标准,尤其警惕「WebFlux + JDBC」的阻塞陷阱。

6.1 响应式编程与 Reactor

传统 Servlet
模型(阻塞)
:每个请求占用一个线程,线程从头到尾「同步等待」——等数据库、等下游接口。线程数有限(默认
Tomcat 约 200),当并发连接数超过线程数,请求只能排队。IO
密集场景下,大量线程的时间都耗在「等」上,CPU
利用率很低,吞吐上不去。

响应式模型(非阻塞):基于事件驱动 + 非阻塞
IO,用「少量线程」处理「大量连接」。一个请求发起 IO
后不阻塞等待,而是注册回调,IO 完成时再通知处理。核心是
Reactor 库(Spring 官方,基于 Reactive Streams
规范),它的两大核心类型:

类型 含义 类比
Mono<T> 0 或 1 个元素的异步序列 异步的「单个返回值」
Flux<T> 0 到 N 个元素的异步序列 异步的「列表/流」

为什么是 Reactive Streams 规范?
它定义了四个接口:Publisher(发布者)、Subscriber(订阅者)、Subscription(订阅关系)、Processor(处理器)。核心价值是定义了**背压(Backpressure)**机制——让下游能「告诉上游发慢点」,防止下游被数据淹没。

6.2 Mono 与 Flux
的最小可用示例

先感受一下响应式 API 的写法:

package com.example.advancedprinciples.reactive;

import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.Arrays;
import java.util.List;

@Slf4j
public class ReactorDemo {

    public static void main(String[] args) {
        // Mono:0 或 1 个元素
        Mono<String> mono = Mono.just("Hello Reactive");
        // 订阅才会真正触发执行(惰性),onNext 拿到数据,onError 处理异常,onComplete 表示结束
        mono.subscribe(
                data -> log.info("Mono 收到:{}", data),
                err -> log.error("Mono 出错", err),
                () -> log.info("Mono 完成")
        );

        // Flux:0 到 N 个元素
        List<String> names = Arrays.asList("alice", "bob", "cindy");
        Flux<String> flux = Flux.fromIterable(names)
                // 操作符:过滤
                .filter(name -> name.length() > 3)
                // 操作符:转换
                .map(String::toUpperCase);
        flux.subscribe(name -> log.info("Flux 收到:{}", name));

        // 响应式是「流式编程」:像 Java Stream 一样链式操作,但底层是异步非阻塞的
        Flux.interval(Duration.ofMillis(100))   // 每 100ms 发一个数字
                .take(5)                         // 只取前 5 个
                .map(i -> "第 " + i + " 个")
                .subscribe(log::info);
    }
}

关键认知:惰性 +
订阅驱动
Mono.just(...)Flux.fromIterable(...)
都只是「声明了一个数据流」,真正的执行从 .subscribe()
才开始。这就是响应式编程和命令式编程最根本的思维差异——你描述「数据如何流动」,而不是「一步步怎么执行」。

6.3 背压(Backpressure)

问题Flux
可以瞬间发出海量元素,但下游消费者处理慢,数据就会在下游堆积,最终
OOM。背压就是让「下游消费者控制上游生产者」的机制。

package com.example.advancedprinciples.reactive;

import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Subscription;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;

@Slf4j
public class BackpressureDemo {

    public static void main(String[] args) throws InterruptedException {
        // 用 create 手动控制数据流,模拟「上游能发很快」的场景
        Flux<Integer> flux = Flux.create((FluxSink<Integer> sink) -> {
            int i = 0;
            while (i < 1000) {
                log.info("上游尝试发出:{}", i);
                sink.next(i++);   // 发出元素
            }
            sink.complete();
        }, FluxSink.OverflowStrategy.DROP);   // 下游处理不过来时,丢弃新元素(策略之一)

        flux
                // onBackpressureBuffer:下游处理不过来时,先缓冲(默认有界 256)
                // 这里改用 limitRate 演示「下游主动请求」控制上游速率
                .limitRate(10)   // 每次最多请求 10 个元素
                .subscribe(new org.reactivestreams.Subscriber<Integer>() {
                    private Subscription subscription;

                    @Override
                    public void onSubscribe(Subscription s) {
                        this.subscription = s;
                        // 关键:下游只请求 5 个,上游就只发 5 个,这就是背压
                        subscription.request(5);
                    }

                    @Override
                    public void onNext(Integer value) {
                        log.info("下游处理:{}", value);
                        // 处理完再请求 1 个(慢消费者:一个接一个地要)
                        subscription.request(1);
                    }

                    @Override
                    public void onError(Throwable t) {
                        log.error("出错", t);
                    }

                    @Override
                    public void onComplete() {
                        log.info("处理完成");
                    }
                });

        Thread.sleep(3000);
    }
}

背压的本质Subscriber 通过
Subscription.request(n) 告诉上游「我只能再接收 n
个」,上游据此控制发送速率。Reactor 提供了多种策略:

策略 行为 适用场景
request(n) 下游按需请求(默认背压) 通用
onBackpressureBuffer 积压时先缓冲(可配容量) 允许短暂堆积
onBackpressureDrop 积压时丢弃新元素 可丢弃的实时数据
onBackpressureLatest 积压时只保留最新元素 只关心最新值(如行情)
onBackpressureError 积压时直接报错 不允许丢数据

注意:背压只适用于「有界、可回压」的场景。像
Flux.interval
这种「时间驱动」的源,上游根本停不下来,背压策略只能决定「积压了怎么办」,无法真正让时间变慢。WebFlux
里最容易踩的坑,就是把「阻塞
JDBC」塞进响应式流里,因为它破坏了「非阻塞」的假设(见 6.6)。

6.4 WebFlux 响应式接口实战

第 1 步:引入 WebFlux
依赖
(注意:spring-boot-starter-web
spring-boot-starter-webflux 不要同时引入,二选一):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

第 2 步:写响应式 Controller

package com.example.advancedprinciples.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.Arrays;
import java.util.List;

/**
 * 响应式 Controller:返回 Mono/FluxWebFlux 会自动处理异步非阻塞的响应。
 */
@Slf4j
@RestController
@RequiredArgsConstructor
public class ReactiveUserController {

    private static final List<String> USERS = Arrays.asList("alice", "bob", "cindy", "david");

    // 返回 Mono:单个结果
    @GetMapping("/reactive/user/{id}")
    public Mono<String> getUser(@PathVariable("id") String id) {
        return Mono.justOrEmpty(
                        USERS.stream().filter(u -> u.equals(id)).findFirst())
                // 记录日志:响应式里用 doOnNext 做副作用操作
                .doOnNext(u -> log.info("查询到用户:{}", u))
                // 找不到时切换到一个「错误信号」,而不是返回 null
                .switchIfEmpty(Mono.error(new RuntimeException("用户不存在:" + id)));
    }

    // 返回 Flux:多个结果,模拟耗时操作并逐条返回(SSE 场景)
    @GetMapping(value = "/reactive/users", produces = "text/event-stream")
    public Flux<String> listUsers() {
        return Flux.fromIterable(USERS)
                // 每个元素延迟 300ms,模拟「边查边返回」的流式响应
                .delayElements(Duration.ofMillis(300))
                .doOnNext(u -> log.info("流式返回用户:{}", u));
    }
}

第 3 步:用 WebClient 做响应式调用(WebFlux 的响应式
HTTP 客户端):

package com.example.advancedprinciples.client;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@Slf4j
public class WebClientDemo {

    public static void main(String[] args) {
        // WebClient 是 WebFlux 栈里的响应式 HTTP 客户端(替代阻塞的 RestTemplate)
        WebClient client = WebClient.create("http://localhost:8080");

        Mono<String> result = client.get()
                .uri("/reactive/user/alice")
                .retrieve()
                .bodyToMono(String.class);

        // 订阅并处理结果
        result.subscribe(
                data -> log.info("响应结果:{}", data),
                err -> log.error("调用失败", err)
        );
    }
}

关键点提示

  • WebFlux 里不能像 MVC 那样「返回 null」,要用
    Mono.empty()(表示无结果)或
    Mono.error()(表示出错)。
  • 日志、埋点等「副作用」操作用
    doOnNext/doOnError/doOnComplete,不要写在
    map 里(map 应该是纯函数)。
  • produces = "text/event-stream" 配合 Flux
    可实现 SSE(Server-Sent Events)流式推送。

本章小结(6.4):WebFlux 的 Controller 返回
Mono/Flux,用 WebClient
做响应式调用;「无结果」用 Mono.empty(),「出错」用
Mono.error(),副作用用 doOnXxx

6.5 R2DBC:响应式的数据库访问

WebFlux 是非阻塞的,但 JDBC 是阻塞的——JDBC 的
Connection/Statement
都是「同步等待」的。如果在 WebFlux 里直接调 JDBC,会阻塞 Netty
的事件循环线程,把整个非阻塞模型拖垮。响应式数据库访问要用
R2DBC(Reactive Relational Database
Connectivity)。

依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
    <groupId>io.r2dbc</groupId>
    <artifactId>r2dbc-postgresql</artifactId>   <!-- 以 PostgreSQL 为例 -->
    <scope>runtime</scope>
</dependency>

配置

spring:
  r2dbc:
    url: r2dbc:postgresql://localhost:5432/mydb
    username: postgres
    password: postgres

响应式 Repository(Spring Data R2DBC):

package com.example.advancedprinciples.entity;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;

@Data
@Table("t_user")
public class User {
    @Id
    private Long id;
    private String name;
    private String email;
}
package com.example.advancedprinciples.mapper;

import com.example.advancedprinciples.entity.User;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * 响应式 Repository:继承 ReactiveCrudRepository,方法返回 Mono/Flux
 * 注意:接口名是 ReactiveCrudRepository(不是 JpaRepository)。
 */
public interface UserRepository extends ReactiveCrudRepository<User, Long> {

    Mono<User> findByName(String name);

    Flux<User> findByEmailContaining(String emailPart);
}
package com.example.advancedprinciples.controller;

import com.example.advancedprinciples.entity.User;
import com.example.advancedprinciples.mapper.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
@RequiredArgsConstructor
public class ReactiveUserDbController {

    private final UserRepository userRepository;

    @GetMapping("/reactive/db/user/{name}")
    public Mono<User> findByName(@PathVariable("name") String name) {
        // 全链路响应式:Controller → Repository → R2DBC 驱动,无阻塞
        return userRepository.findByName(name);
    }

    @GetMapping("/reactive/db/users")
    public Flux<User> findAll() {
        return userRepository.findAll();
    }
}

R2DBC 的局限(必须知道)

  1. 不支持 JPA/Hibernate(那是阻塞的 ORM),只能用
    Spring Data R2DBC 这类「轻量映射」。
  2. 不支持懒加载、一级缓存、脏检查等 JPA
    特性,实体关系处理能力弱。
  3. 生态不如 JDBC
    成熟
    ,部分数据库驱动、连接池功能还不完善。
  4. 不适合复杂事务编排,跨多个资源的响应式事务支持有限。

本章小结(6.5):WebFlux 配
R2DBC(ReactiveCrudRepository + r2dbc-xxx
驱动)才能做到「全链路非阻塞」;但 R2DBC 生态弱于
JDBC/JPA,需按项目复杂度权衡。

6.6 WebFlux vs MVC
选型(关键决策)

这是面试和架构设计里的高频题。核心结论:WebFlux 不是「更好的
MVC」,而是「不同的技术栈」,选型看场景。

维度 Spring MVC(Servlet) WebFlux(Reactive)
编程模型 命令式、同步 响应式、异步非阻塞
底层服务器 Tomcat / Jetty(Servlet 容器) Netty(默认)/ Tomcat/Undertow
线程模型 一个请求一个线程(线程池) 少量事件循环线程(Event Loop)
吞吐能力 受线程数限制 相同资源下更高(IO 密集)
数据库 JDBC(阻塞,成熟) R2DBC(非阻塞,生态弱)
学习曲线 平缓 陡峭(要理解响应式思维)
调试难度 简单(同步栈) 困难(异步回调栈)

什么时候用 WebFlux?

  1. IO 密集 +
    高并发
    :网关、代理、实时数据推送、大量外部 HTTP
    调用聚合等场景。
  2. 流式数据:SSE、WebSocket、实时行情。
  3. 全链路响应式:从数据库到服务间调用都走非阻塞(R2DBC
    + WebClient + 消息队列)。

什么时候别用 WebFlux?

  1. 依赖阻塞
    JDBC/JPA
    :数据访问层是阻塞的,整个链路非阻塞就白费,还会拖垮
    Netty 线程(这是最大的坑)。
  2. CPU 密集计算:响应式不解决 CPU
    瓶颈,反而增加复杂度。
  3. 团队不熟悉响应式:异步调试成本高,维护性差。
  4. 大量现有同步代码/第三方阻塞库:混用阻塞调用会让「非阻塞」形同虚设。

「WebFlux 配 JDBC
会阻塞」的坑(专属约束重点)
:这是选型里最需要警惕的。Netty
默认的事件循环线程数约等于 CPU 核数(比如 8 个)。如果 WebFlux
接口里调用了阻塞 JDBC,这 8 个线程会被 Statement.execute()
卡住,此时整个服务都无法处理其他请求——比 Servlet
模型还糟(Servlet 至少有 200 个线程可用)。所以:

一旦决定用 WebFlux,数据访问必须用 R2DBC(或把 JDBC
调用丢到专用线程池并做响应式桥接),绝不能直接在响应式线程里调阻塞
JDBC。

Spring Boot 如何识别你用的是哪套栈? 回到第 1.2
节:WebApplicationType.deduceFromClasspath() 会根据
classpath 里有没有 DispatcherServlet(MVC)或
DispatcherHandler(WebFlux)来决定。如果两者都引入,它会退回
SERVLET 类型——所以不要同时引入
spring-boot-starter-web
spring-boot-starter-webflux

本章小结(6.6):WebFlux 适合「IO
密集、高并发、全链路非阻塞」场景,MVC
适合「JDBC/JPA、团队熟悉同步」场景;两者是不同技术栈,切勿混用,尤其不能在响应式线程里调阻塞
JDBC。



5.
生产级实战项目:订单中心(串联本篇 80% 知识点)

本章用一个「订单中心」模块,把启动流程、自动配置、循环依赖、AOP/事务、自定义
Starter、WebFlux 串起来,形成一个能跑、能验证的完整项目。

1. 项目目标

  • 引入第 5 章开发的 snowflake-spring-boot-starter,让订单
    ID 用雪花算法自动生成
  • OrderService@Transactional
    保证下单「扣库存 + 生成订单」的原子性
  • 用 Facade 类拆解 OrderService
    UserService 的循环依赖,避免启动报错
  • --debug 验证自动配置报告,确认 Snowflake Starter
    与事务自动配置已生效
  • 通过 /proxy/type 接口验证 Bean 被 CGLIB 代理

2. 目录结构

advanced-principles/
├── pom.xml                                      # 引入 snowflake starter + web
└── src/main/
    ├── java/com/example/advancedprinciples/
    │   ├── AdvancedPrinciplesApplication.java   # 启动类
    │   ├── entity/
    │   │   └── Order.java                        # 订单实体
    │   ├── mapper/
    │   │   └── OrderMapper.java                  # 订单数据访问(简化,内存存储)
    │   ├── service/
    │   │   ├── OrderService.java                 # 订单服务(@Transactional)
    │   │   ├── UserService.java                  # 用户服务
    │   │   └── OrderUserFacadeService.java       # Facade:拆循环依赖 + 编排下单
    │   ├── controller/
    │   │   ├── OrderController.java              # 下单接口
    │   │   └── ProxyCheckController.java         # 代理类型验证接口
    │   └── common/
    │       └── ApiResult.java                    # 统一响应体(总规范公共类)
    └── resources/
        ├── application.yml
        └── META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports  # (仅自定义自动配置时需要)

3.
统一响应体与错误码(复用总规范公共类)

package com.example.advancedprinciples.common;

/**
 * 统一错误码:贯穿全系列教程的公共枚举,基础码与总规范逐字一致、数值不变。
 * 各阶段可按需扩展,但 SUCCESS/PARAM_ERROR/SYSTEM_ERROR 等基础码必须保留。
 */
public enum ErrorCode {
    SUCCESS(0, "success"),
    PARAM_ERROR(40001, "参数错误"),
    UNAUTHORIZED(40101, "未登录或登录已过期"),
    FORBIDDEN(40301, "无权限访问"),
    USER_NOT_FOUND(40401, "用户不存在"),
    SYSTEM_ERROR(50000, "系统繁忙,请稍后重试");

    private final int code;
    private final String message;

    ErrorCode(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}
import lombok.Data;
import org.slf4j.MDC;

@Data
public class ApiResult<T> {
    private int code;        // 0=成功,非 0=错误码
    private String message;  // 提示信息
    private T data;          // 业务数据
    private String traceId;  // 链路追踪 id

    public static <T> ApiResult<T> ok(T data) {
        ApiResult<T> r = new ApiResult<>();
        r.setCode(ErrorCode.SUCCESS.getCode());
        r.setMessage(ErrorCode.SUCCESS.getMessage());
        r.setData(data);
        r.setTraceId(MDC.get("traceId"));
        return r;
    }
    public static <T> ApiResult<T> ok() {
        return ok(null);
    }
    public static <T> ApiResult<T> fail(int code, String message) {
        ApiResult<T> r = new ApiResult<>();
        r.setCode(code);
        r.setMessage(message);
        r.setTraceId(MDC.get("traceId"));
        return r;
    }
    public static <T> ApiResult<T> fail(ErrorCode ec) {
        return fail(ec.getCode(), ec.getMessage());
    }
}

4. 订单实体与 Mapper

package com.example.advancedprinciples.entity;

import lombok.Data;

import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 订单实体(简化,仅演示)。
 * 生产环境中此实体应带 JPA/MyBatis-Plus 注解并落库,这里用内存 Map 模拟。
 */
@Data
public class Order {
    private String orderId;          // 雪花算法生成的订单号
    private Long userId;             // 下单用户
    private BigDecimal amount;       // 订单金额
    private String status;           // 订单状态:PENDING / PAID
    private LocalDateTime createdAt; // 创建时间
}
package com.example.advancedprinciples.mapper;

import com.example.advancedprinciples.entity.Order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 订单数据访问(内存版)。
 * 生产环境应替换为真实数据库 + MyBatis-Plus/JPA;此处用 ConcurrentHashMap 模拟,
 * 让本篇聚焦于「原理」而非数据持久化细节。
 */
@Slf4j
@Repository
public class OrderMapper {

    // 线程安全的内存存储
    private final Map<String, Order> store = new ConcurrentHashMap<>();

    public void insert(Order order) {
        store.put(order.getOrderId(), order);
        log.info("订单已入库:{}", order.getOrderId());
    }
}

5. 业务服务(含事务 +
Facade 拆循环依赖)

package com.example.advancedprinciples.service;

import com.example.advancedprinciples.entity.Order;
import com.example.advancedprinciples.mapper.OrderMapper;
import com.example.snowflake.IdService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderMapper orderMapper;
    private final IdService idService;   // 注入 Starter 提供的雪花 ID 服务

    /**
     * 下单(带事务):雪花 ID + 落库。
     * @Transactional 是环绕通知,异常时自动回滚(见第 4 章)。
     */
    @Transactional
    public Order createOrder(Long userId, BigDecimal amount) {
        // 参数校验:生产级必须有入参校验
        if (userId == null || userId <= 0) {
            throw new IllegalArgumentException("userId 非法:" + userId);
        }
        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("金额必须大于 0:" + amount);
        }

        Order order = new Order();
        // 用雪花算法生成全局唯一、趋势递增的订单号
        order.setOrderId(idService.nextId());
        order.setUserId(userId);
        order.setAmount(amount);
        order.setStatus("PENDING");
        order.setCreatedAt(LocalDateTime.now());

        orderMapper.insert(order);
        log.info("下单成功,订单号:{},金额:{}", order.getOrderId(), amount);
        return order;
    }
}
package com.example.advancedprinciples.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {

    /**
     * 校验用户是否存在(简化:直接返回 true)。
     * 生产环境应查询用户表,这里为聚焦原理从简。
     */
    public boolean exists(Long userId) {
        // 假设 userId > 0 即视为存在
        return userId != null && userId > 0;
    }
}
package com.example.advancedprinciples.service;

import com.example.advancedprinciples.entity.Order;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

/**
 * Facade:编排「用户校验 + 下单」。
 *
 * 设计意图:如果 OrderService 依赖 UserServiceUserService 又依赖 OrderService
 * 就会形成循环依赖(第 3 章)。把「编排逻辑」下沉到 Facade
 *  OrderService  UserService 各自独立,彻底拆掉循环依赖。
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderUserFacadeService {

    private final UserService userService;
    private final OrderService orderService;

    /**
     * 下单门面:先校验用户,再创建订单。
     */
    public Order placeOrder(Long userId, BigDecimal amount) {
        if (!userService.exists(userId)) {
            throw new IllegalArgumentException("用户不存在:" + userId);
        }
        // 注意:这里通过注入的「代理对象」orderService 调用,事务正常生效
        // 若改成 this.xxx() 自调用,就会触发第 4.7 节的「自调用失效」
        return orderService.createOrder(userId, amount);
    }
}

6. Controller 层

package com.example.advancedprinciples.controller;

import com.example.advancedprinciples.common.ApiResult;
import com.example.advancedprinciples.entity.Order;
import com.example.advancedprinciples.service.OrderUserFacadeService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@Slf4j
@RestController
@RequiredArgsConstructor
public class OrderController {

    private final OrderUserFacadeService facadeService;

    @PostMapping("/order")
    public ApiResult<Order> placeOrder(@RequestParam("userId") Long userId,
                                       @RequestParam("amount") BigDecimal amount) {
        try {
            Order order = facadeService.placeOrder(userId, amount);
            return ApiResult.ok(order);
        } catch (IllegalArgumentException e) {
            // 业务参数异常:返回参数错误码,详细堆栈进日志
            log.warn("下单参数异常:{}", e.getMessage());
            return ApiResult.fail(40001, e.getMessage());
        } catch (Exception e) {
            // 兜底:对外模糊提示,详细堆栈进日志
            log.error("下单失败", e);
            return ApiResult.fail(50000, "系统繁忙,请稍后重试");
        }
    }
}
package com.example.advancedprinciples.controller;

import com.example.advancedprinciples.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 验证 AOP 代理类型(第 4 章):确认被 @Transactional  Bean  CGLIB 代理。
 */
@RestController
@RequiredArgsConstructor
public class ProxyCheckController {

    private final OrderService orderService;

    @GetMapping("/proxy/type")
    public String proxyType() {
        // 输出应含 "$$SpringCGLIB$$",证明是 CGLIB 代理
        return orderService.getClass().getName();
    }
}

7. 配置文件

# application.yml
spring:
  application:
    name: advanced-principles
  # 雪花 Starter 的配置(来自第 5 章自定义 Starter)
  # snowflake 前缀由 IdProperties 的 @ConfigurationProperties(prefix = "snowflake") 决定

snowflake:
  enabled: true        # 开启雪花 ID 生成器
  worker-id: 1         # 机器 ID,多实例部署时每个实例不同
  datacenter-id: 2     # 数据中心 ID

8. 运行与验证步骤

第 1 步:安装 Starter(在
snowflake-spring-boot-starter 项目根目录):

cd /path/to/snowflake-spring-boot-starter
mvn clean install

第 2 步:启动订单中心(在 advanced-principles
项目根目录):

mvn spring-boot:run --debug
# --debug 用于查看自动配置报告,验证 Snowflake 与事务自动配置已生效

启动日志里应能看到:

自动配置 SnowflakeIdGenerator,workerId=1, datacenterId=2
Tomcat started on port(s): 8080 (http) with context path ''
Started AdvancedPrinciplesApplication in X.XXX seconds

第 3 步:验证自动配置报告(在启动日志中搜索):

Positive matches:
   IdAutoConfiguration matched:
      - @ConditionalOnClass found required class 'com.example.snowflake.SnowflakeIdGenerator'
      - @ConditionalOnProperty (snowflake.enabled=true) matched
   TransactionAutoConfiguration matched:
      - ...

第 4 步:调用接口

# 下单(雪花 ID 生成订单号)
curl -X POST "http://localhost:8080/order?userId=1&amount=99.90"
# 预期返回:{"code":0,"message":"success","data":{"orderId":"1789...","userId":1,...}}

# 验证代理类型(应为 CGLIB)
curl "http://localhost:8080/proxy/type"
# 预期返回:com.example.advancedprinciples.service.OrderService$$SpringCGLIB$$0

9. 本实战项目覆盖的知识点对照

知识点 落在项目哪里
启动流程 / refresh / Tomcat 第 8 步 --debug 启动观察,第 1 章
自动配置 + @Conditional Snowflake Starter 的 IdAutoConfiguration,第 2、5
三级缓存 / 循环依赖 OrderUserFacadeService 拆循环依赖,第 3 章
AOP / JDK vs CGLIB /proxy/type 验证 CGLIB 代理,第 4 章
@Transactional 环绕通知 OrderService.createOrder,第 4 章
自定义 Starter snowflake-spring-boot-starter,第 5 章
统一响应体 ApiResult,贯穿总规范

6. 常见坑与排错指南

坑 / 现象 原因 解决方案
自动配置「应该生效」却没生效 @ConditionalOnClass 不满足(缺依赖)或
@ConditionalOnMissingBean 不满足(容器已有同类型
Bean)
--debug 看自动配置报告,定位是 Positive 还是 Negative
matches 及原因
循环依赖报 BeanCurrentlyInCreationException 构造器注入 + 互相引用,实例化阶段无法提前暴露 提取 Facade 类拆依赖;或临时用
@Lazy;字段注入能隐藏但属掩盖设计问题
循环依赖场景下事务失效 用两级缓存时提前暴露的是原始对象而非代理对象 理解三级缓存「工厂延迟决定代理」的机制(第 3.4
节),避免依赖半成品状态
同类方法自调用,@Transactional 不生效 this.method() 绕过代理对象,环绕通知没执行 拆到另一个 Bean,或容器取代理,或
AopContext.currentProxy()
@Transactional 遇受检异常不回滚 默认只回滚 RuntimeException/Error @Transactional(rollbackFor = Exception.class)
WebFlux 接口里调 JDBC,整个服务卡死 JDBC 阻塞 Netty 事件循环线程(线程数≈CPU 核数) 改用 R2DBC;或把 JDBC 调用丢到专用线程池并做响应式桥接
同时引入 spring-boot-starter-web
-webflux,响应式不生效
deduceFromClasspath() 检测到 Servlet 类,退回 SERVLET
类型
二选一,不混用两套栈
自定义 Starter 不生效 忘记写 AutoConfiguration.imports
文件,或类名/路径写错
确认
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
内容为配置类全限定名
雪花 ID 出现重复 时钟回拨(NTP 校时/虚拟机迁移)或 workerId 重复 实现时钟回拨检测;多实例保证 workerId 唯一
Starter 命名不合规导致依赖冲突 官方 spring-boot-starter-* 与第三方
*-spring-boot-starter 混淆
第三方统一用 *-spring-boot-starter 后缀

8. 总结与延伸阅读

本章总结

这一篇把 Spring Boot
从「黑盒」拆成了「白盒」:启动流程是一条七段流水线,核心在
refresh() 的 12 步;自动配置靠
@Import(AutoConfigurationImportSelector) +
AutoConfiguration.imports 文件 + @Conditional
家族三件套;循环依赖靠三级缓存解决,而「三级」的精髓在于用工厂延迟决定是否返回
AOP 代理,从而保证对象一致性;AOP 与事务本质是同一件事——动态代理 +
环绕通知,@Transactional 就是
TransactionInterceptor 这个环绕通知;自定义 Starter
把「功能 + 配置属性 + 自动配置类 + imports
注册」打包成开箱即用的依赖;WebFlux
则是另一套「非阻塞」技术栈,选型要看清「IO 密集还是 CPU
密集、是否全链路响应式」。掌握了这些原理,你就不再是「会用框架」,而是「能解释框架、能排疑难杂症」的进阶工程师。

延伸阅读

  1. Spring Boot 官方文档https://docs.spring.io/spring-boot/docs/3.2.x/reference/html/
    —— 重点看 Creating Your Own Auto-configuration
    Developing Your First Spring Boot Application 两节
  2. Spring Framework 官方文档https://docs.spring.io/spring-framework/reference/
    —— IoC Container 的 Bean 生命周期、Data Access
    的事务管理章节
  3. 《Spring 揭秘》(王福强 著)—— 深入浅出讲透 IoC 与
    AOP 的经典书籍
  4. 《Spring Boot 编程思想(核心篇)》(小马哥 著)——
    源码级剖析自动装配与启动流程
  5. Reactor 官方参考文档https://projectreactor.io/docs/core/release/reference/
    —— 系统学习 Mono/Flux、背压与调度器
  6. Spring Boot
    源码
    (建议直接读):AutoConfigurationImportSelectorAbstractApplicationContext#refreshDefaultSingletonBeanRegistryAbstractAutoProxyCreatorTransactionAspectSupport

本教程为阶段 10 篇,版本约定 Spring Boot 3.2.x / JDK
17。下一篇(阶段 11)将进入架构设计与 DDD 主题。

正文完
 0
评论(没有评论)