版本约定:Spring Boot 3.2.x / JDK 17 / Maven
3.9.x。本篇默认你已经完成阶段 1(Spring Core)与阶段 2(Spring Boot
基础),能熟练使用
@Component、@Autowired、@Configuration、application.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 | @SpringBootApplication、application.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 能跳进源码(关键)
断点跟踪源码前,先确认框架源码可读:
- 在
pom.xml里Ctrl+Click任意
SpringApplication类名,IDEA 会自动下载
spring-boot-3.2.5-sources.jar。 - 若跳进的是
.class反编译代码(泛型丢失、注释缺失),在
Maven 面板勾选 Download Sources and
Documentation。 - 确认
Preferences → Build → Build Tools → Maven → Importing
里勾选了Sources。
提示:本篇会标注「源码位置」,如
SpringApplication#run(String...),你可以在 IDEA 里用
Navigate → Class(Ctrl+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 断点跟踪实战
光读代码不够,按下述步骤亲手断点走一遍:
- 在
SpringApplication#run(String...)
的第一行打断点,Debug启动。 Step Over到prepareEnvironment(...),观察
environment里已经读到了application.yml
的配置。- 进入
refreshContext(...)再进入
AbstractApplicationContext#refresh(),逐行
Step Over。 - 在
onRefresh()处Step Into,一路跟到
TomcatWebServer#initialize(),看
tomcat.start()。 - 回到
finishRefresh(),Step Into到
TomcatWebServer#start(),此时控制台会打印
Tomcat started on port(s): 8080。 - 最后看
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
的通用机制:可以导入普通配置类、ImportSelector、ImportBeanDefinitionRegistrar
三种。这里导入的是 AutoConfigurationImportSelector——它会在
refresh() 的第 ⑤ 步
invokeBeanFactoryPostProcessors() 中被处理。
本章小结(2.1):自动配置的开关是
@EnableAutoConfiguration,它通过
@Import(AutoConfigurationImportSelector.class)
引入一个「选择器」,由选择器决定导入哪些自动配置类。
2.2
AutoConfigurationImportSelector 源码走读
源码位置:AutoConfigurationImportSelector。它实现了
DeferredImportSelector(延迟导入选择器)、BeanClassLoaderAware、EnvironmentAware
等接口。
为什么是 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
里约有两百多个类(DataSourceAutoConfiguration、RedisAutoConfiguration、SecurityAutoConfiguration
等)。
读到候选类之后呢?
这些候选类会作为「待导入的配置类」进入容器,但并不是全部生效——每个自动配置类上都挂着一堆
@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,并用
@AutoConfiguration(proxyBeanMethods = 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'
怎么用:
- 你要的自动配置没有出现在 Positive matches →
大概率是@ConditionalOnClass不满足(缺依赖)。 - 它出现在 Negative matches → 看
Did not match:
下面列出的具体原因,通常是「缺了某个类」或「某个属性为 false」。 - 你自定义的配置被覆盖 → 看它是否因为
@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 循环依赖如何被解决
假设 UserService 和 OrderService
互相注入(这是一个真实业务里常见的反模式,本章用它来演示机制):
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 或自定义切面):
UserService
实例化后,如果两级缓存直接存原始对象,那么
OrderService注入的userService
就是原始对象,没经过 AOP 代理。- 等
UserService初始化完成、创建 AOP
代理后,一级缓存里放的是代理对象。 - 结果:
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()
会做「提前创建代理」的判断。所以流程变成:
UserService实例化后,放进三级缓存的工厂「有 AOP
能力」。OrderService需要userService
时,调用工厂,工厂发现UserService
需要代理,当场创建一个代理对象返回,并放进二级缓存。- 后面
UserService初始化完成,真正做 AOP
时,AbstractAutoProxyCreator#postProcessAfterInitialization
会发现「早先已经为它创建过代理了」,复用二级缓存里那个代理对象(通过
earlyProxyReferences标记),而不是再创建一个新的。 - 最终一级缓存里的
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);
}
}
这样 UserService 和 OrderService
之间不再互相依赖,各自只依赖自己需要的,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? 三个原因:
- 统一行为:不再因为「有没有接口」而导致代理策略不同,行为更可预期。
- 性能:CGLIB 运行期是直接子类调用,比 JDK
反射调用更快。 - 功能: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 代理由
BeanPostProcessor(AbstractAutoProxyCreator)在
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;
}
这就是「环绕通知」的完整形态:前(开事务)→ 中(执行目标)→
后(提交)→ 异常(回滚)。
几个必须记住的事务细节:
- 默认只回滚 RuntimeException 和
Error,受检异常(Checked
Exception)不回滚。若要回滚受检异常,需显式
@Transactional(rollbackFor = Exception.class)。 - 传播行为默认
REQUIRED:有事务则加入,没有则新建。 - 事务的「开/关」依赖线程绑定:事务管理器用
ThreadLocal(TransactionSynchronizationManager)把连接绑到当前线程,所以同一线程内多个
@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-web、spring-boot-starter-data-redis |
| 第三方/自定义 Starter | *-spring-boot-starter |
mybatis-spring-boot-starter、snowflake-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
的「开关三件套」):
@ConditionalOnClass—— 保证依赖存在(防止类缺失导致
ClassNotFoundException)。@ConditionalOnProperty——
给用户一个配置开关(snowflake.enabled=false
一键关闭)。@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;
/** 生成一个 ID(String 形式,避免前端精度丢失) */
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/Flux,WebFlux 会自动处理异步非阻塞的响应。
*/
@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 的局限(必须知道):
- 不支持 JPA/Hibernate(那是阻塞的 ORM),只能用
Spring Data R2DBC 这类「轻量映射」。 - 不支持懒加载、一级缓存、脏检查等 JPA
特性,实体关系处理能力弱。 - 生态不如 JDBC
成熟,部分数据库驱动、连接池功能还不完善。 - 不适合复杂事务编排,跨多个资源的响应式事务支持有限。
本章小结(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?
- IO 密集 +
高并发:网关、代理、实时数据推送、大量外部 HTTP
调用聚合等场景。 - 流式数据:SSE、WebSocket、实时行情。
- 全链路响应式:从数据库到服务间调用都走非阻塞(R2DBC
+ WebClient + 消息队列)。
什么时候别用 WebFlux?
- 依赖阻塞
JDBC/JPA:数据访问层是阻塞的,整个链路非阻塞就白费,还会拖垮
Netty 线程(这是最大的坑)。 - CPU 密集计算:响应式不解决 CPU
瓶颈,反而增加复杂度。 - 团队不熟悉响应式:异步调试成本高,维护性差。
- 大量现有同步代码/第三方阻塞库:混用阻塞调用会让「非阻塞」形同虚设。
「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 依赖 UserService、UserService 又依赖 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 还是 Negativematches 及原因 |
循环依赖报 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
密集、是否全链路响应式」。掌握了这些原理,你就不再是「会用框架」,而是「能解释框架、能排疑难杂症」的进阶工程师。
延伸阅读
- 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两节 - Spring Framework 官方文档:https://docs.spring.io/spring-framework/reference/
——IoC Container的 Bean 生命周期、Data Access
的事务管理章节 - 《Spring 揭秘》(王福强 著)—— 深入浅出讲透 IoC 与
AOP 的经典书籍 - 《Spring Boot 编程思想(核心篇)》(小马哥 著)——
源码级剖析自动装配与启动流程 - Reactor 官方参考文档:https://projectreactor.io/docs/core/release/reference/
—— 系统学习Mono/Flux、背压与调度器 - Spring Boot
源码(建议直接读):AutoConfigurationImportSelector、AbstractApplicationContext#refresh、DefaultSingletonBeanRegistry、AbstractAutoProxyCreator、TransactionAspectSupport
本教程为阶段 10 篇,版本约定 Spring Boot 3.2.x / JDK
17。下一篇(阶段 11)将进入架构设计与 DDD 主题。