很多人在准备Spring面试时,都把IOC当成一道概念题来复习:控制反转、依赖注入、容器管理对象生命周期,背得滚瓜烂熟。但面试官只要顺着“Bean是什么时候被创建出来的”“循环依赖到底怎么解掉的”“三级缓存里的那个ObjectFactory到底什么时候才真正执行lambda表达式”多问几步,大多数人就卡住了。这不是知识储备不够,而是把IOC理解成了一个名词,而不是一段可以逐行阅读的代码。
这篇内容我会结合自己反复读Spring源码的笔记,把你真正需要理解的IOC内部细节串起来。不讲“什么是控制反转”这种教科书定义,而是从BeanDefinition的加载、BeanFactory的启动、依赖注入的时机、循环依赖的三级缓存,一直聊到面试现场那些看起来简单、实际上坑很多的追问。适合正在准备Java后端面试的开发者,也适合那些已经工作一两年、想真正把Spring用明白的工程师。
1. 先把问题问清楚:IOC面试到底在考什么
1.1 从一道连环追问说起:问的不是答案,是源码阅读习惯
之前有个读者跟我说,他去某公司面试,面试官上来就问:“Spring的IOC容器是怎么把Bean创建出来的?”他条件反射地开始背“BeanFactory管理Bean的生命周期,ApplicationContext是BeanFactory的子接口,调用getBean的时候完成实例化”这类话。面试官点点头,接着问:“那getBean第一次被触发,是在什么时候?”
这个问题一下子就把他问住了。因为大多数人对IOC的理解停留在接口关系图上,从没想过整个容器启动过程中,哪个代码文件、哪个方法、哪一行真正调用了getBean。
面试官后面又追了几个问题:单例Bean是在容器启动时就new出来,还是等你第一次使用才new?如果某个Bean是懒加载的,那它的依赖还会被自动注入吗?@Autowired到底是按类型还是按名字找Bean?它找候选者的过程,会不会把@Qualifier的限定条件也考虑进去?
这些连环追问背后,面试官真正想确认的只有一件事:你有没有真的打开过Spring源码,从头到尾跟着容器启动流程走一遍。概念可以背,调用链是背不出来的。
1.2 从背答案到源码级理解的四层台阶
我把IOC的理解水平分成四层,你可以对照一下自己现在在哪一层。
第一层是“会使用”。能写@Component、@Autowired,知道把对象交给Spring管理能省去手动new的麻烦。这一层只能应付日常工作。
第二层是“懂原理”。知道IOC容器本质是一个Map,BeanDefinition是创建Bean的说明书,AOP是通过动态代理实现的。到了这一步,已经能解释很多框架层面的行为,但遇到“为什么循环依赖有时报错有时能解掉”这种问题,还是会懵。
第三层是“读过源码”。能说出refresh()里大概有几个关键方法。BeanFactory的预实例化在finishBeanFactoryInitialization里触发。doCreateBean分几步。这一层的候选人已经能跟面试官聊到具体类名、方法名,会给面试官留下“这个人是真的钻研过”的印象。
第四层是“能落地”。不仅读过,还能说清楚“为什么是这样设计”“换一种设计会有什么问题”。比如能解释为什么三级缓存要设计成三个Map而不是一个Map,为什么构造器注入解决不了循环依赖,为什么prototype作用域的Bean不用提前实例化。这一层已经是源码级面试的理想状态。
这篇文章的目的,就是帮你从第一、二层尽可能扎实地走到第三、四层。
2. BeanDefinition与容器启动:IOC的起点全在“定义”里
2.1 BeanDefinition到底存了什么,为什么它是IOC的地基
很多人一想到IOC容器,第一反应是“一个装对象的Map”。其实Spring内部真正管理的第一手资料不是对象,而是BeanDefinition。你可以把它理解为一张“制造的图纸”:对象是零件,BeanDefinition是图纸,BeanFactory是车间。
图纸上会记录哪些信息?看BeanDefinition的关键属性就能明白:beanClassName、scope、lazyInit、dependsOn、initMethodName、destroyMethodName、propertyValues、constructorArgumentValues、autowireMode、primary。就是说,这个Bean应该被创建成单例还是原型,是不是懒加载,依赖哪些其他Bean,初始化之前调哪个方法,销毁时调哪个方法,属性用什么值填充,构造器参数怎么给,有没有标记为优先候选者,全都记在这张图纸上。
这里面有几个容易忽略的点。propertyValues是PropertyValues类型,它是一个持有PropertyValue的集合。每个PropertyValue本质上就是“属性名-属性值”的键值对,但属性值可以是TypedStringValue(字符串值)、RuntimeBeanReference(引用另一个Bean)、BeanDefinitionHolder(内部Bean定义)。当你写 的时候,XML解析器并不是直接把dataSourceBean这个名字存进去,而是封装成一个RuntimeBeanReference,等到注入阶段再解析这个名字对应的实际Bean。
另外constructorArgumentValues也很关键,构造器注入的整个实现都依赖它。Spring在解析构造器参数时,会保留参数的index、type、name、value信息。如果你不指定这些信息,Spring就只能靠类型匹配和参数个数去猜,这也是为什么构造器注入容易踩坑的地方——比如参数数量相同但顺序不同时,猜错就会报错,或者注入错的对象。
所以你看,整个IOC的起点并不是某个对象被new出来,而是BeanDefinition被完整地解析、注册到容器里。没有这张图纸,后面的一切都无从谈起。
2.2 refresh()这条主线,决定容器是否真正“准备好了”
容器启动以后,真正的入场动作发生在AbstractApplicationContext.refresh()。这是整个IOC体系里最核心的一个方法,我把它的关键步骤用两段话讲明白。
refresh()干了这些事:先调用prepareRefresh()做一些准备工作,比如设置启动时间和活跃状态,初始化一些占位符。然后是obtainFreshBeanFactory(),这一步会把BeanDefinition加载进来,并创建出BeanFactory。加载XML配置的XmlBeanDefinitionReader在这里工作,它把XML里的 标签一行行解析成BeanDefinition,注册到DefaultListableBeanFactory的beanDefinitionMap里。beanDefinitionMap就是一个ConcurrentHashMap,key是beanName,value是BeanDefinition。
紧接着是prepareBeanFactory(),这一步会给容器配置一些基础设施,比如类加载器、SpEL表达式解析器、属性编辑器注册器。还有一个容易被忽略的点:这一步会注册几个特殊的Bean后置处理器,比如ApplicationContextAwareProcessor,它负责把ApplicationContext注入给实现了ApplicationContextAware接口的Bean。如果你在代码里让它实现了ApplicationContextAware,那setApplicationContext方法就是由它触发的。
之后是postProcessBeanFactory()和invokeBeanFactoryPostProcessors()。前者是留给子类扩展的钩子,后者会调用所有BeanFactoryPostProcessor,对BeanDefinition做修改。比如常见的PropertySourcesPlaceholderConfigurer就是在这里把${db.url}这种占位符替换成真实配置值。要注意它操作的是BeanDefinition,不是Bean实例,所以这个阶段即使Bean还没被创建,修改照样生效。
关键节点在registerBeanPostProcessors(),这里会注册一堆BeanPostProcessor,它们后面会在Bean实例化和初始化前后插手。到了finishBeanFactoryInitialization(),默认的单例非懒加载Bean就在这里被实例化。最后一步finishRefresh()会发布ContextRefreshedEvent事件,容器才算是真正准备好。
2.3 预实例化的顺序问题:为什么默认单例会提前被new出来
面试里经常有人问:“Spring默认创建的Bean是什么时候实例化的?”标准答案是:容器启动完成前,单例且非懒加载的Bean会被预实例化。但很多人只记住了这句话,没理解为什么。
做设计的思路其实很简单:如果这些Bean不在启动阶段准备好,等第一次getBean的时候再创建,那第一个请求进来就要承担大量创建对象的开销,这体验很差。提前创建还有一个好处,能尽早发现配置错误,比如依赖不存在、类型不匹配,启动就报错总比线上运行才炸要好得多。
有个细节值得单独拿出来说:预实例化是在finishBeanFactoryInitialization()方法里完成的。它调用的是preInstantiateSingletons(),从beanDefinitionMap里遍历所有BeanDefinition,逐个判断是不是单例、是不是懒加载。如果是正常的单例Bean,就直接调用getBean(beanName)。所以你会发现,即使你没主动调用过getBean,容器启动后所有非懒加载的单例也都已经创建完成并放进了单例缓存池。
懒加载Bean就不一样了。lazy-init为true的Bean,在启动阶段只会注册BeanDefinition,不会被提前实例化,直到第一次getBean才创建。需要注意的是,懒加载只影响实例化时机,不影响依赖注入顺序。如果懒加载Bean在预实例化阶段就已经被其他非懒加载Bean依赖,那它照样会被提前创建,懒加载就形同虚设了。
3. 依赖注入的源码细节:别只盯着@Autowired
3.1 Bean的完整诞生流程:从createBean到populateBean
依赖注入不是一个单独的动作,它是Bean创建流程中的一个环节。整个创建流程在AbstractAutowireCapableBeanFactory.doCreateBean()里,我把关键步骤拆开讲。
第一步是createBeanInstance(),选择实例化策略。构造器注入在这里通过构造器推断来完成。Spring会扫描BeanDefinition里的constructorArgumentValues,再结合候选构造器,判断参数个数、类型、是否匹配。这里有个很容易踩的坑:如果类有多个构造器,而你没有用@Autowired(required=false)或者没有显式指定参数,Spring的构造器推断可能会选中一个不是你想要的构造器。
第二步是允许MergedBeanDefinitionPostProcessor修改BeanDefinition,比如@Autowired、@Value这些注解的解析信息,就是在这个阶段被CommonAnnotationBeanPostProcessor和AutowiredAnnotationBeanPostProcessor收集出来的。它们会找出类里所有需要注入的字段、方法、构造器,封装成InjectionMetadata。
第三步是提前暴露对象引用,对应解决循环依赖的手段。这一步只在“允许循环依赖且当前Bean是单例”的情况下才生效。具体做法是:如果这个Bean正在创建中,Spring会把一个ObjectFactory放进三级缓存singletonFactories里。这个工厂什么时候会真正调用?后面循环依赖部分我会细说。
第四步是populateBean(),它才是真正执行属性填充的地方。它的任务是把@Autowired注解标记的字段注入进去,或者把XML配置里的property值set进去。执行逻辑是通过后置处理器挨个查找需要注入的字段和方法,逐个反射调用。
第五步是initializeBean(),执行初始化逻辑。先调用Aware接口的回调方法,比如BeanNameAware、BeanFactoryAware、ApplicationContextAware。然后调用BeanPostProcessor的postProcessBeforeInitialization。然后是init-method或者@PostConstruct。最后是postProcessAfterInitialization。AOP动态代理通常就在postProcessAfterInitialization这一步里生成。
3.2 字段注入、setter注入、构造器注入在源码里的不同路径
面试里关于注入方式的讨论几乎是必考的。用@Autowired直接打在字段上,和打在setter方法上,以及通过构造器传参,源码层面的区别很有意思。
字段注入走的是AutowiredFieldElement的inject方法。它的处理方式很直接:拿到Field对象,通过findAutowireCandidates找到候选的Bean,然后Field.set(obj, bean)直接塞进去。这个方式没有中间机会做额外处理,而且可测试性比较差,因为字段是私有的,单元测试的时候不启动Spring容器,就只能靠反射去赋值。
setter注入会走AutowiredMethodElement的inject方法。Spring会从BeanDefinition里找到setter方法对应的属性描述符,然后反射调用Method.invoke(bean, arg)。这比字段注入多了一个方法调用的过程,所以如果你在setter方法里写了自己的一些逻辑,比如初始化一个成员变量,这些逻辑会被执行。
构造器注入走的路径又不一样,发生在createBeanInstance()阶段,根本不会进到populateBean()。所以构造器注入最大的特点就是“创建即完整”——对象new出来的时候,所有依赖都已经就位。这带来的好处是Bean状态完整、不可变,适合强依赖的场景。坏处是可能出现循环依赖无法解决的问题,这个我后面单独展开。
如果你问面试中该回答什么倾向,我的经验是:优先构造器注入,强依赖走构造器,可选依赖用setter。虽然Spring官方文档也是这么倾向的,但能讲出“构造器注入的依赖必须在实例化阶段就准备好,所以它天然不适合循环依赖场景”这个源码细节的人,其实并不算多。
3.3 类型匹配背后的候选者筛选:没你想的那么简单
@Autowired默认按类型注入。可Spring的容器里可能有多个相同类型的Bean,那它到底选哪个?这时候就轮到候选者筛选逻辑出场了。
代码在DefaultListableBeanFactory.findAutowireCandidates()里。第一步先从BeanFactory里找到所有类型匹配的BeanName。第二步处理泛型匹配,比如你注入的是List ,它会把这个泛型参数也当作过滤条件。第三步检查每个候选Bean是否可注入,还要考虑@Qualifier、@Primary这些注解的限定条件。如果最终候选者超过一个,并且没有明确的优先项,就抛NoUniqueBeanDefinitionException。
这里有个细节值得记住:@Autowired在找不到候选者时,如果required=true,会抛异常;required=false时,则跳过注入,让字段保持默认值。但如果存在多个候选者,哪怕required=false,冲突依然会抛异常,这一点容易踩坑。
@Resource的处理方式就很不一样,它来自JSR-250规范。CommonAnnotationBeanPostProcessor处理@Resource时,如果注解里指定了name,就按名字精确找;没指定name,就以字段名或setter方法名作为beanName去找;找不到名字匹配的,再退化成按类型查找。所以@Resource本质上“优先按名字,名字找不到再按类型”。
4. 那些面试官常挂嘴边但很少讲透的细节
4.1 三级缓存解决循环依赖:源码逐段拆解
循环依赖是IOC面试的高频重点,几乎人手一份“三级缓存”口诀:singletonObjects、earlySingletonObjects、singletonFactories。但很多人背完口诀,其实不太清楚每个Map里到底放了什么,什么时候放,什么时候取。
我先用大白话过一遍三个缓存的角色。singletonObjects存的是“成品Bean”。earlySingletonObjects存的是“半成品Bean”,也就是实例化完成但属性还没填充完的对象。singletonFactories存的不是Bean,而是ObjectFactory——一个能临时生成早期对象引用的工厂。
现在还原循环依赖的场景。假设A依赖B,B也依赖A。创建A时,A实例化完成后,还没填充属性,Spring就先把它包装成一个ObjectFactory放进singletonFactories,这样其他Bean就能在A完全创建好之前拿到它的早期引用。接着A开始populateBean,发现要注入B,于是getBean(B),进入创建B的流程。
B实例化完成后,同样会把ObjectFactory放进singletonFactories,然后开始populateBean,发现要注入A,于是getBean(A)。注意,这时候getBean的大部分查找逻辑会先命中三级缓存。DefaultSingletonBeanRegistry.getSingleton()里的逻辑是:先看singletonObjects里有没有;没有再看earlySingletonObjects里有没有;还没有,就取singletonFactories里对应的ObjectFactory,调用它的getObject()方法。这个调用一般会返回A的早期引用,然后Spring把这个早期引用放进earlySingletonObjects,同时移除singletonFactories里的记录。这样B就拿到了A的引用,注入完成。B创建完成后,Spring会把B放入singletonObjects,A在populateBean里也成功拿到B,最终A继续走完初始化流程进入singletonObjects。
这里有一个面试必问的细节:singletonFactories里的ObjectFactory.getObject(),正常情况下返回的就是普通早期引用,就是简单的bean引用本身。但如果这个Bean被AOP代理过,那问题就复杂了。比如循环依赖中的A需要被代理,如果直接返回原实例,B拿到的只是原始对象,不是代理对象,那前面做的AOP增强就全都失效了。
Spring解决这个问题的思路是:在singletonFactories里存入的ObjectFactory中,getObject()会调用一个关键方法getEarlyBeanReference(),这个方法的实现可以让SmartInstantiationAwareBeanPostProcessor有机会介入,提前创建AOP代理对象。所以循环依赖中如果A需要代理,那么B拿到的那个早期引用,其实已经是代理对象了。
4.2 为什么构造器注入和prototype救不了循环依赖
三级缓存能解决循环依赖,但有两个典型场景是解决不了的。面试里经常把这两个场景拿来考验人。
场景一是构造器注入循环依赖。Apollo依赖构造器注入B,B又构造器注入A。创建A先要拿到B的构造器参数,于是getBean(B);创建B又要先拿到A的构造器参数,于是getBean(A)。可问题在于,A还在实例化阶段,实例都还没new出来,更别提把它放进三级缓存了。三级缓存里的ObjectFactory是在实例化完成之后、属性填充之前才放进去的。构造器注入在实例化阶段就卡住了,所以Spring检测到这种情况会直接抛BeanCurrentlyInCreationException。
场景二是prototypeBean之间的循环依赖。三级缓存仅在单例Bean的创建链路里起作用。靠getBean每次都会创建新实例,Spring不会缓存prototype的早期引用,也不会缓存成品。所以A依赖B、B又依赖A,而且两个都是prototype的话,容器不去处理,直接暴露循环依赖的异常。这也从侧面说明了,Spring默认的“单例”这个设计,不只是性能考虑,某些方面还给框架处理复杂依赖关系提供了空间。
4.3 BeanFactory与ApplicationContext,谁在偷偷干活
这个问题的答案是:真正干活的是BeanFactory,ApplicationContext是在它之上加了一堆“便利功能”。
DefaultListableBeanFactory算是最完整的BeanFactory实现,beanDefinitionMap、单例池、依赖查找都在这套体系里。而ApplicationContext本身并不会自己实现getBean,它的实现是委托给内部的BeanFactory。所以你调用applicationContext.getBean("xxx"),本质上是applicationContext内部的beanFactory在干活。
那ApplicationContext多干了什么?它扩展了三个方向。第一是面向企业级应用的功能集成,比如国际化MessageSource、事件发布ApplicationEventPublisher、资源加载ResourceLoader。第二是自动注册一些基础设施Bean,比如BeanFactoryPostProcessor和BeanPostProcessor会被自动检测并注册,而如果你直接使用BeanFactory,手动注册会很繁琐。第三是环境抽象和占位符处理,Spring Boot里那么多application.properties配置,本质上都是这些扩展能力在支撑。
面试中如果被问到“BeanFactory和ApplicationContext的区别”,不要只回答“ApplicationContext是加强版”。最好能展开说:ApplicationContext会自动完成BeanFactoryPostProcessor的注册与调用,自动完成BeanPostProcessor的注册,默认就会预实例化单例Bean;而如果只使用BeanFactory,需要自己调用postProcessBeanFactory这种逻辑。把这些细节讲出来,面试官就知道你是真的读过源码。
4.4 @Configuration的full模式和lite模式:代理问题避坑
这个细节很多人没注意到。Spring对@Configuration注解的类分两种模式:full模式和lite模式。判断标准很简单:如果这个类上标注了@Configuration,那么是full模式,CGLIB会为它生成代理子类;如果只标注了@Component、@Service这种注解,或者只是普通类里用@Bean方法,那就是lite模式。
这两种模式最大的区别体现在:full模式下,你可以在一个@Bean方法里直接调用另一个@Bean方法,Spring会拦截这个调用,返回的是容器里的单例Bean,而不是新建一个对象。lite模式下则没有这种拦截,你调用的就是一个普通的方法,执行一次就创建一次新对象,哪怕目标Bean本身是单例,这里也可能出现同一个类有多个实例的情况。
面试里有个经典坑题:在@Configuration类里定义了一个@Bean方法,又在该类中注入同一个类型的依赖,问有没有问题。如果你懂full模式的代理机制,就能解释清楚:@Configuration是full模式,Spring通过CGLIB代理拦截了@Bean方法的调用,所以返回的都是容器管理的单例。但如果这个类是lite模式,调用@Bean方法就有风险了,可能一个类会有两份实例。
这个点面试官也喜欢用来考察候选人是否真的深入阅读过配置类解析的源码。重点在于ConfigurationClassPostProcessor这个BeanFactoryPostProcessor,它会在容器启动早期解析配置类,判断beanMethod里的方法是否有独立存在的价值,进而生成CGLIB代理。
5. 源码级面试的备考路径与现场答题技巧
5.1 读源码的正确顺序:别一上来就扎进DefaultListableBeanFactory
很多人在读Spring源码时最容易犯的错误,就是打开DefaultListableBeanFactory几百行代码,从头开始读,结果读了几百行就晕了。我自己踩过这个坑,后来摸索出一个更适合面试准备的顺序。
先不要碰实现类,先从入口接口开始。打开ApplicationContext,再看AbstractApplicationContext的refresh()。为什么从这个方法开始?因为它是整个容器的生命周期主线,你把refresh()里的每个步骤弄清了,IOC的整体骨架就已经建立。接着看BeanFactory和它的子接口,尤其是ListableBeanFactory、HierarchicalBeanFactory、AutowireCapableBeanFactory之间的关系,搞清楚哪些能力分别由哪个接口定义。
过了接口这关,再进入DefaultListableBeanFactory。但不用通读,只需要关注几个关键方法:preInstantiateSingletons()、resolveDependency()、findAutowireCandidates()、doCreateBean()。抓主线调用链,而不是抓每一个分支。
最后再回到循环依赖和AOP代理这两个常见面试盲区。去读DefaultSingletonBeanRegistry里那三个Map的相关方法,加上AbstractAutoProxyCreator里的getEarlyBeanReference和postProcessAfterInitialization。一边读一边可以记笔记,画出一条完整的“A依赖B、B依赖A”的对象创建流程图。
5.2 高频追问场景与回答示例
为了让你对实际面试有更直观的感觉,我整理了两个高频提问场景,附带我推荐的回答思路。
第一类是“能说说Spring的单例Bean是怎么保证只有一份的吗”。很多人会直接说“Spring容器用Map存了Bean,每次getBean都返回同一个”。这个回答太浅了。可以补充说:单例Bean在容器启动阶段通过preInstantiateSingletons()提前创建,创建完成后放入DefaultSingletonBeanRegistry的singletonObjects。后续的getBean调用会先走getSingleton(beanName),命中缓存就直接返回,不走创建流程,所以同一个BeanDefinition只会生成一个实例。如果深入一点,还可以提earlySingletonObjects和singletonFactories在循环依赖中的角色,把单例缓存和循环依赖关联起来。
第二类是“@Autowired和构造器注入哪个好”。这题看似开放,但其实面试官想听到的是一个有取舍的回答。你可以说:在普通场景下,我更推荐使用构造器注入,因为能保证依赖的不可变性和完整性,方便单元测试。但有几种情况不适合构造器注入,比如存在循环依赖或者可选依赖多于必有依赖时,setter注入或字段注入更灵活。然后补充源码层面的判断依据:构造器注入在createBeanInstance里就完成依赖解析,所以创建顺序必须无环;字段注入在populateBean阶段才执行,这时候实例已经存在,所以能配合三级缓存解决循环依赖。这样的回答既有观点,又有深度。
5.3 从源码读到面试落地的三个小技巧
最后分享几个做源码面试准备时的实用技巧,都是我亲自验证过有效的。
第一个技巧是画调用链,而不是抄源码。读源码最难的是把握主干和分支的关系。我推荐用一张“方法调用链”图而不是贴代码。比如把getBean的完整调用链画出来,从getBean进入doGetBean,从doGetBean进入getSingleton,从getSingleton到doCreateBean,再到populateBean、initializeBean等等。画完之后,很多看似零散的细节会自动串起来。
第二个技巧是,面试中谈到源码时,尽量带行号和类名,而不是只说接口名。这是最直观的区分“读过源码”和“没读过源码”的信号。比如你可以说“DefaultSingletonBeanRegistry里那个getSingleton方法,大概在两百多行的位置,有三个缓存Map的判断逻辑”,听起来可信度和专业感完全不一样。平时读源码时可以顺手记一把关键方法的行号。
第三个技巧是面试中遇到真的不会的问题,不要硬答。可以很坦诚地说,这条链路我没能完全读透,但我可以从BeanFactoryPostProcessor或者refresh()的角度去推测它的行为逻辑。能说出一个合理的推测,往往比胡扯一个错误的答案强很多。很多面试官要的不是你什么都知道,而是面对未知问题时推演路径的能力。
我个人最后的体会是:IOC真正难的不是“控制反转”这四个字,而是把四个字展开成一条能说清楚“容器从加载配置到完成一个Bean的完整生命周期”的记忆链。只要你愿意沉下心,从refresh()开始,一条线读到getBean、getSingleton、doCreateBean、populateBean,IOC在面试里就不会再是背不动的知识点了。