SPRING BEAN · LIFECYCLE · CIRCULAR DEPENDENCY

一颗 Bean,从出生到退休

先看一张图纸怎么变成一个能用的 Bean,再看两个 Bean 互相等对方时,Spring 怎么靠三个货架解开死结。

图纸BeanDefinition

先有图纸,再有 Bean。扫描、@Bean、自动配置,产出的都是图纸。

写着:哪个类、单例还是原型、依赖谁
改装师BeanPostProcessor

每个 Bean 出厂前都要过他的手两次。@PostConstruct、AOP 代理,都是他顺手办的。

初始化前一次,初始化后一次
三个货架三级缓存

成品架、半成品架、预约架。两个 Bean 互相等时,靠它们先借后还。

DefaultSingletonBeanRegistry 里的三个 Map
配角 Bean 就是那颗豆子,你写的类的一个对象 改图纸的人 BeanFactoryPostProcessor 工厂 ApplicationContext,管所有 Bean 的一生

一生的十四站

单例 Bean 从图纸到退休的完整路线。橙色两站是改装师出手的地方,虚线那站是为循环依赖准备的。

图纸容器启动时
1登记图纸BeanDefinition扫描、@Bean、自动配置
2改图纸BeanFactoryPostProcessor比如把 ${…} 换成真实值
出生实例化
3调用构造器new OrderService(…)构造器注入在这一步
4写预约单三级缓存万一有人提前来借
装零件属性填充
5注入依赖@Autowired · @Value · @Resource字段和 setter 注入在这一步
初始化initializeBean
6认亲setBeanName · setBeanFactory · setApplicationContext各种 Aware 回调
7改装师第一次postProcessBeforeInitialization@PostConstruct 在这里被调用
8自检afterPropertiesSet()InitializingBean
9自定义初始化init-method@Bean(initMethod = …)
10改装师第二次postProcessAfterInitializationAOP 代理在这里套上
上岗可以用了
11放上成品架singletonObjects之后被注入、被调用
退休容器关闭时
12@PreDestroy注解方法
13destroy()DisposableBean
14destroy-method@Bean(destroyMethod = …)

打印出来看看

一个 Bean 把所有回调都实现一遍,再配一个改装师。启动、关闭,控制台的顺序就是上面那张图。

out(…) 就是 System.out.println
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;                                   // 换成代理就返回代理
    }
}
控制台
  1. 构造器
  2. 属性注入 setStock
  3. BeanNameAware
  4. ApplicationContextAware
  5. 改装师 before第一次
  6. @PostConstruct
  7. afterPropertiesSet
  8. init-method
  9. 改装师 after第二次
  10. …… 应用运行中,关闭时 ……
  11. @PreDestroy
  12. destroy
  13. 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 Boot 2.6 起默认禁止循环依赖。下面前两个剧情,要先配上 spring.main.allow-circular-references=true;否则 Spring 不写预约单,走到第 6 步就直接报错。
一级 · 成品架singletonObjects
二级 · 半成品架earlySingletonObjects
三级 · 预约架singletonFactories
成品半成品预约单代理

“正在创建”名单

调用栈(最下面是当前)

    1/10
    getBean("a")

    为什么是三级,两级不够吗?

    问题出在 AOP。借出去的 A,必须和最后放上成品架的 A 是同一个对象。

    只有两级会怎样

    • 要么每个 Bean 一 new 出来就先套好代理,可大多数 Bean 根本没被循环引用,白做了,也打乱了“初始化完再代理”的顺序。
    • 要么借给 B 的是真身,最后成品架上放的却是代理。B 手里拿错了对象,事务、切面对它全部失效。

    第三级的预约单

    预约单只存一个“领取方法”(() -> getEarlyBeanReference(a))。没人来借,A 照常走完流程,第 10 站再套代理;真有人来借,才当场决定给真身还是给代理,并把结果放上二级货架,保证之后谁来拿都是同一个。

    Spring 取单例的顺序(简化版,去掉了加锁)
    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。

    可以@Lazy

    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 走到了第几站”。

    启动报 The dependencies of some of the beans in the application context form a cycle

    Boot 2.6+ 默认禁止循环依赖。先拆职责或用 @Lazy,实在不行再打开开关。

    构造器互相依赖,报 BeanCurrentlyInCreationException

    三级缓存救不了构造器循环。在其中一个构造器参数上加 @Lazy,或重构。

    循环依赖碰上 @Async

    报 “injected into other beans … in its raw version as part of a circular reference, but has eventually been wrapped”。去掉循环,或在注入点加 @Lazy。

    构造器里用 @Autowired 的字段,是 null

    字段注入在第 5 站,构造器在第 3 站。构造时就要用的依赖,改成构造器注入。

    @PostConstruct 里调本类的 @Transactional 方法

    没有事务:代理第 10 站才套上,而且是 this 调用。改用 ApplicationRunner,从别的 Bean 调。

    单例里注入原型,只注入了一次

    单例只造一次,原型也只在那时注入一次。每次都要新的,用 ObjectProvider<T>.getObject() 或 @Lookup。

    原型 Bean 的 @PreDestroy 没执行

    容器造完原型就不管了。连接、线程之类的资源要自己释放。

    日志:is not eligible for getting processed by all BeanPostProcessors

    某个改装师依赖了普通 Bean,那个 Bean 被提前造出来,错过了部分改装(比如没被 AOP 代理)。改装师的依赖用 ObjectProvider 延迟获取,在配置类里用 static @Bean 声明改装师。