Spring Bean的创建过程在Java面试里属于“必考但十个人有八个说不透”的话题。我带团队面试时习惯问一个问题你天天写Component、AutowiredSpring到底是怎么把一个对象造出来的大多数人能答到“帮我们new了一个对象”这层就停了能继续往下说BeanDefinition、实例化策略、循环依赖三级缓存的少之又少。这篇就把这条链路从头到尾拆开讲透覆盖四种实例化方式、完整的生命周期回调、三级缓存的设计原因以及BeanPostProcessor如何成为AOP和各种自定义注解的发动机。适合正在准备面试的初中级开发也适合那些Spring项目跑了几年、但遇到依赖注入报错只能靠重启解决的同事。1. 为什么Spring要把“new一个对象”这件事搞得这么复杂Spring整天强调控制反转字面上就是把对象的创建权从程序员手里转交给容器。很多人把这当成一句口号背下来却没真正理解它到底解决了什么问题。回到最朴素的世界没有Spring的时候一个订单服务要调库存服务库存服务要调数据库你的代码大概长这样public class OrderService { private StockService stockService new StockService(); private UserService userService new UserService(); }如果UserService里又依赖了一堆别的Service这行代码会变得非常恐怖每一次new都会把整棵依赖树手工拉起来。更麻烦的是如果哪天想把UserService替换成带缓存的代理实现你得把new的这行代码全部改一遍。这就是对象之间的强耦合改一处牵一发动全身。Spring把创建权收走以后你要做的只是声明“我有一个依赖”容器负责在合适的时机把它造好、塞进来。这个改变让业务代码从对象组装中解放出来但也带来一个巨大的代价框架必须在对象生命周期的每个关键节点都能介入。它得知道什么时候构造、什么时候填充属性、什么时候执行初始化回调、什么时候销毁这就是我们常说的Bean生命周期它本质上是一套框架和对象之间的“约定”。所以Spring Bean不是普通的Java对象。一个被Spring管理的Bean指的是被容器实例化、组装、初始化并纳入生命周期管理、最后还能被容器销毁回收的对象。普通的new对象一旦创建出来谁创建谁负责释放完全不受管。而Bean从出生到销毁每个阶段都有明确定义的回调点这是理解整个Spring容器设计的钥匙。明白了这一层再去看网上铺天盖地的“三级缓存”“实例化顺序”你会有完全不一样的感觉不再是在背知识点而是顺着框架设计者的思路在走。2. BeanDefinitionSpring描述Bean的“设计图纸”BeanDefinition在Spring的体系里是个非常关键但常被忽视的概念。很多人会问Spring怎么知道一个类的构造参数是什么怎么知道它是单例还是原型怎么知道初始化方法叫什么名字答案全都记在BeanDefinition里它是Spring描述Bean的元数据可以理解成对象的“设计图纸”。2.1 一张图纸上到底写了什么一个完整的BeanDefinition至少包含这些关键信息属性作用说明beanClass真正要创建的类也就是Class对象scope作用域singleton单例、prototype原型lazyInit是否懒加载单例对象是否延迟到首次getBean再创建initMethodName初始化回调方法名比如init()destroyMethodName销毁回调方法名比如close()propertyValues属性值集合XML时代的 标签就存在这constructorArgumentValues构造器参数值autowireMode自动装配模式按类型还是按名称dependsOn显式依赖列表创建前必须先创建这些Beanprimary是否优先候选多候选注入时使用用代码来注册一个BeanDefinition非常简单Spring提供了BeanDefinitionBuilderBeanDefinitionBuilder builder BeanDefinitionBuilder.genericBeanDefinition(OrderService.class); builder.setScope(singleton); builder.addPropertyValue(timeout, 3000); builder.setInitMethodName(init); builder.setLazyInit(false); DefaultListableBeanFactory factory new DefaultListableBeanFactory(); factory.registerBeanDefinition(orderService, builder.getBeanDefinition());这段代码跑起来你不需要任何Spring Boot环境一个DefaultListableBeanFactory就是一个完整的最小容器之后调用factory.getBean(orderService)就能拿到对象。理解这一点是手写Spring的起点。2.2 三种注册路径扫描、配置类与编程式大部分项目中的BeanDefinition不是我们手动注册的而是由Spring的扫描器和解析器自动生成的。注解扫描路径ApplicationContext启动后ClassPathBeanDefinitionScanner会扫描指定包下的Component、Service、Repository等注解把每个类包装成ScannedGenericBeanDefinition然后注册进BeanDefinitionRegistry。这就是Service能生效的最底层原理不是Spring认识Service这个注解而是扫描器把所有带有这类注解的类都登记为BeanDefinition。配置类Bean路径Configuration类里的Bean方法会被ConfigurationClassPostProcessor解析每个Bean方法被转换成ConfigurationClassBeanDefinition。这里有个细节Bean方法本质上是一个“工厂方法”Spring不只是new一个类而是调用那个方法来获得对象所以返回值必须是实际要发布的类型方法名默认就是beanName。编程式路径有些场景比如在单元测试里手工组装一个容器或者要动态注册一个运行时才知道的类可以直接构造BeanDefinition手动注册上面那段代码就是演示。2.3 手写Spring时最容易忽略的一个环节如果你尝试自己搞一个迷你版Spring最容易踩的坑就是一上来就想“创建对象”完全绕过了BeanDefinition。但实际上Spring容器里的一切都从BeanDefinition开始实例化只是拿这张“图纸”去施工。没有BeanDefinition的类在Spring眼里根本不存在。这也解释了为什么一个没有被扫描到、也没在配置类里声明过的普通类即使你到处写AutowiredSpring也只会报NoSuchBeanDefinitionException因为它压根没有图纸。理解了BeanDefinition你才能回答面试里那个经典问题Spring在创建对象之前做了哪些准备工作答案不是一句“扫描注解”而是“把每一个被管理的类转换成BeanDefinition注册进BeanDefinitionRegistry在实例化前完成所有优先级排序和依赖预处理”。3. getBean启动的完整生命周期从调用入口到销毁回收先明确一个问题单例Bean是什么时候创建的很多人以为是getBean才创建其实大部分单例在容器启动阶段就被创建了。ApplicationContext在refresh过程中会执行finishBeanFactoryInitialization把所有非懒加载的单例Bean预实例化一遍。也就是说你启动一个Spring Boot应用看到控制台里所有Bean都在启动时被创建这就是在批量调用getBean。但无论哪种入口真正创建逻辑都收敛在getBean里。3.1 getBean的前置处理缓存、父容器与依赖排序创建一个Bean之前Spring要先做四件事解析beanName传入的字符串可能带“”前缀这代表要获取FactoryBean本身而不是它生产的对象需要先把前缀剔除拿到真正的beanName。查一级缓存调用getSingleton(beanName)查找singletonObjects这是“已完成全部初始化的单例”的存放地。如果命中了直接返回成品创建流程结束。查父容器当前容器没找到就去父容器getBean。处理dependsOn如果BeanDefinition里显示当前Bean依赖了某些Bean必须先递归创建这些前置依赖。这就是为什么两个Bean有先后依赖时Spring能保证顺序。这里有个容易忽略的点查缓存这一步不只是查一级缓存而是执行一个复杂的查找链这涉及循环依赖的处理后面单独展开。3.2 doCreateBean的“三段式”实例化、属性填充、初始化真正创建的逻辑都在doCreateBean里可以分成三个阶段。第一阶段实例化对应代码createBeanInstance。Spring根据BeanDefinition里记录的构造参数和自动装配模式选择合适的构造器来创建对象。这里调用的还是普通的反射newInstance所以“实例化”就是字面意思对象被new出来了但此刻它内部全是空值依赖尚未注入初始化方法尚未执行。第二阶段属性填充对应populateBean。Spring遍历BeanDefinition里的propertyValues通过反射调用setter为属性赋值。同时会调用实现了InstantiationAwareBeanPostProcessor的后处理器来处理Autowired、Resource这些注入注解。这一阶段完成后对象内部的依赖引用才真正可用。第三阶段初始化对应initializeBean。这个阶段是回调的密集区顺序非常重要顺序回调类型典型代表1Aware系列回调BeanNameAware、BeanClassLoaderAware、BeanFactoryAware、ApplicationContextAware2BeanPostProcessor.postProcessBeforeInitialization各框架的启动前处理3初始化方法PostConstruct、InitializingBean.afterPropertiesSet、init-method(按顺序执行)4BeanPostProcessor.postProcessAfterInitializationAOP代理对象生成的时机注意一个细节ApplicationContextAware的注入不是由Spring内核直接处理的而是通过ApplicationContextAwareProcessor这个BeanPostProcessor完成的这从侧面展示了后处理器的扩展威力。3.3 单例注册与销毁回收初始化完成后若是单例BeanSpring会调用addSingleton将其放入一级缓存singletonObjects从此后续getBean都能直接命中。销毁阶段发生在容器关闭时。单例Bean在创建完成后会注册一个DisposableBeanAdapter到disposableBeans列表。容器执行close时按逆序遍历List逐个调用销毁逻辑。销毁方法的触发顺序是PreDestroy注解方法、DisposableBean.destroy接口方法、destroy-method指定的方法。这一套倒序的清理机制设计上有点像try-with-resources把资源释放的责任从业务代码里剥离开了。如果Bean的scope是prototype那就简单得多每次getBean都走一遍完整创建流程返回后容器不再追踪也不负责销毁。这也是原型Bean经常导致内存泄漏的原因之一谁拿走了谁负责清理。4. 实例化方式的底层差异构造器、工厂方法与Supplier不少人对“实例化”的理解就只有构造器但Spring实际上支持好几种实例化方式。面试里问到Bean实例化方式这个词时能把这几种路径说清楚是很大的加分项。4.1 五种实例化路径及适用场景实例化方式底层机制常见用途构造器实例化反射Constructor.newInstance普通业务类最常用静态工厂方法Method.invoke调用static方法第三方的构造逻辑在静态方法里实例工厂方法先创建工厂Bean再调其方法老式XML配置遗留场景Supplier实例化直接执行lambda代码块编程式注册、动态构造逻辑FactoryBean调用getObject()方法框架集成如MyBatis的Mapper扫描构造器实例化是默认路径Spring会通过ConstructorResolver来推断该用哪个构造器如果只有一个构造器直接用有多个时优先找被Autowired标注的构造器Spring 4.3之后如果一个类只有单个构造器可以省略Autowired。这里也解释了为什么你写了一个带参构造器以后Spring可能会报找不到无参构造器因为它优先尝试无参构造再用BeanDefinition里的构造参数值去匹配带参构造匹配失败就会抛异常。静态工厂方法和实例工厂方法在XML时代用得比较多现在基本被Bean方法替代了。你要知道的是Bean方法本质上就是一种实例工厂方法的变体Configuration类本身被实例化后Spring调用其方法获取对象。Supplier实例化是编程式注册时的优雅手段比如运行时需要根据配置动态决定构造参数BeanDefinitionBuilder builder BeanDefinitionBuilder.genericBeanDefinition(ConnectionPool.class); builder.setInstanceSupplier(() - new ConnectionPool(config.getMaxSize(), config.getTimeout())); factory.registerBeanDefinition(connectionPool, builder.getBeanDefinition());FactoryBean则要单独说它的getObject返回值是“工厂生产的对象”。MyBatis的MapperScannerRegistrar就是典型的FactoryBean玩法每个Mapper接口注册一个MapperFactoryBeangetObject里生成动态代理对象。所以当你在Spring里注入一个Mapper接口时拿到的其实是一个代理而Spring只认识FactoryBean本身。4.2 为什么推荐构造器注入而不是字段注入实例化方式直接影响了依赖注入的可靠性。字段注入的代码写起来很爽但存在几个问题依赖不透明类的依赖全藏在私有字段里不便测试必须靠反射或者完整容器才能构造更重要的是它容易掩盖循环依赖。构造器注入则会强制你在创建对象时就把依赖交代清楚对象要么构造完整要么干脆创建失败不存在“半成品”状态。当然构造器注入也有代价当两个Bean互相通过构造器引用时循环依赖无法解决因为此时根本还没有东西可以提前暴露。这也是市面上一部分人仍然坚持字段注入的原因但作为默认规范团队项目用构造器注入会更稳。4.3 代理对象如何绕过构造器有个反直觉的场景Spring AOP最终返回的是代理对象那代理对象本身是如何实例化的如果目标类有构造器JDK动态代理通过Proxy.newProxyInstance生成它不调用目标类的构造器而是创建一个同接口的代理类实例CGLIB则通过生成目标类的子类实际还是绕过了业务构造器。这也是为什么字段注入在代理场景下经常踩坑代理对象无法正确初始化真实对象的私有字段依赖。看AOP相关的疑难杂症时优先怀疑这一层往往比怀疑配置语法更有效。5. 三级缓存循环依赖的解决方案与边界三级缓存是Spring Bean创建过程里最被追捧、也最容易讲混的概念。要理解它先理解它解决了什么问题。5.1 问题复现与解决思路循环依赖长这样A持有B的引用B也持有A的引用。如果都通过字段或setter注入单纯递归式创建必死循环创建A时要填B创建B时要填AA还在创建中没有对象可以注入给B。老Java程序员都经历过EJB时代循环依赖处理不好就是StackOverflow。Spring的思路很直接——先创建A的早期引用把A的一个“半成品”暴露出来这样B创建时能拿到A的引用完成注入B完成后再回头把B注入给A两个Bean就都完整了。问题在于这个“早期引用”放在哪里就催生了三级缓存。5.2 一级、二级、三级缓存各自的职责缓存名称数据结构存放内容singletonObjectsConcurrentHashMap一级缓存完整的单例成品earlySingletonObjectsConcurrentHashMap二级缓存提前暴露的早期引用singletonFactoriesHashMap三级缓存ObjectFactory工厂能生成早期引用创建过程中Bean在实例化完成但属性尚未填充时Spring就把一个ObjectFactory放进三级缓存。这个工厂的核心能力是在需要时调用getEarlyBeanReference生成早期引用。getBean缓存查找的完整顺序是这样的先查singletonObjects命中直接返回完整Bean。查earlySingletonObjects命中说明有Bean正处在循环依赖中返回早期引用。查singletonFactories拿到ObjectFactory并调用getObject把产物放入二级缓存同时移除三级缓存里的工厂。后续再有人要这个Bean直接从二级缓存拿。如果你只想解决“B拿到A的引用”这个问题其实二级缓存就够了A实例化后直接把A放进去B拿的就是原始对象。但Spring偏偏多加了一层三级缓存原因只有一个——AOP。AOP代理是在postProcessAfterInitialization阶段生成的。如果只有二级缓存A实例化后放进二级缓存的是一个原始对象后续A被AOP代理替换后B手里拿到的还是原始对象代理完全失效。除非改成A实例化后立刻判断是否需要代理需要就直接生成代理放二级缓存。这在没有循环依赖时会导致不必要的代理提前创建白白损耗性能。三级缓存则把“是否生成代理”的决定延迟到真正有人依赖A的那一刻。没人循环依赖就等到初始化后正常生成代理有人循环依赖就通过SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference在ObjectFactory里生成早期代理引用。所以三级缓存的核心价值就一句话在保持“没有循环依赖时不提前创建代理”的前提下让循环依赖场景下注入到其他Bean的仍然是增强后的代理对象。5.3 三级缓存解不了的循环依赖三级缓存不是万能药至少有三种场景它无能为力构造器注入的循环依赖A的构造器需要BB的构造器需要A。实例化都没完成没有“半成品”可暴露直接报错。非单例Bean的循环依赖prototype每次都是全新的根本没有缓存的余地一进入循环就死锁。Async标注的Bean循环依赖异步代理和普通AOP代理不是同一个机制即使通过早期引用暴露了后续注入时拿到的方法增强可能不完整表现成线程池或代理失效一类诡异问题。Spring Boot 2.6开始默认禁止循环依赖启动时遇到循环直接抛异常宁可让你改设计也不要依赖这个机制。以前的版本默默允许导致大量项目把循环依赖当常规操作后来每次升级都崩一批。如果项目里出现循环依赖正确姿势是重构依赖关系拆一个中间对象出来而不是打开allow-circular-references配置一了百了。6. BeanPostProcessorSpring所有“黑科技”的插件机制前面几章反复提到了BeanPostProcessor但它值得单独拿出来讲因为理解了它你就理解了Spring一半的扩展能力。6.1 接口设计与执行时机最核心的接口就两个方法public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException; }真正意义上的服务对象是“别人”Spring在初始化阶段自动调用所有已注册的BeanPostProcessor传给它们当前正在创建的Bean允许它们修改、包装、替换。返回值会被当作最终的Bean。除了这个基础接口还有InstantiationAwareBeanPostProcessor它把介入时机扩展到实例化前后和属性填充阶段分别对应postProcessBeforeInstantiation、postProcessAfterInstantiation、postProcessPropertyValues。还有SmartInstantiationAwareBeanPostProcessor额外提供了determineCandidateConstructors决定用哪个构造器和getEarlyBeanReference支撑三级缓存早期引用。AOP的AbstractAutoProxyCreator就是SmartInstantiationAwareBeanPostProcessor的实现。常见后处理器和能力的对应关系整理成表后处理器能力AutowiredAnnotationBeanPostProcessorAutowired、Value注入CommonAnnotationBeanPostProcessorResource、PostConstruct、PreDestroyApplicationContextAwareProcessorApplicationContextAware回调注入AnnotationAwareAspectJAutoProxyCreator基于注解的AOP代理ConfigurationClassPostProcessor严格来说是BeanFactoryPostProcessor负责解析Configuration6.2 一个实战案例用自定义后处理器做方法耗时日志很多人在项目里做“方法日志切面”第一反应是引入Spring AOP的Aspect。但如果你只想对某个自定义注解做非常聚焦的代理增强完全可以自己写一个超轻量的后处理器还能顺便加深对原理的理解。假设定义了一个注解MethodLog目标是打印被标注方法的耗时。核心代码如下public class MethodLogBeanPostProcessor implements BeanPostProcessor { Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { Class? targetClass bean.getClass(); if (targetClass.isAnnotationPresent(MethodLog.class) || Arrays.stream(targetClass.getMethods()).anyMatch(m - m.isAnnotationPresent(MethodLog.class))) { return Proxy.newProxyInstance(targetClass.getClassLoader(), targetClass.getInterfaces(), (proxy, method, args) - { long start System.currentTimeMillis(); Object result method.invoke(bean, args); System.out.println(method.getName() cost (System.currentTimeMillis() - start) ms); return result; }); } return bean; } }注册进容器后凡是标注了MethodLog且实现了接口的BeanpostProcessAfterInitialization阶段就会被替换成JDK动态代理。执行时打印耗时原始对象通过method.invoke(bean, args)调用。整个过程你完全避开了复杂的AOP表达式也直观理解了“代理在Bean生命周期的最后阶段替换了原始Bean”这一事实。实际生产环境建议还是用Spring AOP或AspectJ自己写后处理器适合小场景和学习验证但要小心代理类型转换问题、接口缺失问题而且没实现接口的类需要换CGLIB策略。6.3 扩展自定义功能的标准姿势明白了这个机制再去看一堆框架的源码会豁然开朗Spring Security的方法级权限校验是后处理器把对象包了一层安全代理Spring的Async是AsyncAnnotationBeanPostProcessor把对象包了异步代理OpenFeign的FeignClient注解是注册时通过FactoryBean生成了HTTP动态代理。所有“加一个注解就多一个能力”的黑科技底层几乎都是BeanPostProcessor在对象初始化前后动了手脚。所以如果你要给团队框架做扩展比如自动给某些Bean做统一的前后处理正确做法不是去改Spring源码而是写一个BeanPostProcessor并注册进去。7. 高频追问与实践坑位把原理变成肌肉记忆原理讲完了接下来聊聊面试和实战里最常见的那些变化题。7.1 面试官最爱追问的五个点第一问Bean实例化阶段到底做了什么回答重点落在ConstructorResolver选择构造器、反射newInstance上同时要说清此刻对象还是空壳。第二问依赖注入发生在哪个阶段属性填充阶段Autowired的注入由后处理器完成不是反射直接赋值。第三问AOP代理在什么时候生成的postProcessAfterInitialization阶段后处理器返回代理对象替代原始Bean被放入容器。第四问PostConstruct、InitializingBean、init-method的执行顺序PostConstruct先执行接着afterPropertiesSet最后init-method。销毁顺序反过来PreDestroy先DisposableBean.destroy其次destroy-method最后。第五问为什么解决循环依赖需要三级缓存而不是两级核心是延迟AOP代理的创建时机允许对象在实例化后先暴露为早期引用等真正有循环依赖时再决定是否生成代理。7.2 实践中容易翻车的几个场景单例注入原型Bean这是经典坑。容器启动时创建一个单例Bean属性填充阶段把原型Bean注入进去之后再getBean原型Bean单例里持有的永远是当初那一个不会每次新建。正确做法是注入ObjectFactory或者使用Lookup。PostConstruct在代理对象上执行多次这个非常隐蔽。如果某个类被AOP代理而代理机制的某个环节又触发了一次对象创建原始对象和代理对象各自执行一次PostConstruct你会在日志里看到初始化逻辑跑了两遍。排查思路是检查是否为该Bean配置了多个后处理器以及是否有工厂方法生成对象后再次进入初始化。高版本Spring Boot循环依赖直接抛错。升级到2.6以后项目里如果存在循环依赖启动直接失败。别急着开allow-circular-references开关先审视依赖结构把循环拆掉。这个默认行为其实是在逼着代码变好。还有一个很容易踩的坑忘了注册BeanPostProcessor本身也是一个Bean。Spring会提前把实现了BeanPostProcessor接口的类创建好并注册到容器里之后才创建普通Bean。所以如果你用Bean方式注册自定义后处理器最好声明为static方法否则配置类可能被提前实例化导致一些底层功能来不及注册。7.3 学习建议手写一个简化版Spring我的建议一直很朴素想彻底搞懂Bean创建过程就自己手写一个极简容器。不需要实现全量功能但要把这几件事做出来一个BeanDefinition类放beanClass、scope、属性值。一个BeanFactory类内部用ConcurrentHashMap存单例对象提供register和getBean。getBean里做懒加载第一次取没有就去实例化并执行属性注入。加一个简单的BeanPostProcessor列表支持在初始化前后调回调。再尝试实现singletonFactories三级缓存跑通一个setter注入的循环依赖。做完这个再看Spring源码里的DefaultSingletonBeanRegistry和AbstractAutowireCapableBeanFactory你会发现自己能逐行读懂而不是盯着花括号发呆。开源的Spring Boot商城项目、后台管理系统其实都是同一种套路入口是自动配置类中间是工厂方法生产Bean最后靠各种后处理器完成增强。理解了创建过程再去看那些项目里自定义的BeanProcessor和FactoryBean基本几秒钟就能猜出它们想干什么。我个人带团队时的体会是真正让人豁然开朗的时刻不是背完生命周期那张表而是亲手在一个自定义注解上写出第一个BeanPostProcessor然后看着注入的字段被代理对象的日志打印出来。那一刻“Spring帮我们new对象”这句话就彻底变成了“Spring替我们管理了一个对象的完整人生”。如果这篇能帮你把这个链条上的每一环都接上那它就没白写。看到这里不妨现在打开你的项目找个FactoryBean或者AOP切面的实现类打断点跟一遍收获会比读十篇文章都大。