为什么要搞懂Spring生命周期
1. 面试高频题 - Bean生命周期、AOP代理生成时机、如何动态注册Bean...这些都是扩展点。关注我,下一篇专门讲面试技巧。
2. 工作需要 - 动态数据源、加载外部配置、缓存预热、审计日志、优雅关闭...这些场景都要用扩展点。
举个例子:今年双11,一个多租户系统宕机,原因是Nacos配置没生效,所有租户连到了同一个数据库。如果懂ApplicationContextInitializer,这问题根本不会发生。
💡 提示:完整项目代码和快速上手指南在文章末尾,可以直接跳到最后查看。
Spring Boot生命周期全景图
先看个整体,Spring Boot从启动到关闭,经历6个大阶段:
graph LR
A[应用启动] --> B[准备环境和配置]
B --> C[加载Bean定义]
C --> D[实例化Bean]
D --> E[初始化Bean]
E --> F[应用运行]
F --> G[关闭清理]
style A fill:#e1f5ff
style B fill:#fff9e6
style C fill:#fff9e6
style D fill:#ffe6f0
style E fill:#ffe6f0
style F fill:#e6ffe6
style G fill:#ffe6e6
15个扩展点就分布在这6个阶段:
| 阶段 | 扩展点数量 | 主要解决什么问题 |
|---|---|---|
| 阶段1-2:准备和加载 | 3个 | 从哪里读配置?哪些类要注册成Bean? |
| 阶段3:实例化Bean | 3个 | 怎么new对象?如何解决循环依赖? |
| 阶段4:初始化Bean | 4个 | 属性怎么注入?AOP代理怎么生成? |
| 阶段5:应用运行 | 3个 | 启动后做什么?如何预热缓存? |
| 阶段6:关闭清理 | 2个 | 关闭前如何释放资源? |
这篇文章用一个多租户系统,把15个扩展点串起来。看完你能知道:每个扩展点在什么时机执行,什么场景用什么扩展点。
不过为了降低运行门槛,我把PostgreSQL、Redis、Nacos都Mock掉了:
- 数据库用H2内存数据库
- Redis用ConcurrentHashMap
- Nacos用本地JSON文件
这样你clone代码就能直接跑,不用安装一堆环境。
我们的设计思路
先看个整体架构图:
graph TB
A1["[1.1] ApplicationContextInitializer<br/>加载Nacos配置<br/><small>TenantContextInitializer</small>"] --> A2["[1.2] BeanDefinitionRegistryPostProcessor<br/>注册数据源Bean<br/><small>TenantBeanRegistrar</small>"]
A2 --> A3["[1.3] BeanFactoryPostProcessor<br/>解密数据库密码<br/><small>PropertyDecryptorPostProcessor</small>"]
A3 --> B1["[2.3] FactoryBean<br/>创建H2数据源A<br/><small>MockDataSourceFactory</small>"]
A3 --> B3["[2.3] FactoryBean<br/>创建H2数据源B<br/><small>MockDataSourceFactory</small>"]
B1 --> B2["初始化表结构A<br/><small>执行schema.sql</small>"]
B3 --> B4["初始化表结构B<br/><small>执行schema.sql</small>"]
B2 --> C1["[3.3] BeanPostProcessor<br/>生成AOP审计代理<br/><small>AuditLogProxyCreator</small>"]
B4 --> C1
C1 --> C2["[3.4] Aware接口<br/>注入ApplicationContext<br/><small>TenantContextAware</small>"]
C2 --> D1["[4.1] SmartInitializingSingleton<br/>预热租户路由<br/><small>TenantCacheWarmer</small>"]
D1 --> D2["[4.3] ApplicationRunner<br/>预加载字典数据<br/><small>TenantDataPreloader</small>"]
D2 --> D3["[4.3] CommandLineRunner<br/>启动定时任务<br/><small>ScheduledTaskStarter</small>"]
style A1 fill:#fff9e6
style A2 fill:#fff9e6
style A3 fill:#fff9e6
style B1 fill:#ffe6f0
style B2 fill:#ffe6f0
style B3 fill:#ffe6f0
style B4 fill:#ffe6f0
style C1 fill:#e6f3ff
style C2 fill:#e6f3ff
style D1 fill:#e6ffe6
style D2 fill:#e6ffe6
style D3 fill:#e6ffe6
可以看到:
- 黄色区域是阶段1的3个扩展点,负责配置准备
- 粉色区域是阶段2的扩展点,负责创建数据源
- 蓝色区域是阶段3的扩展点,负责AOP代理和容器注入
- 绿色区域是阶段4的扩展点,负责缓存预热和启动任务
看到这个流程,你可能会问:为什么要在这些时机做这些事?
这就要从Spring的生命周期说起了。
阶段1:容器准备 - 配置怎么加载?
我们要解决的问题
多租户系统的配置通常存在Nacos配置中心。但问题来了:
- 本地有application.yml
- Nacos有配置
- 命令行还能传参数
Spring到底用哪个?优先级是什么?
我当时遇到的就是这个问题。Nacos配置明明改了,但应用还是用的旧配置。
扩展点1.1:ApplicationContextInitializer
后来发现,Spring容器刷新之前,有个ApplicationContextInitializer扩展点。这是最早能介入的地方,可以往Environment里塞配置。
sequenceDiagram
participant App as 应用启动
participant Init as Initializer
participant Env as Environment
participant Spring as Spring容器
App->>Init: 执行initialize
Init->>Env: 加载Nacos配置
Init->>Env: addFirst设最高优先级
Init->>Spring: 容器开始刷新
我们的实现是这样的:
public class TenantContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext context) {
ConfigurableEnvironment env = context.getEnvironment();
// 从Mock Nacos加载配置
MockNacosConfigLoader loader = new MockNacosConfigLoader();
Map<String, Object> nacosConfig = loader.loadTenantConfigAsProperties();
// 添加为最高优先级配置源
env.getPropertySources().addFirst(
new MapPropertySource("nacos-tenant-config", nacosConfig)
);
log.info("从Nacos加载了{}个租户的配置", nacosConfig.size() / 4);
}
}
注意这里用的是addFirst(),意思是把Nacos配置的优先级设为最高。这样即使本地yml和Nacos冲突,也会用Nacos的。
启动日志会打印:
[main] TenantContextInitializer : 从Nacos加载了2个租户的配置
你可能会问:怎么让Spring执行这个初始化器?
需要在META-INF/spring.factories里注册:
org.springframework.context.ApplicationContextInitializer=\
com.example.tenant.config.TenantContextInitializer
扩展点1.2:BeanDefinitionRegistryPostProcessor
配置加载完了,接下来要把租户数据源注册成Bean。
这里有个问题:租户数量是动态的,从配置中心读出来的。你不可能在代码里写死@Bean DataSource tenantA()。
Spring提供了BeanDefinitionRegistryPostProcessor扩展点,可以在运行时动态注册Bean。
sequenceDiagram
participant Spring
participant Reg as Registrar
participant Registry
Spring->>Reg: Bean定义加载完成
Reg->>Reg: 解析租户配置
loop 每个租户
Reg->>Registry: 注册数据源Bean
end
Spring->>Spring: 开始实例化Bean
代码大概是这样:
public class TenantBeanRegistrar implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
// 遍历配置中的租户
for (int i = 0; i < 10; i++) {
String tenantId = env.getProperty("tenant.datasources[" + i + "].tenantId");
if (tenantId != null) {
// 构建Bean定义(使用FactoryBean)
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(MockDataSourceFactory.class)
.addPropertyValue("tenantId", tenantId)
.addPropertyValue("jdbcUrl", env.getProperty("...jdbcUrl"));
// 注册到容器
registry.registerBeanDefinition("dataSource_" + tenantId,
builder.getBeanDefinition());
log.info("注册数据源Bean: dataSource_{}", tenantId);
}
}
}
}
日志会显示:
[main] TenantBeanRegistrar : 注册数据源Bean: dataSource_tenant-a
[main] TenantBeanRegistrar : 注册数据源Bean: dataSource_tenant-b
这就是MyBatis扫描Mapper接口的原理。MapperScannerConfigurer就是用这个扩展点,动态注册Mapper的Bean定义。
扩展点1.3:BeanFactoryPostProcessor
数据源Bean注册完了,但有个安全问题:数据库密码明文存在配置中心,不安全。
我们的做法是:配置中心存加密密码ENC(3DES:abc123...),启动时解密成明文。
BeanFactoryPostProcessor可以在Bean实例化之前,修改Bean的元数据(比如属性值)。
public class PropertyDecryptorPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
for (String beanName : beanFactory.getBeanDefinitionNames()) {
if (beanName.startsWith("dataSource_")) {
BeanDefinition bd = beanFactory.getBeanDefinition(beanName);
// 获取password属性
Object password = bd.getPropertyValues().get("password");
if (password.toString().startsWith("ENC(")) {
// 解密
String decrypted = decrypt3DES(password.toString());
bd.getPropertyValues().add("password", decrypted);
log.info("解密数据源密码: {}", beanName);
}
}
}
}
}
这样,等Bean实例化的时候,拿到的就是解密后的明文密码了。
到这里,容器准备阶段就结束了。此时:
- 配置已加载(来自Nacos)
- 租户数据源Bean已注册
- 密码已解密
接下来Spring会开始实例化这些Bean。
阶段2:Bean实例化 - 对象怎么创建?
我们要解决的问题
现在要创建数据源对象了。但H2数据源的创建比较复杂:
- 配置JDBC URL
- 创建HikariCP连接池
- 执行schema.sql初始化表结构
- 预热连接
如果直接在代码里new,会很乱。Spring提供了FactoryBean,专门用来创建复杂对象。
扩展点2.3:FactoryBean
FactoryBean是个工厂Bean,它的getObject()方法返回的才是真正的Bean。
graph LR
A[Spring容器] -->|getBean| B[DataSourceFactory]
B -->|getObject| C[HikariDataSource]
C --> D[初始化表结构]
D --> E[返回数据源]
style A fill:#e1f5ff
style B fill:#fff9e6
style C fill:#ffe6f0
style D fill:#e6f3ff
style E fill:#e6ffe6
代码是这样的:
public class MockDataSourceFactory implements FactoryBean<DataSource> {
private String tenantId;
private String jdbcUrl;
@Override
public DataSource getObject() throws Exception {
log.info("开始创建H2数据源: {}", tenantId);
// 创建HikariCP连接池
HikariConfig config = new HikariConfig();
config.setJdbcUrl(jdbcUrl);
config.setUsername("sa");
config.setPassword("");
HikariDataSource ds = new HikariDataSource(config);
// 初始化表结构
try (Connection conn = ds.getConnection()) {
ScriptUtils.executeSqlScript(conn,
new ClassPathResource("schema.sql"));
}
log.info("H2数据源创建完成: {}", tenantId);
return ds;
}
@Override
public Class<?> getObjectType() {
return DataSource.class;
}
}
当Spring需要dataSource_tenant-a这个Bean时,会调用FactoryBean的getObject(),返回的HikariDataSource就是真正的数据源。
日志会显示创建过程:
[main] MockDataSourceFactory : 开始创建H2数据源: tenant-a
[main] MockDataSourceFactory : H2数据库Schema初始化完成: tenant-a
[main] MockDataSourceFactory : H2数据源创建完成: tenant-a
扩展点2.1 和 2.2:两个不太常用的
还有两个实例化阶段的扩展点:
- InstantiationAwareBeanPostProcessor:可以在new对象前后干预
- SmartInstantiationAwareBeanPostProcessor:处理循环依赖、选择构造函数
这两个比较底层,一般业务代码用不到。主要是框架层面用,比如:
- Spring的
@Autowired注解就是用InstantiationAwareBeanPostProcessor实现的 - 循环依赖的解决用到了SmartInstantiationAwareBeanPostProcessor的
getEarlyBeanReference()
我们的项目里也简单实现了一下,主要是演示用。
阶段3:Bean初始化 - 属性怎么注入?代理怎么生成?
我们要解决的问题
现在Bean对象已经new出来了,但还是个"半成品":
- 属性还没注入(@Autowired还没生效)
- AOP代理还没生成(@Transactional还不起作用)
这个阶段就是要把Bean变成"成品"。
Bean初始化的完整流程
sequenceDiagram
participant Spring
participant Bean
participant BPP as PostProcessor
participant Init
Spring->>Bean: new对象
Spring->>Bean: 注入@Autowired
Spring->>BPP: postProcessBefore
Spring->>Init: afterPropertiesSet
Spring->>Bean: 执行@PostConstruct
Spring->>BPP: postProcessAfter
BPP->>Spring: 返回代理对象
扩展点3.3:BeanPostProcessor - 生成AOP代理
我们的多租户系统需要审计日志:记录谁在什么时候调用了什么方法。
传统做法是在每个方法里写日志代码,太麻烦。我们用AOP代理:
public class AuditLogProxyCreator implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 检查Bean上有没有@AuditLog注解
if (bean.getClass().isAnnotationPresent(AuditLog.class)) {
// 生成代理对象
ProxyFactory factory = new ProxyFactory(bean);
factory.addAdvice(new MethodInterceptor() {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 方法执行前打印日志
log.info("【审计】{}.{} - 入参: {}",
beanName,
invocation.getMethod().getName(),
Arrays.toString(invocation.getArguments()));
// 执行原方法
Object result = invocation.proceed();
// 方法执行后打印结果
log.info("【审计】{}.{} - 返回: {}",
beanName,
invocation.getMethod().getName(),
result);
return result;
}
});
Object proxy = factory.getProxy();
log.info("为Bean生成审计代理: {}", beanName);
return proxy; // 返回代理对象
}
return bean; // 不需要代理,返回原对象
}
}
注意最后的return proxy,这里返回的不是原始Bean,而是代理对象。Spring容器拿到的就是这个代理。
当你调用TenantService的方法时,实际调用的是代理对象的方法,会先打印日志,再执行业务逻辑。
日志会这样显示:
[main] AuditLogProxyCreator : 为Bean生成审计代理: tenantService
...
[http-nio-8080-exec-1] AuditLogProxyCreator : 【审计】tenantService.listUsers - 入参: [tenant-a]
[http-nio-8080-exec-1] AuditLogProxyCreator : 【审计】tenantService.listUsers - 返回: [{id=1, name=张三}, ...]
这就是Spring AOP的原理。@Transactional、@Async也是在BeanPostProcessor的这个时机生成代理的。
扩展点3.2:InitializingBean
还有个InitializingBean接口,可以在属性注入完成后做一些初始化工作。
public class TenantConfigInitializer implements InitializingBean {
@Autowired
private Environment environment;
@Override
public void afterPropertiesSet() throws Exception {
// 校验租户配置
String defaultTenant = environment.getProperty("tenant.default");
if (defaultTenant == null) {
throw new IllegalStateException("未配置默认租户");
}
log.info("租户配置校验通过,默认租户: {}", defaultTenant);
}
}
这个方法会在@Autowired注入完成后执行,可以用来做配置校验、资源初始化之类的。
扩展点3.4:Aware接口 - 注入容器对象
有时候我们需要在Bean里获取Spring容器本身,比如动态获取其他Bean、发布事件等。
Spring提供了几个Aware接口:
@Component
public class TenantContextHolder implements
ApplicationContextAware, // 注入ApplicationContext
BeanFactoryAware, // 注入BeanFactory
EnvironmentAware { // 注入Environment
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext context) {
TenantContextHolder.applicationContext = context;
}
// 工具方法:根据租户ID获取数据源
public static DataSource getDataSource(String tenantId) {
return applicationContext.getBean("dataSource_" + tenantId, DataSource.class);
}
}
这样我们就可以在任何地方通过TenantContextHolder.getDataSource("tenant-a")动态获取数据源了。
阶段4:启动完成 - 缓存怎么预热?
我们要解决的问题
Bean都初始化完了,但应用还不能对外服务。多租户系统需要:
- 把租户路由规则加载到Redis(避免每次请求都查数据库)
- 预加载字典数据
- 启动定时任务
这些逻辑要等所有Bean都初始化完成后才能执行。
扩展点4.1:SmartInitializingSingleton
Spring提供了SmartInitializingSingleton扩展点,在所有单例Bean初始化完成后执行。
graph LR
A[Bean1初始化] --> D[所有Bean完成]
B[Bean2初始化] --> D
C[Bean3初始化] --> D
D --> E[SmartInitializing执行]
E --> F[缓存预热]
E --> G[预加载数据]
style A fill:#ffe6f0
style B fill:#ffe6f0
style C fill:#ffe6f0
style D fill:#fff9e6
style E fill:#e6f3ff
style F fill:#e6ffe6
style G fill:#e6ffe6
代码是这样的:
@Component
public class TenantCacheWarmer implements SmartInitializingSingleton {
@Autowired
private ApplicationContext context;
@Autowired
private MockRedisTemplate redis;
@Override
public void afterSingletonsInstantiated() {
// 获取所有数据源Bean
Map<String, DataSource> dataSources = context.getBeansOfType(DataSource.class);
for (String beanName : dataSources.keySet()) {
String tenantId = extractTenantId(beanName); // dataSource_tenant-a -> tenant-a
// 预热租户路由到Redis
redis.set("tenant:" + tenantId + ":route", beanName);
log.info("预热缓存: tenant:{} -> {}", tenantId, beanName);
// 预加载字典数据
List<Dict> dicts = loadDictFromDB(dataSources.get(beanName));
redis.set("tenant:" + tenantId + ":dict", dicts);
log.info("预热字典数据: {} 共{}条", tenantId, dicts.size());
}
}
}
日志会显示预热过程:
[main] TenantCacheWarmer : 预热缓存: tenant:tenant-a -> dataSource_tenant-a
[main] MockRedisTemplate : 【Mock Redis】SET tenant:tenant-a:route = dataSource_tenant-a
[main] TenantCacheWarmer : 预热字典数据: tenant-a 共2条
扩展点4.2 和 4.3:ApplicationListener、ApplicationRunner
还有两个扩展点:
- ApplicationListener:监听容器启动完成事件
- ApplicationRunner:容器启动完成后执行任务
@Component
public class SystemReadyListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// 发送系统就绪消息到MQ
rabbitTemplate.convertAndSend("system.events", "系统已启动");
log.info("系统启动完成,已发送上线通知");
}
}
@Component
@Order(1) // 控制执行顺序
public class TenantDataPreloader implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// 预加载更多数据
log.info("预加载tenant-a的字典数据: 2条");
log.info("预加载tenant-b的字典数据: 2条");
}
}
这两个扩展点的区别:
- ApplicationListener是事件驱动的,可以监听多种事件
- ApplicationRunner是启动后执行一次,适合做数据初始化
阶段5:关闭清理 - 资源怎么释放?
我们要解决的问题
应用要关闭了,需要清理资源:
- 关闭每个租户的数据源连接
- 清空Redis缓存
- 关闭MQ连接
如果不清理,可能导致数据库连接泄漏。
扩展点5.1:DisposableBean
Spring提供了DisposableBean接口,在Bean销毁前执行。
@Component
public class ResourceCleaner implements DisposableBean {
@Autowired
private List<DataSource> allDataSources;
@Autowired
private MockRedisTemplate redis;
@Override
public void destroy() throws Exception {
log.info("容器关闭,开始清理资源");
// 关闭所有数据源
for (DataSource ds : allDataSources) {
if (ds instanceof HikariDataSource) {
((HikariDataSource) ds).close();
log.info("关闭数据源: {}", ds);
}
}
// 清空Redis缓存
redis.clear();
log.info("清空Redis缓存");
log.info("资源清理完成");
}
}
当你按Ctrl+C或者执行kill -15时,Spring会优雅关闭,调用这个destroy()方法。
日志会显示清理过程:
[main] ResourceCleaner : 容器关闭,开始清理资源
[main] ResourceCleaner : 关闭数据源: dataSource_tenant-a
[main] ResourceCleaner : 关闭数据源: dataSource_tenant-b
[main] MockRedisTemplate : 【Mock Redis】CLEAR 清空所有缓存
[main] ResourceCleaner : 清空Redis缓存
[main] ResourceCleaner : 资源清理完成
注意:如果你用kill -9强制杀进程,这个方法不会执行。所以生产环境一定要用优雅关闭。
完整的流程图
把所有扩展点串起来,整个流程是这样的:
graph TB
Start[应用启动] --> Init1["[1.1] 加载Nacos配置<br/>TenantContextInitializer"]
Init1 --> Init2["[1.2] 注册数据源Bean<br/>TenantBeanRegistrar"]
Init2 --> Init3["[1.3] 解密密码<br/>PropertyDecryptorPostProcessor"]
Init3 --> Create1["[2.3] 创建数据源A<br/>MockDataSourceFactory"]
Init3 --> Create2["[2.3] 创建数据源B<br/>MockDataSourceFactory"]
Create1 --> InitBean1["[3.1] 检查AuditLog<br/>AuditLogProxyCreator"]
Create2 --> InitBean1
InitBean1 --> InitBean2["[3.2] 校验租户配置<br/>TenantConfigInitializer"]
InitBean2 --> InitBean3["[3.3] 生成AOP代理<br/>AuditLogProxyCreator"]
InitBean3 --> InitBean4["[3.4] 注入容器对象<br/>TenantContextAware"]
InitBean4 --> Startup1["[4.1] 预热缓存<br/>TenantCacheWarmer"]
Startup1 --> Startup2["[4.2] 发送启动通知<br/>SystemReadyListener"]
Startup2 --> Startup3["[4.3] 预加载数据<br/>TenantDataPreloader"]
Startup3 --> Running[应用运行]
Running --> Shutdown[收到关闭信号]
Shutdown --> Cleanup["[5.1] 清理资源<br/>ResourceCleaner"]
Cleanup --> End[容器关闭]
style Start fill:#e1f5ff
style Init1 fill:#fff9e6
style Init2 fill:#fff9e6
style Init3 fill:#fff9e6
style Create1 fill:#ffe6f0
style Create2 fill:#ffe6f0
style InitBean1 fill:#e6f3ff
style InitBean2 fill:#e6f3ff
style InitBean3 fill:#e6f3ff
style InitBean4 fill:#e6f3ff
style Startup1 fill:#e6ffe6
style Startup2 fill:#e6ffe6
style Startup3 fill:#e6ffe6
style Running fill:#f0f0f0
style Shutdown fill:#ffe6e6
style Cleanup fill:#ffe6e6
style End fill:#d0d0d0
每个节点都标注了:
- 扩展点编号(如[1.1]、[2.3])
- 业务功能(加载配置、创建数据源)
- 实现类名(TenantContextInitializer)
这样你看日志的时候,就能对应上是哪个扩展点在执行了。
几个要注意的地方
1. 扩展点的执行次数
不是所有扩展点都只执行1次。我整理了一下:
只执行1次(容器级别):
- ApplicationContextInitializer
- BeanDefinitionRegistryPostProcessor
- BeanFactoryPostProcessor
- SmartInitializingSingleton
- ApplicationListener
- ApplicationRunner
每个Bean都执行(Bean级别):
- InstantiationAwareBeanPostProcessor
- BeanPostProcessor(Before和After)
比如你有10个Bean,BeanPostProcessor的postProcessBeforeInitialization会执行10次。
2. 为什么AOP有时候不生效
我之前遇到过一个坑:在Service类里,方法A调用方法B,方法B上有@Transactional,但事务不生效。
原因是:方法A里调用的是this.methodB(),this不是代理对象,而是原始对象。
解决办法:
// 错误写法
public void methodA() {
this.methodB(); // this不是代理
}
// 正确写法
@Autowired
private ApplicationContext context;
public void methodA() {
TenantService proxy = context.getBean(TenantService.class);
proxy.methodB(); // 通过代理对象调用
}
3. ApplicationContextInitializer怎么注册
这个扩展点比较特殊,需要手动注册。有三种方式:
// 方式1:META-INF/spring.factories(推荐)
org.springframework.context.ApplicationContextInitializer=\
com.example.TenantContextInitializer
// 方式2:启动类手动添加
public static void main(String[] args) {
SpringApplication app = new SpringApplication(Application.class);
app.addInitializers(new TenantContextInitializer());
app.run(args);
}
// 方式3:application.yml配置
context:
initializer:
classes: com.example.TenantContextInitializer
我一般用方式1,因为把配置放在spring.factories,不用改启动类。
4. FactoryBean的坑
获取FactoryBean有个细节:
// 获取FactoryBean创建的对象(数据源)
DataSource ds = context.getBean("dataSource_tenant-a", DataSource.class);
// 获取FactoryBean本身
MockDataSourceFactory factory = context.getBean("&dataSource_tenant-a", MockDataSourceFactory.class);
注意Bean名称前面要加&符号。我第一次用的时候没加,拿到的是数据源而不是工厂,调试了半天。
实际运行效果
我把完整的启动日志贴一下,你可以看到扩展点的执行顺序:
// 阶段1:容器准备
2025-11-24 20:02:45 [main] TenantContextInitializer : 从Nacos加载了2个租户的配置
2025-11-24 20:02:46 [main] TenantBeanRegistrar : 注册数据源Bean: dataSource_tenant-a
2025-11-24 20:02:46 [main] TenantBeanRegistrar : 注册数据源Bean: dataSource_tenant-b
2025-11-24 20:02:46 [main] PropertyDecryptorPostProcessor : 解密数据源密码: dataSource_tenant-a
// 阶段2:Bean实例化
2025-11-24 20:02:46 [main] MockDataSourceFactory : 开始创建H2数据源: tenant-a
2025-11-24 20:02:46 [main] MockDataSourceFactory : H2数据库Schema初始化完成: tenant-a
2025-11-24 20:02:46 [main] MockDataSourceFactory : H2数据源创建完成: tenant-a
// 阶段3:Bean初始化
2025-11-24 20:02:46 [main] AuditLogProxyCreator : 检测到@AuditLog注解: tenantService
2025-11-24 20:02:46 [main] TenantConfigInitializer : 租户配置校验通过,默认租户: tenant-a
2025-11-24 20:02:46 [main] AuditLogProxyCreator : 为Bean生成审计代理: tenantService
// 阶段4:启动完成
2025-11-24 20:02:52 [main] TenantCacheWarmer : 预热缓存: tenant:tenant-a -> dataSource_tenant-a
2025-11-24 20:02:52 [main] TenantCacheWarmer : 预热字典数据: tenant-a 共2条
2025-11-24 20:02:52 [main] SystemReadyListener : 系统启动完成,已发送上线通知
2025-11-24 20:02:52 [main] TenantDataPreloader : 预加载tenant-a的字典数据: 2条
// 应用运行中,处理请求
2025-11-24 20:02:58 [http-nio-8080-exec-1] AuditLogProxyCreator : 【审计】tenantService.listUsers - 入参: [tenant-a]
2025-11-24 20:02:58 [http-nio-8080-exec-1] AuditLogProxyCreator : 【审计】tenantService.listUsers - 返回: [{id=1, name=张三}, ...]
// 阶段5:关闭清理
2025-11-24 20:03:00 [main] ResourceCleaner : 容器关闭,开始清理资源
2025-11-24 20:03:00 [main] ResourceCleaner : 关闭数据源: dataSource_tenant-a
2025-11-24 20:03:00 [main] ResourceCleaner : 清空Redis缓存
2025-11-24 20:03:00 [main] ResourceCleaner : 资源清理完成
从日志可以清楚地看到每个扩展点的执行时机。
最后
这篇文章用一个多租户系统,把Spring Boot的15个扩展点串起来了。每个扩展点都有:
- 执行时机
- 业务场景
- 代码实现
- 日志验证
看完之后,你应该能理解Spring容器从启动到关闭的完整流程。
项目代码已经放在Gitee了,clone下来对着日志跑一遍,理解会更快。
后续会持续分享硬核干货,有兴趣的话,请关注我。
快速上手
项目用的是Mock方式,无需安装任何中间件。
# 克隆项目
git clone https://gitee.com/sh_wangwanbao/surfing-spring-extension/tree/main
# 启动应用
mvn spring-boot:run
# 测试租户切换
curl http://localhost:8080/demo/users?tenantId=tenant-a
curl http://localhost:8080/demo/users?tenantId=tenant-b
应用启动大概3-5秒,然后观察日志输出,可以看到15个扩展点的执行过程。
都看到这里了,点个赞呗,你的支持是我创作的最大动力。
Gitee项目:gitee.com/sh_wangwanb…