多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Spring refresh()源码导读:从IoC容器初始化到Bean生命周期

Spring refresh()源码导读:从IoC容器初始化到Bean生命周期 我先说个结论Spring的refresh()方法是所有Spring面试题的最大公约数。不管是问IoC原理、Bean生命周期、三级缓存、Autowired怎么生效还是问你项目启动时到底发生了什么追到最后都会落到AbstractApplicationContext.refresh()这一个方法上。方法名叫“刷新”但干的是整个Spring容器的初始化——创建BeanFactory、加载BeanDefinition、执行后处理器、实例化所有单例Bean、发布事件一条龙全在这里。我见过不少同事包括我自己最开始读Spring源码的时候都是从这个方法切入然后在各种doXXX和registerXXX之间迷路一上午过去脑袋里只剩一堆方法名。这篇文章就把我自己反复摸爬滚打出来的阅读技巧整理一遍目标是让刚开始读源码的人也能顺着refresh()这条线把Spring容器启动过程真正走通。1. 为什么所有Spring面试题最终都会落在refresh()上1.1 refresh()是IoC容器的“总导演”先看一个最简单的启动方式public static void main(String[] args) { AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class); }点进AnnotationConfigApplicationContext这个构造器你会发现它做了两件事先调用无参构造器初始化AnnotatedBeanDefinitionReader和ClassPathBeanDefinitionScanner然后调用register(componentClasses)把配置类注册成BeanDefinition最后一行就是refresh()。如果是老项目用的ClassPathXmlApplicationContext构造器最后也是refresh()。到了Spring BootSpringApplication.run()内部创建好ApplicationContext之后进入的仍然是AbstractApplicationContext.refresh()只是中间多了一层refreshContext()。也就是说不管你是注解驱动、XML驱动还是Spring Boot最终启动入口都是这一个方法。所以读refresh()不只是读一个方法而是拿到了整个IoC容器的执行地图。你以后看到任何关于“Bean什么时候创建”“配置类什么时候解析”“AOP切面什么时候生效”的疑问都可以在这张地图上定位到具体坐标。1.2 阅读能力的三层分水岭我自己把读refresh()的水平分成三层第一层能背出方法里12个步骤的名字知道每步大概干嘛。第二层能说清楚invokeBeanFactoryPostProcessors和finishBeanFactoryInitialization这两个核心步骤内部的关键分支尤其是后处理器的执行顺序。第三层能结合实际问题定位源码比如循环依赖为什么三级缓存能解为什么Autowired能识别ApplicationContextAware这类接口。这篇文章的定位是帮你从第一层扎实走到第二层并且给第三层铺好路。我不打算把每行代码都抄出来讲而是讲“怎么读”比“读到了什么”更重要的那些方法。2. 读源码前的三件套环境、断点、目标2.1 源码怎么拉、怎么导入才不劝退很多人读Spring源码第一步就卡住了用Maven下载的sources.jar虽然能看代码但注释残缺跳转体验也差方法多了以后根本理不清。我的建议是直接拉GitHub上的源码工程自己导入IDEA。git clone https://github.com/spring-projects/spring-framework.git cd spring-framework git checkout v5.3.39注意spring-framework由Gradle管理不是Maven。IDEA导入时选择Gradle项目等待依赖下载完就行。JDK版本建议8或11太新的JDK反而可能在编译工具链上出幺蛾子。如果你平时用的是Spring Boot 2.7.x对应Spring Framework 5.3.x如果用Boot 3.x那Framework是6.x。我建议第一次读选5.3.x网上讨论最多、资料最全坑也都被前人踩得差不多了。提示源码工程里有很多模块你只需要关注spring-context、spring-beans这两个模块的源码其他如spring-web、spring-jdbc暂时不用管。IDEA里对着AbstractApplicationContext直接跳转也可以。2.2 第一个断点应该打在哪环境准备好以后不要急着到处点。建一个最普通的Java工程引入你本地源码模块然后写一个最小启动类Configuration ComponentScan(com.demo) public class AppConfig { }public class DemoApplication { public static void main(String[] args) { AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class); context.close(); } }在AbstractApplicationContext.refresh()方法的第一行打个断点然后以Debug模式运行。停住之后看一下IDEA左下角的调用栈AnnotationConfigApplicationContext的构造器在调用refresh()而底层到AbstractApplicationContext。这个栈帧就是你的“锚点”接下来所有追踪都从这个锚点出发。从断点开始我建议你开启“Step Over”模式一步一步看refresh()内部方法的调用顺序先别用“Step Into”钻进去。这一步的目标是建立“顺序感”。等你把12个步骤的名字都混了个脸熟再决定往哪个方法里钻。2.3 每次阅读只带一个目标读源码最忌讳的就是想一口气全看懂。我的习惯是每次打开refresh()之前先给自己定一个小目标第一次只看主流程12个步骤分别干什么。第二次只跟finishBeanFactoryInitialization这个单例创建链路。第三次只关注invokeBeanFactoryPostProcessors的后处理器调度顺序。一次一题读不懂就停改天再来。带着目标读比漫无目的扫代码高效得多。3. refresh()十二步先背框架再逐个击破3.1 十二步对照表先把refresh()的主体结构砸出来。Spring 5.3.x里AbstractApplicationContext.refresh()大致的流程如下我配上了一句话作用说明步骤方法名一句话作用1prepareRefresh()记录启动时间、设置容器状态、初始化属性源、收集早期监听器2obtainFreshBeanFactory()创建/刷新BeanFactory并加载BeanDefinition如XML场景3prepareBeanFactory(beanFactory)为BeanFactory配置类加载器、SpEL解析器、Aware回调等4postProcessBeanFactory(beanFactory)模板方法留给子类扩展比如Web容器相关5invokeBeanFactoryPostProcessors(beanFactory)执行BeanFactoryPostProcessor和BeanDefinitionRegistryPostProcessor6registerBeanPostProcessors(beanFactory)注册所有BeanPostProcessor但此时不执行7initMessageSource()初始化国际化消息源8initApplicationEventMulticaster()初始化事件多播器9onRefresh()模板方法子类扩展如Web服务器启动10registerListeners()把监听器Bean注册到多播器并广播早期事件11finishBeanFactoryInitialization(beanFactory)实例化所有非懒加载单例Bean核心步骤12finishRefresh()初始化生命周期处理器发布ContextRefreshedEvent后面还有catch (BeansException ex)和finally { resetCommonCaches(); }。异常分支干的是销毁已经创建的Bean避免容器处于半初始化状态finally清理的是反射缓存、ResolvableType缓存这些元数据。这张表值得先记下来。不用死记读两遍源码再看这张表自然就记住了。3.2 前三步准备、拿工厂、做“出厂设置”第1步prepareRefresh()相对简单核心是几点设置容器启动时间、把closed和active标志位翻转、初始化PropertySources占位符、准备一组earlyApplicationListeners。它有一个模板方法initPropertySources()默认空实现留给子类填充环境变量。阅读时你只需要知道“这一步是热身”代码本身不值得逐行盯。第2步obtainFreshBeanFactory()值得重点看一眼因为它是“BeanFactory从无到有”的关键。AbstractApplicationContext里这个方法很短是个模板方法骨架具体实现取决于子类GenericApplicationContext的refreshBeanFactory()里会new一个DefaultListableBeanFactory然后调用loadBeanDefinitions(beanFactory)。AbstractRefreshableApplicationContext还会设置allowBeanDefinitionOverriding、allowCircularReferences这些标志位。这里有一个很反直觉的点refresh()方法本身并没有“创建BeanFactory”的逻辑真正创建是子类干的。所以读refresh()时很多代码其实是“多态调用”阅读时别把obtainFreshBeanFactory()当成一个简单getter它是模板方法。第3步prepareBeanFactory(beanFactory)是做“出厂设置”的。它给BeanFactory设置了类加载器、SpEL表达式解析器、占位符解析器然后注册了一堆“手动指定依赖”的组件beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory); beanFactory.registerResolvableDependency(ResourceLoader.class, this); beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this); beanFactory.registerResolvableDependency(ApplicationContext.class, this);同时还会ignoreDependencyInterface(EnvironmentAware.class)、ignoreDependencyInterface(ApplicationEventPublisherAware.class)等等。这个ignore的意思是当Spring准备给Bean做属性注入时遇到这些Aware接口不要走自动装配而是由容器在之后用回调方式显式设置。这就是为什么你的ApplicationContextAware接口能拿到容器引用而不是靠Autowired塞进来。阅读技巧第3步不需要把每一行注册都背下来只需要明白它是在“调教”BeanFactory让它具备Spring容器该有的各种基础设施能力。3.3 第5、6步两个“后处理器”的分水岭invokeBeanFactoryPostProcessors和registerBeanPostProcessors是12步里最难啃的两块。它们名字很像很多人读完就混了。我提供一个区分角度invokeBeanFactoryPostProcessors立刻执行。这些后处理器专门用来修改BeanDefinition比如ConfigurationClassPostProcessor就是在这里解析Configuration、ComponentScan、Bean的因此BeanDefinition真正的内容大多在这时候才补全。registerBeanPostProcessors只注册、不执行。它把所有BeanPostProcessor实例放进BeanFactory的一个list里等后面createBean时才会被逐个调用用来干预Bean实例化过程。记住一句话一个管“定义”一个管“实例”。这两个步骤的具体阅读方法我放在第4章专门展开这里先把位置和分工搞清楚。3.4 第7到10步消息、事件和Web服务器第7步initMessageSource()看一眼就行。它先从BeanFactory里找有没有用户自定义的MessageSource没有就用DelegatingMessageSource兜底。第8步initApplicationEventMulticaster()同理。优先用用户自定义的ApplicationEventMulticaster没有就new一个SimpleApplicationEventMulticaster。注意这里有个细节多播器会尝试从BeanFactory里拿TaskExecutor如果有就把异步执行器塞进去这样事件就能异步发布。自己业务里想要异步事件回头可以往这个方向配。第9步onRefresh()是模板方法Web场景下子类会在这里启动内嵌Tomcat/Netty。纯注解容器里它是空实现。第10步registerListeners()把容器里实现了ApplicationListener接口的Bean注册到多播器。它还会把prepareRefresh()阶段收集的早期事件earlyApplicationEvents先缓存起来等后续finishRefresh()发布ContextRefreshedEvent再一并分发。阅读技巧这四步不是核心矛盾快速扫过即可。如果你本来就不关心事件机制可以直接跳到第11步后面前后文也不会有太大割裂感。3.5 第11、12步真正的主菜和收尾甜点第11步finishBeanFactoryInitialization(beanFactory)是全文最重要的地方。它做两件事给BeanFactory设置类型转换器ConversionService和字符串解析器EmbeddedValueResolver。冻结Bean定义freezeConfiguration()然后调用preInstantiateSingletons()实例化所有非懒加载的单例Bean。preInstantiateSingletons()会遍历所有BeanDefinition跳过抽象类、跳过懒加载和原型Bean然后对每个单例Bean调用getBean(beanName)。也就是说你业务里大部分Service、Component、Bean单例对象都是在这一步被真正new出来的。第12步finishRefresh()是收尾清理JVM一级缓存中的资源类、初始化LifecycleProcessor、调用onRefresh()这一步是web容器启动逻辑的位置、发布ContextRefreshedEvent。提示事件发布的顺序很有讲究。必须在所有单例Bean都创建完才发ContextRefreshedEvent否则监听器回调时想依赖其他Bean可能还没创建好。4. 最值得死磕的三个子流程别人不会告诉你的那些点4.1 invokeBeanFactoryPostProcessors先看调度顺序再看内部实现这是refresh()里最劝退的一个方法因为点进去以后你会看到大量循环和递归。我第一次读时一路Step Into直接进了ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry然后又跳进一个内部类的几百行代码里人懵了。后来我换了策略第一遍只看“入口调度顺序”绝不深入任何具体PostProcessor的实现。入口调度顺序大致是这样的先收集所有BeanDefinitionRegistryPostProcessor类型的Bean。区分PriorityOrdered、Ordered、普通无序三类按顺序依次执行它们的postProcessBeanDefinitionRegistry()方法。执行完后还会再次从BeanFactory里扫描因为前一轮处理可能注册了新的BeanDefinitionRegistryPostProcessor若有则继续循环执行。全部BeanDefinitionRegistryPostProcessor执行完毕最后才统一执行普通BeanFactoryPostProcessor的postProcessBeanFactory()。这里为什么容易晕因为postProcessBeanDefinitionRegistry()执行过程中往往又会产生新的后处理器Spring需要反复重新扫描所以代码里才有while循环和repeat标志。你如果跟着某一轮递归进去看细节就很容易丢掉“当前处于第几轮”这个上下文。阅读技巧就是先画一张“调度顺序图”把PriorityOrdered → Ordered → 普通这个套路记下来然后随便找一个具体的PostProcessor实现去验证比如ConfigurationClassPostProcessor。等你验证完一个这条“调度顺序”就算彻底吃透了。4.2 registerBeanPostProcessors顺序就是生命这个方法比上一个温和但同样有排序逻辑。它的执行顺序是注册实现PriorityOrdered接口的BeanPostProcessor。注册实现Ordered接口的。注册普通无序的。注册MergedBeanDefinitionPostProcessor用于处理Value、Autowired、PostConstruct等注解的元数据合并。最后注册一个ApplicationListenerDetector用来把实现了ApplicationListener接口的Bean自动登记成监听器。你可能发现规律了Spring在处理多个同类型组件时特别喜欢用“PriorityOrdered → Ordered → 无排序”的套路。这个套路在invokeBeanFactoryPostProcessors里出现过在registerBeanPostProcessors里又出现后面你读MBeanExporter之类组件时还会见到。阅读时我建议你找几个具体的后处理器看看它们的order值AutowiredAnnotationBeanPostProcessor负责Autowired和Value注入排序优先级很高。CommonAnnotationBeanPostProcessor负责Resource、PostConstruct、PreDestroy。ApplicationListenerDetector排在最后。为什么顺序重要因为BeanPostProcessor在Bean创建过程中是链式调用的前面的后处理器先把Bean加工一波后面才能继续加工。顺序错了注入时机就乱了。你只要理清“谁先谁后”很多Bean初始化诡异问题都能定位到“某个后处理器没生效”上面。4.3 finishBeanFactoryInitialization三级缓存到底在哪一刻生效死磕完两个后处理器主菜就是finishBeanFactoryInitialization。这个方法的链路是preInstantiateSingletons() - getBean(beanName) - doGetBean(beanName) - getSingleton(beanName) // 查三级缓存 - singletonFactory.getObject() // 缓存都没有才走createBean - createBean(beanName) - doCreateBean(beanName)很多人读到这里又迷糊了因为doGetBean非常长。我的建议是这条链路里你先只盯DefaultSingletonBeanRegistry这个类把三级缓存的读写时机搞清楚再往doCreateBean里钻。三个缓存的字段定义在DefaultSingletonBeanRegistryprivate final MapString, Object singletonObjects new ConcurrentHashMap(256); private final MapString, ObjectFactory? singletonFactories new HashMap(16); private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16);singletonObjects一级缓存存放已经完整创建好的单例Bean。earlySingletonObjects二级缓存存放“提前暴露”的半成品Bean这些Bean已经实例化但尚未完成属性填充/初始化。singletonFactories三级缓存存放的是ObjectFactory调用它的getObject()可以拿到早期引用。阅读时在getSingleton(String beanName, boolean allowEarlyReference)上打条件断点条件可以写beanName.equals(userService)。你会看到以下场景第一次getSingleton调用三级缓存全都没命中返回null。进入doCreateBean实例化对象后会调用addSingletonFactory(beanName, () - getEarlyBeanReference(...))这个lambda被放进三级缓存。此时Bean是个“半成品”。populateBean填充属性时如果发现要注入的对象还没创建完就会触发其他Bean的创建形成循环。循环发生时其他Bean能通过三级缓存的ObjectFactory拿到这个半成品的早期引用于是塞进二级缓存。最终当前Bean完成初始化调用addSingleton(beanName, singletonObject)放进一级缓存同时清掉二三级缓存。这个机制困扰我很久的是“为什么需要三级缓存而不是直接二级”。关键就在三级缓存存的是ObjectFactory不是实例本身。由于ObjectFactory.getObject()可以返回经过AOP代理包装后的早期对象Spring才能保证循环依赖发生时注入的是代理而非裸对象。你顺着这个思路去读getEarlyBeanReference和AbstractAutoProxyCreator的getEarlyBeanReference重写就能把AOP预演和循环依赖串起来。阅读技巧总结在doCreateBean里只关注三处——实例化后addSingletonFactory之前的代码、populateBean入口、addSingleton之前的状态。其他地方比如各种applyMergedBeanDefinitionPostProcessors这轮可以先跳过。5. 踩坑实录读源码时最容易掉的三个坑5.1 钻进递归黑洞忘了自己在哪invokeBeanFactoryPostProcessors内部实现invokeBeanDefinitionRegistryPostProcessors时会循环调用还会嵌套处理dependsOn。很多人在这一步点着点着就彻底忘了自己是从哪进来的。我的对策很简单每次进入一个方法前先问自己三句话——谁在调用我我处于第几轮循环处理的是哪个BeanIDEA的Debugger里线程栈的“栈顶”通常是你正在执行的方法“栈底”是线程的起点。你需要关注的是从refresh()到你当前位置之间的这几帧不要管栈更深处Spring内部的一些机械操作。如果发现当前帧离refresh()已经隔了七八层大概率你钻得太深了先回到refresh()这一帧重新按Step Over走一遍。5.2 只读正常分支忽略异常分支读源码有个天然的坏习惯跟着快乐路径走看到if (xxx) return就跳过。但doGetBean里大量逻辑恰恰藏在条件分支里。比如if (mbd.isSingleton()) { sharedInstance getSingleton(beanName, () - { return createBean(beanName, mbd, args); }); }如果之前缓存里有这个方法直接就返回了。可“没有缓存”的情况才更关键。我建议你阅读时看到if先想想“如果条件不满足下一行是什么”强迫自己至少把分支的另一侧扫一眼。比如allowCircularReferences为false时循环依赖会直接抛BeanCurrentlyInCreationException这个分支你不看后面排查“循环依赖为什么不生效”就会一头雾水。5.3 版本漂移网上文章对不上号Spring Framework迭代很快。5.3.x和6.x之间不仅包名从javax.换成jakarta.内部代码结构也有变化。比如6.x里删掉了一些过时的*Aware接口注册逻辑DefaultSingletonBeanRegistry的三级缓存实现虽然大体保留但细节已经调整。如果你照着网上2020年的文章去读6.x源码很可能找不到对应类名或方法位置。记住一件事读源码前先确认版本最好以你项目实际依赖的Spring版本为准。你项目用的是Boot 2.7就切到Framework 5.3.x项目用Boot 3.x就切到6.x。旧文章当思路参考不要当代码字典。6. 读完之后refresh()给你留下什么把refresh()读完以后你再回头看那些面试题会有一种“题都变透明了”的感觉控制反转发生在哪obtainFreshBeanFactory创建工厂invokeBeanFactoryPostProcessors解析BeanDefinitionfinishBeanFactoryInitialization实例化Bean——整个过程都是容器在替我们管理对象。三级缓存什么时候写入不是一开始写而是对象实例化后、属性填充前通过addSingletonFactory写入。Autowired什么时候生效在createBean的populateBean阶段由AutowiredAnnotationBeanPostProcessor等后处理器完成。手写Spring应该参考什么无非是一个BeanDefinition注册表、一个单例池、一个后处理器列表、一个事件多播器然后按refresh()的顺序把它们串起来。如果看完脑子里还是乱我建议你分三次再读一遍第一次只跟主流程把12个步骤名字背下来 第二次跟finishBeanFactoryInitialization重点看getSingleton和addSingleton 第三次跟invokeBeanFactoryPostProcessors只看调度顺序不看内部实现。每次阅读给自己设定一个明确的小问题比如“BeanFactory是哪里new出来的”“BeanPostProcessor是在哪一步被注册的”“循环依赖的早期引用从哪里拿到的”。读完之后合上源码用自己的话把流程讲一遍卡住的点就是下次要回去重新看的地方。我自己到现在每次遇到容器启动相关的诡异问题仍然会先回到refresh()这张地图上定位坐标而不是漫无目的地全局搜索。这个方法的可靠性远比记住几行源码高得多。希望这篇阅读技巧也能让你下次打开Spring源码时不再发怵。
返回列表