SPRING BEAN · LIFECYCLE · CIRCULAR DEPENDENCY
一颗 Bean,从出生到退休
先看一张图纸怎么变成一个能用的 Bean,再看两个 Bean 互相等对方时,Spring 怎么靠三个货架解开死结。
BeanDefinition先有图纸,再有 Bean。扫描、@Bean、自动配置,产出的都是图纸。
BeanPostProcessor每个 Bean 出厂前都要过他的手两次。@PostConstruct、AOP 代理,都是他顺手办的。
初始化前一次,初始化后一次三级缓存成品架、半成品架、预约架。两个 Bean 互相等时,靠它们先借后还。
DefaultSingletonBeanRegistry 里的三个 Map一生的十四站
单例 Bean 从图纸到退休的完整路线。橙色两站是改装师出手的地方,虚线那站是为循环依赖准备的。
BeanDefinition扫描、@Bean、自动配置BeanFactoryPostProcessor比如把 ${…} 换成真实值new OrderService(…)构造器注入在这一步三级缓存万一有人提前来借@Autowired · @Value · @Resource字段和 setter 注入在这一步setBeanName · setBeanFactory · setApplicationContext各种 Aware 回调postProcessBeforeInitialization@PostConstruct 在这里被调用afterPropertiesSet()InitializingBeaninit-method@Bean(initMethod = …)postProcessAfterInitializationAOP 代理在这里套上singletonObjects之后被注入、被调用注解方法DisposableBean@Bean(destroyMethod = …)打印出来看看
一个 Bean 把所有回调都实现一遍,再配一个改装师。启动、关闭,控制台的顺序就是上面那张图。
public class Demo implements BeanNameAware, ApplicationContextAware,
InitializingBean, DisposableBean {
public Demo() { out("构造器"); }
@Autowired
public void setStock(StockService s) { out("属性注入 setStock"); }
public void setBeanName(String n) { out("BeanNameAware"); }
public void setApplicationContext(ApplicationContext c) { out("ApplicationContextAware"); }
@PostConstruct void postConstruct() { out("@PostConstruct"); }
public void afterPropertiesSet() { out("afterPropertiesSet"); }
void initMethod() { out("init-method"); }
@PreDestroy void preDestroy() { out("@PreDestroy"); }
public void destroy() { out("destroy"); }
void destroyMethod() { out("destroy-method"); }
}
@Configuration
class Config {
@Bean(initMethod = "initMethod", destroyMethod = "destroyMethod")
Demo demo() { return new Demo(); }
}
@Component
class Mechanic implements BeanPostProcessor { // 改装师
public Object postProcessBeforeInitialization(Object b, String n) {
if (b instanceof Demo) out("改装师 before");
return b;
}
public Object postProcessAfterInitialization(Object b, String n) {
if (b instanceof Demo) out("改装师 after");
return b; // 换成代理就返回代理
}
}
- 构造器
- 属性注入 setStock
- BeanNameAware
- ApplicationContextAware
- 改装师 before第一次
- @PostConstruct
- afterPropertiesSet
- init-method
- 改装师 after第二次
- …… 应用运行中,关闭时 ……
- @PreDestroy
- destroy
- destroy-method
口诀:初始化三兄弟是 @PostConstruct → afterPropertiesSet → init-method,销毁三兄弟顺序一样:@PreDestroy → destroy → destroy-method。
两组容易混的概念
一组管“改的是什么”,一组管“一生由谁负责”。
改图纸BeanFactoryPostProcessor
在第 2 站,任何 Bean 造出来之前,改 BeanDefinition。
例子:PropertySourcesPlaceholderConfigurer 把 ${…} 换成真实值;ConfigurationClassPostProcessor 解析 @Configuration 和自动配置。
改成品BeanPostProcessor
在第 7、10 站,每个 Bean 初始化前后各过一次,可以返回一个替身。
例子:CommonAnnotationBeanPostProcessor 调 @PostConstruct;AOP 的 AutoProxyCreator 套代理。
singleton默认。启动时造一个,一直用到关闭,走完全部 14 站。
prototype每次 getBean 造一个新的。造完就不管了,@PreDestroy 不会被调用。
request每个 HTTP 请求一个,请求结束就销毁。
session每个会话一个,会话失效就销毁。
循环依赖:A 等 B,B 等 A
造 A 要先有 B,造 B 又要先有 A。Spring 的办法:A 刚 new 出来还没装完零件,就先写一张预约单,谁急着要就先借给谁。
spring.main.allow-circular-references=true;否则 Spring 不写预约单,走到第 6 步就直接报错。singletonObjectsearlySingletonObjectssingletonFactories“正在创建”名单
调用栈(最下面是当前)
getBean("a")为什么是三级,两级不够吗?
问题出在 AOP。借出去的 A,必须和最后放上成品架的 A 是同一个对象。
只有两级会怎样
- 要么每个 Bean 一 new 出来就先套好代理,可大多数 Bean 根本没被循环引用,白做了,也打乱了“初始化完再代理”的顺序。
- 要么借给 B 的是真身,最后成品架上放的却是代理。B 手里拿错了对象,事务、切面对它全部失效。
第三级的预约单
预约单只存一个“领取方法”(() -> getEarlyBeanReference(a))。没人来借,A 照常走完流程,第 10 站再套代理;真有人来借,才当场决定给真身还是给代理,并把结果放上二级货架,保证之后谁来拿都是同一个。
Object getSingleton(String name) {
Object bean = singletonObjects.get(name); // 一级:成品
if (bean == null && isSingletonCurrentlyInCreation(name)) { // 正在创建,说明遇上循环
bean = earlySingletonObjects.get(name); // 二级:已经借出去过的
if (bean == null) {
ObjectFactory<?> ticket = singletonFactories.get(name); // 三级:预约单
if (ticket != null) {
bean = ticket.getObject(); // 当场决定:真身还是代理
earlySingletonObjects.put(name, bean);
singletonFactories.remove(name);
}
}
}
return bean;
}
哪些循环能解,怎么拆
关键只有一条:A 能不能在“还没装零件”的时候先被 new 出来。
| 情况 | 能解吗 | 原因 |
|---|---|---|
| 单例 + 字段 / setter 注入 | 能 | 先 new 出来,再写预约单。Boot 2.6+ 要手动打开开关 |
| 单例 + 构造器注入 | 不能 | 还没 new 出来,写不了预约单 |
| 原型 prototype | 不能 | 原型不进货架,每次都新造 |
| 带 @Async 的单例 | 常常不能 | @Async 的代理只在第 10 站做,提前借出去的是真身 |
| 注入点加 @Lazy | 能 | 先给一个懒代理,第一次调用时才去拿真的 |
把 A、B 都要用的那部分抽成 C,让 A、B 都依赖 C。
public A(@Lazy B b)。构造器注入也能用。
ObjectProvider<B>,调用 getObject() 时才去拿 B。
spring.main.allow-circular-references=true,只对字段 / setter 注入有用。
JDK 8 和 JDK 17:流程没变,默认值和包名变了
十四站的顺序、三级缓存,Spring 5 和 Spring 6 完全一样。
Spring 5 · Boot 2
JDK 8+- Boot 2.5 及以前默认允许循环依赖,三级缓存默默兜底
- Boot 2.6 起默认禁止,启动时直接打印依赖环
@PostConstruct、@PreDestroy、@Resource在javax.annotation包。JDK 8 自带,JDK 11 起移出 JDK,需要额外依赖- Spring 4.3 起,类只有一个构造器时可以不写 @Autowired
Spring 6+ · Boot 3+
JDK 17+- 默认禁止循环依赖,和 Boot 2.6+ 一样
- 注解换成
jakarta.annotation.PostConstruct/PreDestroy/Resource;@Inject换成jakarta.inject.Inject - 还用 javax 版注解的老代码,不会报错,回调静默不执行
- 打 GraalVM 原生镜像时,图纸在构建期就生成好(AOT),运行时不再扫描
常踩的坑
多数是没想清楚“这一刻 Bean 走到了第几站”。
Boot 2.6+ 默认禁止循环依赖。先拆职责或用 @Lazy,实在不行再打开开关。
三级缓存救不了构造器循环。在其中一个构造器参数上加 @Lazy,或重构。
报 “injected into other beans … in its raw version as part of a circular reference, but has eventually been wrapped”。去掉循环,或在注入点加 @Lazy。
字段注入在第 5 站,构造器在第 3 站。构造时就要用的依赖,改成构造器注入。
没有事务:代理第 10 站才套上,而且是 this 调用。改用 ApplicationRunner,从别的 Bean 调。
单例只造一次,原型也只在那时注入一次。每次都要新的,用 ObjectProvider<T>.getObject() 或 @Lookup。
容器造完原型就不管了。连接、线程之类的资源要自己释放。
某个改装师依赖了普通 Bean,那个 Bean 被提前造出来,错过了部分改装(比如没被 AOP 代理)。改装师的依赖用 ObjectProvider 延迟获取,在配置类里用 static @Bean 声明改装师。