资讯详情

Spring Bean生命周期与三级缓存:从依赖注入到循环依赖的源码级拆解

📅 2026/10/9 3:53:07 | 华诺云谱 👁 阅读
Spring Bean生命周期与三级缓存:从依赖注入到循环依赖的源码级拆解
1. 项目概述第一次看到Spring Bean别慌先说个真实感受很多刚接触Spring的朋友第一次看到ApplicationContext context new ClassPathXmlApplicationContext(spring.xml)这一行代码再看到context.getBean(userService)拿到一个能直接调方法的对象都会觉得这玩意儿像个黑魔法。明明是自己在XML或者注解里写的一个普通类怎么Spring就能帮我把对象建好、把依赖塞进去、甚至把代理对象偷偷换掉这篇文章要干的事就是把Spring最核心的Bean从0到1拆开给你看。不管你是刚学Java的校招生还是写了两三年业务但一直没搞懂底层的老开发这篇文章都能让你建立一条完整的认知链路Bean到底是什么、Spring是怎么把它造出来的、往里面塞依赖靠的是什么机制、出现循环依赖时三级缓存怎么兜底。我会先给你一个打通全局的框架认知然后逐层拆解Bean的生命周期和创建细节再用代码级别的证据带你去三级缓存的内部看一圈最后给出实际开发中最容易踩的坑和排查思路。整篇文章不追求把Spring源码从头讲到位而是追求让你看完之后能够看着Spring的报错日志、说出哦这是哪一步出了问题能手动模拟Spring创建Bean的大致顺序甚至出去面试被问到Bean生命周期时不再心虚。1.1 用点菜和出菜来理解Bean我们要聊的可能有点抽象但用一个餐馆的比喻就能立刻拉近。把Spring容器想象成一个餐厅的中央厨房。你写好的每个类比如UserService、OrderService相当于一道道菜的菜谱。Spring这个厨房拿到菜谱以后并不急着做菜它是先把菜谱登记在一个大菜单上——这一步在Spring里叫BeanDefinition的注册。当你真正getBean(userService)厨房才开始点火做菜如果UserService这道菜需要用到OrderService这道菜做配菜厨房会先去做配菜做完再炒主菜——这一步就是依赖注入。最后炒好的菜放在出餐口你随时可以取同一道菜如果默认是单例厨房会考虑要不要一直用同一份这就是Bean的作用域和单例缓存。这套比喻的好处在于它能让你在脑中建立起一套容器管理对象的心智模型。你写的类不再是你在代码里new出来的而是交出去、由容器决定在什么时候、以什么方式创建、以及创建完之后怎么增强。1.2 这篇文章适合谁能收获什么如果你满足以下任一条件建议读完刚学完Spring Boot基础知道Autowired怎么用但不知道为什么能注入成功面试准备阶段需要系统梳理Bean生命周期避免被连环追问BeanPostProcessor在哪个环节生效线上遇到BeanCurrentlyInCreationException官方叫它循环依赖错误你想彻底搞懂而不是靠猜想从会调API进阶到能看懂框架设计逻辑的Java工程师。这篇文章的干货浓度会比较密但我会在关键点上反复用日常类比和代码片段帮你消化。读的时候也不用一口气背下来更建议把它当工具文需要哪个环节的知识直接跳到对应的小节去查。2. 核心细节拆解Bean的生命周期到底怎么走要说清楚Spring Bean绕不开的一件事就是生命周期。很多人在面试时能背出实例化-属性填充-初始化-销毁但这四个词太笼统了。你至少得知道实例化之前还有BeanDefinition的解析属性填充本质是依赖注入初始化阶段其实是BeanPostProcessor的主战场。这一整条线上每一步都有对应的扩展点和常见坑。2.1 从配置到BeanDefinition——一切的前提先明确一个问题Spring容器启动的时候会先扫描我们指定的包路径、解析XML、或者读取配置类里的Bean方法把这些信息封装成一个个BeanDefinition。BeanDefinition不是Bean本身它是Bean的配方。ClassPathXmlApplicationContext context new ClassPathXmlApplicationContext(applicationContext.xml);这行代码的执行过程中Spring先把XML里的每一个bean标签解析成BeanDefinition配方里记录了这个Bean的类名、是否懒加载、作用域是单例还是原型、构造方法的参数、属性的依赖关系等等。注册到容器内部的DefaultListableBeanFactory.beanDefinitionMap本质是一个ConcurrentHashMap里。名字容易混淆我下面标一下两个关键概念的区别BeanDefinition类似菜谱描述了Bean该怎么创建不包含实际对象Bean实例类似做好的菜是按菜谱真正创建出来的对象。这一步里有一个很多人忽略的扩展点BeanFactoryPostProcessor。它能在Bean实例创建之前对BeanDefinition做修改。最常见的实现是PropertySourcesPlaceholderConfigurer它把${jdbc.url}这种占位符替换成配置项里的真实值。如果你自己实现过BeanFactoryPostProcessor你就知道这个阶段离Bean真正创建还很远但已经可以改配方了。2.2 实例化策略构造器、工厂方法和FactoryBean配方有了真正实例化Bean的时候Spring并不是只会走默认的无参构造。实际生产里常见三种方式构造器实例化最普通通常就是new一个对象静态工厂方法实例化通过类名直接调用某个静态方法拿到对象XML里对应factory-method实例工厂方法实例化先创建一个工厂Bean再通过工厂Bean的某个方法创建目标BeanXML里对应factory-bean和factory-methodFactoryBean接口这是最容易被误会的点。FactoryBean是一个特殊的Bean它本身会被容器管理但你getBean的时候拿到的不是FactoryBean本身而是它getObject()的返回对象。MyBatis的SqlSessionFactoryBean就是典型的例子。值得注意的是Spring在实例化之前会先判断这个Bean有没有对应的InstantiationAwareBeanPostProcessor。如果有并且它的postProcessBeforeInstantiation方法返回了一个代理对象那么Bean的实例化过程会被直接短路后续的普通流程就不走了。这个机制是一系列AOP代理的高级入口也很容易把不熟悉源码的人绕晕。2.3 属性填充和依赖注入的区别很多教程把属性填充和依赖注入当成一回事其实从Spring源码角度它们是一个动作的不同视角populateBean这个方法负责给实例化好的对象填充属性。填充的时机是实例化完成后初始化逻辑执行之前。填充的过程中Spring会遍历PropertyValues判断每个属性依赖是否已经准备好了如果没准备好就触发依赖解析也就是去容器中获取被注入的Bean——这就是依赖注入发生的本质。Autowired、Resource、Value这些注解最终都通过AutowiredAnnotationBeanPostProcessor等后处理器在属性填充环节发挥作用。这里有一个关键细节构造器注入和属性注入的发生时间完全不同。构造器注入发生在实例化阶段因为Spring必须先拿到构造器参数才能new出对象属性注入则发生在实例化之后。所以遇到循环依赖时构造器注入会直接抛出异常而属性注入还有机会通过三级缓存来化解。这个差异会在下一节详细展开。2.4 初始化阶段BeanPostProcessor的主战场属性填充完成之后Spring会调用一系列初始化回调网上经典的Bean生命周期流程图里列的那一串回调多半都集中在初始化阶段。我按调用顺序梳理一遍invokeAwareMethods如果Bean实现了BeanNameAware、BeanClassLoaderAware、BeanFactoryAware在这里回调把BeanName和BeanFactory等容器的信息交给BeanBeanPostProcessor.postProcessBeforeInitialization初始化前回调注意这里能拿到的Bean已经实例化且填充了属性只是还没执行InitializingBean和init-methodInitializingBean.afterPropertiesSetBean自己定义的初始化回调init-methodXML或Bean(initMethodxxx)指定的初始化方法BeanPostProcessor.postProcessAfterInitialization初始化后回调AOP动态代理通常在这里生成。也就是说PostConstruct注解其实是通过CommonAnnotationBeanPostProcessor实现的它的执行时机也在postProcessBeforeInitialization阶段。如果你在面试时被问到AOP代理在哪一步创建最准确的回答是通常发生在BeanPostProcessor.postProcessAfterInitialization具体是AbstractAutoProxyCreator的postProcessAfterInitialization方法。2.5 销毁阶段单例Bean在容器关闭时会依次回调PreDestroy注解标注的方法DisposableBean.destroy()接口方法destroy-method指定的方法。原型作用域的Bean默认不交给Spring管理销毁因为容器不持有它的引用也没办法统一处理。这一点很多人在多例场景踩过坑下面第5节会专门讲。3. 实操过程解析拉着Spring源码走一遍Bean创建这一节我们用追踪源码的方法代替背诵结论。毕竟面试官问流程的时候最忌讳只背结论不给证据。你如果能说出我是从哪一行源码看到的、那个方法叫什么说服力完全不一样。3.1 入口getBean到doGetBean所有获取Bean的入口都收敛到AbstractBeanFactory.getBean它内部调doGetBean。doGetBean第一步就是查单例缓存Object sharedInstance getSingleton(beanName); if (sharedInstance ! null args null) { bean getObjectForBeanInstance(sharedInstance, name, beanName, null); }这里调用的getSingleton就是我们说的一级缓存。如果拿不到再走创建流程。创建之前还会做一次getSingleton(beanName, () - createBean(...))这样的回调式获取目的很简单保证在并发情况下同一个单例Bean只会被创建一次。如果你自己实现过双重检查锁会发现Spring在这里的思路和你写并发代码时一模一样。3.2 createBean到doCreateBeancreateBean最终落到AbstractAutowireCapableBeanFactory.createBean里面有一个关键方法Object bean resolveBeforeInstantiation(beanName, mbd); if (bean ! null) { return bean; }这一步就是前面提到的短路机制。如果某个InstantiationAwareBeanPostProcessor返回了代理对象后面的doCreateBean根本不会执行。正常情况没有代理继续往下就进了doCreateBean。doCreateBean内部做了几件事创建实例createBeanInstance处理构造器选择、参数解析提前暴露引用addSingletonFactory这一步是三级缓存的核心后面单独展开属性填充populateBean初始化initializeBean。注意一个细节Spring默认情况下并不是等Bean完全初始化之后才暴露引用而是在实例化之后、属性填充之前就已经把单例工厂暴露到缓存里了。这样做的唯一目的就是解决循环依赖。如果你不理解这一步后面三级缓存的一切都看不懂。3.3 单例缓存的三层结构我们直接上手看源码。Spring的默认单例注册表是DefaultSingletonBeanRegistry内部有三个关键的Map// 一级缓存存放完全创建好的单例Bean MapString, Object singletonObjects new ConcurrentHashMap(256); // 二级缓存存放早期暴露的Bean引用没完成属性填充和初始化 MapString, Object earlySingletonObjects new ConcurrentHashMap(16); // 三级缓存存放ObjectFactory用于生成早期暴露的Bean MapString, ObjectFactory? singletonFactories new HashMap(16);三个Map的作用我用最通俗的方式解释一级缓存最终拿荣誉证书的选手对象完整初始化完毕三级缓存赛后采访区的待讲稿还没完全准备好但已经可以拿来提前应对采访二级缓存如果某个早期引用从待讲稿转成了实际对象就会暂存在这里防止重复生成。这里最精妙的设计在于三级缓存存放的是ObjectFactory而不是对象本身。ObjectFactory的getObject()方法在获取早期引用的时候会回头检查当前Bean是否存在SmartInstantiationAwareBeanPostProcessor如果有调它的getEarlyBeanReference返回一个可能被代理过的提前引用。发不发生AOP代理在这里就能决定。这也是为什么Spring不直接用二级缓存塞入早期对象的原因——提前塞入的如果是普通对象后续AOP代理生成之后可能会发生引用不一致。3.4 三级缓存的实际执行流程我把getSingleton的核心逻辑用伪代码展示一下protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject this.singletonObjects.get(beanName); // 一级 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(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } } } return singletonObject; }过程中Bean A先实例化暴露三级缓存Bean A填充属性时发现需要Bean B于是创建Bean BBean B填充属性时发现需要Bean A于是它去三级缓存拿到A的早期引用再完成自己的创建最后Bean B创建成功容器回过头来把Bean A的属性填充也完成。这个流程完美解释了为什么属性注入能解决循环依赖、构造器注入不能解决循环依赖。3.5 手写一个简化版的三级缓存如果你还觉得抽象我建议自己动手建一个测试类模一下。下面的代码非常简陋但能让你直观看到三个Map之间的转移关系public class SimpleCycleTest { static MapString, Object singletonObjects new ConcurrentHashMap(); static MapString, Object earlySingletonObjects new ConcurrentHashMap(); static MapString, ObjectFactory? singletonFactories new ConcurrentHashMap(); public static void main(String[] args) { createBeanA(); System.out.println(最终一级缓存: singletonObjects.keySet()); } static void createBeanA() { A a new A(); // 实例化后立即暴露三级缓存 singletonFactories.put(a, () - a); // 假设A需要B B b (B) getBean(b); a.setB(b); // A完成创建升级一级缓存 singletonObjects.put(a, a); singletonFactories.remove(a); earlySingletonObjects.remove(a); System.out.println(A创建完成注入B: b); } static void createBeanB() { B b new B(); singletonFactories.put(b, () - b); // B需要A从缓存拿早期引用 A a (A) getBean(a); b.setA(a); singletonObjects.put(b, b); singletonFactories.remove(b); earlySingletonObjects.remove(b); System.out.println(B创建完成注入A早期引用: a); } static Object getBean(String name) { Object obj singletonObjects.get(name); if (obj ! null) { return obj; } Object early earlySingletonObjects.get(name); if (early ! null) { return early; } ObjectFactory? factory singletonFactories.get(name); if (factory ! null) { Object value factory.getObject(); earlySingletonObjects.put(name, value); singletonFactories.remove(name); return value; } if (b.equals(name)) { createBeanB(); } return singletonObjects.get(name); } }两个类A和B互相持有引用运行之后你会看到B拿到的A是早期引用而最终的A已经升级到一级缓存。核心思想其实就是先让别人能拿到我等我自己准备完了再补完全程。这一步想明白了Spring的循环依赖题基本就拿下了。4. 扩展机制与常见场景Bean的这些能力别忽略很多文章讲完生命周期就结束了但我认为还有两块值得说一是BeanPostProcessor这个扩展点到底怎么自定义使用二是Spring Boot场景下自动配置和Bean的扫描机制是怎么衔接的。这两块理解透了你的Spring水平才算真的上了一个台阶。4.1 自己实现一个BeanPostProcessorSpring留给开发者最多的灵活性就是BeanPostProcessor。你可以在Bean初始化的前后插入自己的逻辑。比如全局给某个接口的实现类注入一个公共的字段、做方法耗时统计、甚至替换掉Bean的实现。一个最简单的实现Component public class MyBeanPostProcessor implements BeanPostProcessor { Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof Animal) { System.out.println(before init: beanName); ((Animal) bean).setName(defaultName); } return bean; } Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof Animal) { System.out.println(after init: beanName); } return bean; // 也可以在这里返回代理对象 } }注意一个关键细节BeanPostProcessor本身是Bean但它们必须在普通Bean实例化之前被实例化。Spring会提前获取所有实现了BeanPostProcessor接口的Bean在准备普通单例之前调用registerBeanPostProcessors。所以如果你在这个processor里又依赖了别的普通Bean很容易出现初始化顺序相关的诡异问题。4.2 Spring Boot自动配置与Bean的扫描顺序在Spring Boot场景下Bean的来源主要有SpringBootApplication里自带的ComponentScan扫描出来的业务Bean自动配置类比如DataSourceAutoConfiguration通过ConditionalOnClass、ConditionalOnMissingBean等条件装配出来的Bean第三方的Configuration类中的所有Bean方法。自动配置的生效靠的是AutoConfigurationImportSelector把META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里的配置类全部导入。导入之后会经过一系列的Conditional判断。如果项目里你自己注册了DataSource自动配置通常因为ConditionalOnMissingBean自动退出避免冲突。搞清楚这个顺序很多MyBatis的数据源莫名其妙换了一个这种问题就能快速排查。遇到Bean覆盖或注册顺序混乱时第一反应不应该是猜而是打开spring-boot-autoconfigure源码里的自动配置类列表结合条件注解逐个检查。4.3 Bean的作用域到底怎么影响生命周期Spring Bean的scope属性最常见的是singleton和prototype但你做Web开发时还会遇到request、session、application。作用域不同生命周期的管理方式完全不同singleton容器启动时或者在第一次getBean时创建取决于是否懒加载整个容器共享一个实例容器销毁时统一销毁prototype每次getBean都会创建新实例创建完成后容器不再持有PreDestroy等销毁逻辑不会自动执行request一次HTTP请求一个实例请求结束时销毁session一个用户会话一个实例会话失效时销毁。用prototype时如果还需要注入单例Bean没问题但反过来单例Bean注入一个prototype的Bean就要小心了——注入的是创建时的那个实例后面再获取同一个Bean容器不会给你新对象。常见解法是用ObjectProvider每调用一次就getObject()一次。Service public class SingletonService { Autowired private ObjectProviderPrototypeService prototypeServiceProvider; public void doSomething() { PrototypeService service prototypeServiceProvider.getObject(); // 每次都是新实例 } }这个细节在业务里可能不常遇到但面试常问线上排查时也容易懵值得记一下。5. 常见问题与排查技巧实录最后一部分我整理一下自己在实际开发和带新人过程中遇到的高频问题。这些问题不看报错的话新手很容易翻车而且翻车的姿势基本都一样。5.1 报错BeanCurrentlyInCreationException一定是循环依赖吗先给结论不一定是。但90%以上是循环依赖或者说跟isSingletonCurrentlyInCreation状态有关。这个报错的本质是容器正在创建Bean A的过程中发现还需要再获取Bean A而当时A还没有准备好进入早期引用链路。最常见的就是构造器注入导致的循环依赖。比如Service public class A { private final B b; public A(B b) { this.b b; } } Service public class B { private final A a; public B(A a) { this.a a; } }这段代码启动就报错Requested bean is currently in creation: Is there an unresolvable circular reference?解决思路有三种把其中一个Autowired构造器注入改成setter或字段注入如果是业务设计上确实有回路考虑使用Lazy打破循环重构依赖关系抽出一个中间层让依赖变成单向的。我个人最推荐第三种。框架帮你解决问题是一回事但业务设计里如果出现大量循环依赖往往意味着模块划分有隐患趁早重构更划算。5.2 为什么Spring 6默认不允许循环依赖了这一点很多人没关注到。Spring 6对应Spring Boot 3开始默认把spring.main.allow-circular-references设为false。也就是说即使属性注入能解决循环依赖Spring也默不允许依赖循环存在启动时直接报错。这是框架有意收紧行为目的是逼着开发者把依赖关系梳理干净。如果你一定要兼容老项目可以显式配置spring: main: allow-circular-references: true但更推荐的做法是用上面说的方案去重构而不是一味靠配置绕过。长期来看循环依赖会让代码变得难测、难维护能避免就避免。5.3 加上Async为什么注入的Bean变成代理了这个现象背后其实覆盖了前面好几个知识点。EnableAsync开启后Spring用AsyncAnnotationBeanPostProcessor对目标Bean生成代理。你在Class里看这个Bean的类型是原始的UserService但实际注入的是CGLIB代理子类。如果这个Bean本身还参与了循环引用那么它在三级缓存阶段可能已经暴露过早期引用代理的生成时机就会变得更为敏感。排查这类问题时建议打印一下实际对象的ClassSystem.out.println(bean.getClass());如果类名里出现$$EnhancerBySpringCGLIB$$或$Proxy说明确实被代理了很多this.selfCall()方法自己调自己不走代理的诡异现象根因都在这里。5.4 自定义Bean方法反复执行是不是单例失效先排除作用域配置问题再检查配置类是不是被多次扫描注册。一个常见场景是同一份配置类既被ComponentScan扫描到又在另一个配置类里被Import重复引入于是配置类对应的Bean方法被调了多次。排查时可以开启日志加上debugtrue看项目启动时的Bean定义注册日志。如果同一个Bean名称出现了多次定义Spring默认情况下后面的定义会覆盖前面的但逻辑顺序并不那么好掌控。所以建议业务配置类统一放在明确的包路径下避免重复扫描。5.5 测试环境常见问题MockBean为什么有时不生效Spring Boot测试中MockBean会通过MockitoPostProcessor向容器注册一个mock的Bean定义替换掉原来的Bean。如果目标Bean在容器启动早期就被其他Bean引用了替换可能来不及生效。遇到这种情况一是可以改用MockitoBeanSpring Boot 3.4新注解二是可以确保mock的是最外层入口依赖让所有内部依赖走容器统一替换。这类问题本质上还是Bean定义注册时机的问题所以本文前面讲的那套BeanDefinition注册流程会成为排查所有诡异问题的底层认知。5.6 一套速记排查思路最后整理一个实践经验向的速查清单是我自己遇到Spring Bean异常时一定会按顺序做的看启动日志里的Bean定义注册顺序先确认要排查的Bean有没有被注册注册的来源是哪里看是否出现循环依赖直接看报错里有没有currently in creation关键字有就先确认是否构造器注入看是否被代理打印getClass()确认没有代理导致类型匹配异常看是否被多个配置类重复定义全局搜索BeanName确认只有一处定义看作用域单例Bean注入原型Bean的需求是否没用对ObjectProvider。这套流程不是理论推演出来的是踩过线上问题后逐渐沉淀的排查习惯。每次遇到类似为什么Bean不是我以为的那样的问题照着这份清单走一遍基本十分钟内能定位方向。6. 写在最后的个人体会拆完Spring Bean的整个工作原理我最大的一个感受是框架设计到深处本质上还是在处理创建对象、管理依赖、控制时机这老三样。三级缓存也好、BeanPostProcessor也好都是在回答三个问题对象什么时候创建、依赖什么时候给、额外逻辑什么时候插。把这三个问题装进脑子里再看任何DI容器比如Guice甚至自己写的IoC都会觉得似曾相识。如果你今天只记住一句话我建议记住这个Spring不是在帮你写对象而是接管了对象的出生到销毁。你写的类只是被它管理的资源很多时候它给你的那个对象未必是你原始类直接的实例可能是被包装、被增强、被提前暴露过的。所有诡异的问题都源于这个未必直接。我在带新人时有一个习惯让他们手写一个极简的IoC容器不用太多几十行就够了只要能管理单例、支持简单的依赖注入再把循环依赖的场景模拟出来看看会发生什么。做过一遍的人再去读Spring源码很多云里雾里的地方都会瞬间清晰。这篇文章算是把我脑中的Spring Bean地图完整摊开了一遍希望能帮你少走我当年走过的那些弯路。如果后续有机会我再接着把BeanPostProcessor的实战场景和Spring AOP的切面执行顺序展开聊聊。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑