1. 从new对象到知道Bean是什么到底差在哪先聊点实在的。我当年刚学Spring的时候最迷惑的就是“Bean到底是个什么玩意儿”。网上说“Bean就是被Spring管理的对象”这话没毛病但等于没说——它解释不了为什么我们要绕这么大一圈把一个对象交给别人管而不是自己new一个。你想想最开始你写代码是不是这样UserService userService new UserService(); userService.setUserDao(new UserDao());这种方式的缺点在项目小的时候完全感觉不到。但当你有十个Service互相依赖、每个Service又要连数据库、又要调外部接口、又要发消息的时候你会在每个用到UserService的地方重复new对象、重复set依赖。一旦UserService的构造函数变了所有调它的地方全得跟着改。更难受的是你没法在创建UserService的中间过程里统一做点“额外的事”——比如打日志、做权限校验、给某个属性动态赋值。你面向的是“怎么把这个对象new出来”这个动作本身而不是“这个对象怎么被组装好、怎么被用起来”这个全流程。Spring的答案就是IoC控制反转。把“创建对象、给对象赋值、管理对象生命周期”这件事从业务代码里拿走交给Spring容器统一来做。这个被容器管理的对象就叫Bean。你可以把容器想象成一个“对象托管中心”——你告诉它“我要一个UserService”它负责创建、注入依赖然后交到你手上。理解Bean的核心不在于记住“Bean是Spring管理的对象”这个结论而在于搞清楚三个问题它是什么时候被创建的、它是怎么被组装出来的、它从创建到销毁的整个过程里到底经历了哪些环节。搞懂这三个问题Spring后面那些看似玄乎的东西——三级缓存、循环依赖、AOP原理、自动配置——都会顺藤摸瓜地串起来。这篇文章我就按这条线把自己从源码到实践摸过的东西梳理一遍。不整虚的全部是能落地、能拿去面试、能帮你排查线上问题的那种内容。全文围绕下面几个关键点展开Bean的生命周期全流程、三级缓存的底层设计逻辑、实例化方式与依赖注入的多种途径、常见坑与排查思路。2. Bean的一生生命周期拆开看2.1 从配置到BeanDefinition先有“图纸”再有“实物”要理解Bean的生命周期你必须先建立一个概念容器里管的不是对象本身而是对象的“元信息”。Spring把你在XML里写的bean标签或者在类上标的Component注解统一转换成一个叫做BeanDefinition的东西。你可以把BeanDefinition理解成一张“生产图纸”——上面写着这个Bean的类全路径是什么用什么类来实例化作用域是singleton还是prototype是懒加载还是容器启动时就创建构造函数参数是什么属性依赖有哪些初始化方法、销毁方法分别叫什么容器启动的时候第一件事就是扫描配置把这些“图纸”收集到一个Map里key是BeanName默认是类名首字母小写value就是BeanDefinition。这一步特别关键因为后面所有实例化、属性填充、初始化全部都是照着BeanDefinition来执行的。你如果在配置里写错了类路径或者某个注解属性填错了通常不会在启动那一刻马上报错——真正报错往往发生在创建这个Bean的时候因为图纸标注的“材料”找不着了。这里有个细节很多人忽略Spring支持XML、注解、JavaConfig三种方式配置Bean但最终它们都殊途同归被解析成BeanDefinition。所以你并不需要纠结“老师傅都用注解我是不是就不用管XML了”——你只要知道注解配置最终也会变成BeanDefinition后面的一切流程都是一样的就够了。2.2 实例化这一步到底发生了什么当容器启动完毕开始创建Bean的时候最先走的是实例化阶段。这个阶段的目标只有一个把类变成一个“原始对象”即刚刚分配内存、还没赋值的对象。你可以通过构造器反射来实例化也可以使用工厂方法。Spring判断用哪种方式实例化的依据是BeanDefinition里的配置。默认情况下它调用的是无参构造器如果你没配构造函数参数通过反射搞定Object instance beanClass.getDeclaredConstructor().newInstance();如果类没有无参构造器你就得在配置里指定构造器参数Spring会找到匹配的构造器来完成实例化。这背后的逻辑其实不复杂但你一定要理解一件事这时拿到的对象所有属性都是默认值依赖还没注入还处于一个“半成品”状态。这个阶段有一个容易踩坑的点——类里面如果写了复杂的实例代码块或者成员变量初始化逻辑它们会在这个阶段执行但此时依赖的Bean还没注入。如果你在成员变量声明处直接调用其他Bean的方法大概率会拿到null因为依赖注入还没发生。2.3 属性填充就是“按图纸把零件装上去”实例化完成之后Spring开始给这个“半成品”填充属性。填充的时机和顺序是有讲究的。对于Autowired、Resource、Value这类注解Spring用的是后置处理器来处理对于XML里的property配置走的是另一条路径。但不管走哪条路最终的结果是这个对象从“只有身份”变成了“有血有肉”。属性填充这一步有个非常重要的事件点如果当前Bean实现了BeanNameAware、BeanFactoryAware、ApplicationContextAware这些Aware接口Spring会在这前后回调对应的方法。它们的顺序大致是BeanNameAware.setBeanName()告诉Bean它在容器里的名字BeanClassLoaderAware.setBeanClassLoader()给Bean设置类加载器BeanFactoryAware.setBeanFactory()给Bean一个感知BeanFactory的能力ApplicationContextAware.setApplicationContext()如果是ApplicationContext容器才会回调这个这些Aware接口在日常业务代码里用得不多但在写框架、做组件封装、需要拿到容器上下文做动态操作的时候非常有用。我自己的一个习惯是能用ApplicationContextAware直接在类里拿上下文就别到处注入ApplicationContext因为前者让你拿到的上下文是当前容器自身语义更准确。2.4 初始化前后BeanPostProcessor在偷偷干大事属性填充完成之后Bean就已经是一个完整对象了但Spring还留了两道“门”让扩展点介入BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterInitialization。这两道门的名字看起来很像但实际作用完全不同。postProcessBeforeInitialization在初始化方法之前执行。这里最典型的应用就是Autowired注解的处理——实际上AutowiredAnnotationBeanPostProcessor已经提前在属性填充阶段就工作了但很多框架级AOP代理的创建、动态代理类的包装都发生在初始化之后。postProcessAfterInitialization是应用最广泛的扩展点。Spring AOP的自动代理就是在这里通过AbstractAutoProxyCreator完成的——容器拿到一个初始化完成的Bean检查它是否需要被代理如果需要就创建一个代理对象返回替换掉原来的原始对象。接下来才是我们常说的“初始化方法”三兄弟实现InitializingBean接口的afterPropertiesSet()方法XML或注解里配置的init-method方法或者Bean(initMethod ...)PostConstruct注解标注的方法这里有个顺序问题经常被面试问到PostConstruct、InitializingBean、init-method的执行顺序是什么答案很明确PostConstruct最优先然后是afterPropertiesSet()最后才是init-method。因为PostConstruct是通过CommonAnnotationBeanPostProcessor这个后置处理器在初始化阶段之前调用的它的时机比InitializingBean更靠前。我自己写代码的习惯是如果逻辑简单直接用PostConstruct方便直观如果涉及到需要在Spring完整初始化之后再做某些操作优先考虑ApplicationRunner或者CommandLineRunner。为什么因为这两个接口是在整个容器上下文刷新完成之后才调用的此时所有Bean已经就绪比在一个Bean的初始化方法里去“指望”其他Bean已经初始化完要靠谱得多。2.5 销毁优雅下线别让资源泄漏Bean使用完之后容器关闭时会触发销毁流程。销毁也分几步如果Bean实现了DisposableBean接口会回调destroy()方法如果配置了destroy-method也会执行PreDestroy注解标注的方法同样是这个阶段的任务优先级高于DisposableBean接口。你可能会问singleton的Bean容器关闭才销毁我还管它干嘛这里最常见的坑其实在prototype作用域上。prototype的Bean创建后不归容器管容器只负责创建、注入、初始化但不会保存它的引用也就不会主动调用它的销毁方法。如果你的prototype Bean里持有连接池、文件句柄这类需要清理的资源就得自己想办法处理别指望Spring帮你擦屁股。另外还有一个小细节PreDestroy只对singleton作用域的Bean生效对prototype无效。这个事我有一年在排查线上连接数暴涨的时候才发现——某个被声明成prototype的短信客户端每次容器关闭都没释放连接直到把连接池打满。后来改成在业务代码里显式调用销毁方法才解决。3. Bean的创建三级缓存与循环依赖的底层真相3.1 为什么会循环依赖容器为什么能“兜底”先看一段代码Service public class UserService { Autowired private OrderService orderService; } Service public class OrderService { Autowired private UserService userService; }Spring启动的时候先创建UserService发现它依赖OrderService去创建OrderService发现它又依赖UserService而UserService还没创建完这时候就“死循环”了。这是循环依赖最典型的场景。早期Spring遇到这个情况是直接报错的。后来官方引入了三级缓存机制使得“singleton Bean之间的循环依赖”在大多数情况下可以被自动化解。但你一定要理解三级缓存不是万能的它只解决了“单例且非构造器注入”的循环依赖。如果循环依赖发生在构造器注入上或者发生在prototype作用域上Spring仍然会直接抛出BeanCurrentlyInCreationException。为什么三级缓存能解决setter注入的循环依赖却解决不了构造器注入的因为构造器注入意味着“你必须先有完整的依赖对象才能创建当前对象”而setter注入允许“先创建当前对象的空壳再往里塞依赖”。三级缓存的本质就是提前暴露“半成品对象”让循环的双方能各自拿到一个引用。半成品不完整没关系等最终属性填充完之后大家手里的引用会看到完整状态。3.2 一级、二级、三级缓存里到底放的什么这是Spring源码里最常被问到的数据结构。很多文章把这三层缓存说得很玄乎其实你拆开看就三行MapString, Object singletonObjects; // 一级缓存成品对象 MapString, Object earlySingletonObjects; // 二级缓存提前暴露的半成品 MapString, ObjectFactory? singletonFactories; // 三级缓存对象工厂一级缓存singletonObjects存的是已经完全初始化的Bean俗称“成品”。大多数情况下你从Spring容器里拿到的Bean就在这里。二级缓存earlySingletonObjects存的是“提前暴露”的半成品对象。这个缓存的存在意义在于当一个对象通过三级缓存的ObjectFactory被提前暴露出去后它的“工厂”就完成了历史使命此时如果有其他Bean依赖它直接从二级缓存里拿“半成品”就行而不需要再次通过工厂创建。三级缓存singletonFactories存的是ObjectFactory——一个函数式接口执行它就能获得这个Bean的早期引用。而这个早期引用可能是原始对象也可能是被AOP代理过的半成品。这里最关键的问题来了为什么需要三级缓存而不直接用二级缓存答案藏在AOP里。3.3 为什么必须要有三级缓存二级行不行很多网上的文章说“三级缓存是为了解决循环依赖下的代理对象问题”这个说法方向是对的但不够精确。真正的原因是这样的在doCreateBean方法里Spring给Bean完成属性填充之前会把一个ObjectFactory放入三级缓存。这个ObjectFactory的getObject()方法内部会调用getEarlyBeanReference()而后者会遍历SmartInstantiationAwareBeanPostProcessor来确定是否需要提前生成代理对象。如果你只用二级缓存也就是直接暴露一个半成品原始对象那么当这个Bean需要被AOP代理时就会出现一个问题原始对象已经被其他Bean引用进去了后续再生成代理对象替换的只是容器里的对象其他Bean手里拿到的仍然是原始对象代理逻辑就失效了。换句话说三级缓存存在的意义是可以允许一个Bean在“还没完成完整的初始化流程”时就被确定是否需要生成代理对象并把这个代理对象暴露出去。归根结底CGLIB或者JDK动态代理都需要一个“目标对象”才能创建代理三级缓存机制保证了在创建代理时能拿到正确的目标且代理对象的生成时机足够晚、又不至于晚到循环依赖已经报错。我个人的理解是你不需要把三级缓存的每一行源码都背下来但需要对“三级缓存是为了让代理对象的创建与循环依赖的提前暴露共存”这个结论有清楚的认知。这里补充一个实践结论如果你用了Lazy注解去解决循环依赖走的是另一条路——直接把依赖方替换成一个代理对象让它在真正调用时再创建。这种方式不经过三级缓存所以也能绕开构造器注入的循环依赖问题。但Lazy的语义是“延迟”如果你依赖的是单例Bean延迟也许没必要如果你依赖的是prototype BeanLazy反而可能带来你预期之外的行为后面我会专门讲。3.4 什么时候三级缓存会失效这个部分必须站在实际排查问题的角度去说。三级缓存不是“万能灵药”以下情况它会失效构造器注入的循环依赖因为创建构造器参数时当前Bean还没进入三级缓存Spring根本找不到“早期引用”prototype作用域的循环依赖原型Bean不缓存不存在早期暴露机制Async注解标记的Bean由于代理创建机制特殊也容易出现循环依赖报错Transactional与某些AOP组合情况下若代理创建时机异常也会报“BeanCurrentlyInCreationException”如果线上出现循环依赖报错我的排查路径一般是先看报错日志里提到了哪几个Bean把它们之间的依赖关系画出来然后确认有没有构造器注入再确认有没有Async或者自定义的BeanPostProcessor参与最后根据情况调整设计——可以把某个依赖改成Lazy也可以把构造器注入改成setter注入或者干脆重新梳理组件边界把循环依赖的关系拆掉。记住循环依赖本质上是一种设计上的“坏味道”能拆尽量拆不要过度依赖Spring的容错能力。4. Bean的实例化方式与依赖注入别再只会用Autowired4.1 三种实例化方式对比构造器、静态工厂、实例工厂Bean的实例化方式不止一种。日常开发中90%的情况是“反射调用构造器”但Spring还支持通过静态工厂方法和实例工厂方法来实例化Bean。前者主要用于集成一些不允许直接new的工具类比如某些配置类里的加密算法实例后者在老的XML配置里比较常见。静态工厂的典型写法是bean idclock classjava.time.Clock factory-methodsystemUTC/实例工厂的写法需要先定义一个工厂Bean再通过factory-bean和factory-method指向它bean idclockFactory classcom.example.ClockFactory/ bean idclock factory-beanclockFactory factory-methodcreateClock/在注解配置时代这些场景通常被Bean方法替代了——Bean方法的本质就是“实例工厂方法”它的执行结果就是被Spring管理的Bean。所以你会看到Configuration类里定义Bean方法本质上就是用一种更优雅的方式表达“这个对象该怎么被创建”。理解这些实例化方式的差异对排查问题有实际意义。比如一个Bean你在Bean方法里new了一次在其他地方又new了一次这两个对象是什么关系答案是只有Bean方法里创建的那个对象被容器管理你自己new的那个Spring管不着那也不是Bean。这个问题我见过太多次说白了就是没理解“Bean是被容器实例化的对象”这句话。4.2 属性注入三兄弟字段、setter、构造器依赖注入的方式官方推荐的是构造器注入。为什么因为构造器注入可以保证依赖的完整性——对象一旦构造出来所有必填依赖就已经就位不存在“半个对象”的状态。而且构造器注入天然配合不可变性字段可以声明为final这在并发场景下更安全。字段注入Autowired写在成员变量上是写起来最爽、但问题也最多的一种一方面它依赖Spring容器在创建对象后反射注入字段调试的时候你很难直观看到赋值过程另一方面脱离Spring容器做单元测试时字段注入的类很难自己组装。setter注入属于折中方案先创建对象再通过setter方法注入依赖。它允许对象创建之后继续修改依赖但在“必须依赖”的场景下容易导致空指针。我自己的实践经验是项目里能用构造器注入的地方一律用构造器注入确确实实需要运行时动态替换的依赖才考虑setter注入字段注入纯粹为了少写代码不建议除非项目里已经统一成这种方式有人会问构造器注入遇到循环依赖怎么办前面已经提到了直接用Lazy打破循环就行。但我要提醒一句如果你发现一个系统里到处都在用Lazy解决循环依赖最该做的不是继续打补丁而是去梳理依赖关系、拆分模块。4.3 Autowired、Resource、Inject的查找逻辑这个知识点是面试高频也是实际工作中容易踩坑的地方。Autowired是Spring的注解默认按类型byType注入如果同类型有多个Bean它会再按字段名byName匹配如果还是匹配不上就报NoUniqueBeanDefinitionException。Resource是JDK标准注解javax.annotation.Resource默认按名称byName注入找不到再按类型找。Inject是JSR-330标准注解按类型注入需要额外引入依赖。它们之间的优先级差异很容易让人记混。我给个口诀Autowired先类型后名字Resource先名字后类型。实际开发中如果某个接口有多个实现类我一般会用Qualifier显式指定Bean名称别依赖默认的按名字匹配那太隐晦了代码评审也不好看。另外还有个细节Autowired可以用在构造器、字段、setter、普通方法上而Resource主要用于字段和setter方法。如果某个Bean只有构造函数需要注入优先用Autowired构造器注入。4.4 手动获取Bean的几种姿势工作中写工具类、写框架代码时经常需要手动从容器里拿Bean。这里有几个方式注入ApplicationContext调用getBean(Class?)实现ApplicationContextAware保存上下文后再拿用AutowiredAnnotationBeanPostProcessor等后置处理器直接干预不推荐业务代码里这么干在Spring Boot里还可以用ApplicationContextHolder之类的工具类来回获取其中最常见的组合是定义工具类实现ApplicationContextAware静态持有ApplicationContext然后封装一个getBean(String name)方法。这里要特别注意“静态持有容器”的坑如果容器被关闭再重新创建静态持有的上下文可能已经过期调用getBean会报IllegalStateException。多租户、热部署场景下要谨慎使用。5. Bean的作用域与注解驱动不同场景下的配置细节5.1 singleton、prototype、request、session到底怎么选Bean的作用域在Spring中默认是singleton也就是整个容器只维护一个实例。绝大多数Service、Dao、Controller都适合单例因为它们是无状态的靠方法参数传递业务数据。prototype意味着每次获取都新建一个实例适合有状态、需要隔离的组件。但你别以为顺手标记个Scope(prototype)就完事了——prototype Bean的创建与销毁都不受容器主动管理它只在获取的时候被创建但“销毁”这件事容器根本不会主动触发。前面已经提到这也是一个容易导致资源泄漏的地方。request和session作用域只在Web应用中生效分别对应一次HTTP请求和一次HTTP会话。如果你在一个singleton的Service里通过代理注入一个request作用域的Bean就必须启用代理模式否则会报“No thread-bound request found”。具体做法是Bean Scope(value request, proxyMode ScopedProxyMode.TARGET_CLASS) public RequestInfo requestInfo() { return new RequestInfo(); }这种场景下代理模式是必须的它会让singleton的Bean每次调用时通过代理去获取当前请求对应的实例。说实话request/session作用域在日常CRUD项目里用得不多但如果做多租户、做用户上下文传递这是绕不开的知识点。5.2 Scope不会自动让代理生效这是我在实际项目里看到的一个高频误用。有人以为只要标注了Scope(prototype)那么注入其他Bean里的实例每次调用都是全新的。但事实是如果注入方和被注入方都是单例注入只会发生一次注入进去的那个引用就被固定下来了——哪怕被注入的类是prototype作用域注入方拿到的还是同一个对象。举个例子Component public class SingletonService { Autowired private PrototypeBean prototypeBean; }虽然PrototypeBean声明了Scope(prototype)但它在SingletonService创建时只注入一次之后SingletonService一直拿着同一个PrototypeBean实例。这不是Spring bug而是注入时机决定的——注入是创建期行为不是调用期行为。要真正实现“每次调用都拿新实例”方案有两个一是注入ObjectFactoryT或ProviderT在调用时手动getObject()二是使用Lookup方法注解让Spring动态生成一个覆盖方法去容器里获取实例。两者都能实现“延迟查找”但语义上略有差别。我自己更倾向于ProviderT因为它侵入性更小测试也更好写。5.3 注解驱动下Bean的注册路径注解驱动的核心是组件扫描ComponentScan告诉Spring扫描哪些包Component、Service、Repository、Controller这些注解标注的类会被自动注册成Bean。它们之间的区别其实只有语义层面对容器而言它们都是“被扫描到的候选组件”。另一个注册路径是ConfigurationBean。Configuration类本身被CGLIB增强Bean方法会被拦截确保返回的Bean是单例的。如果你一不小心把Configuration写成了ComponentBean方法仍然会执行但每次调用都会创建新对象单例语义就失效了——这个问题非常隐蔽我身边就有同事踩过。这里再补一个容易被忽略的细节ComponentScan默认只扫描当前包及其子包所以Spring Boot的主类放在根包下是有讲究的。你把主类放在某个深层子包里某些组件扫不到启动时会报“找不到Bean”的错——这都是配置层面的基础问题但很多人排查半天才发现是包扫描范围不对。6. 面试高频问题与被问烂的核心原理回顾6.1 什么是控制反转与依赖注入控制反转IoC把对象创建和依赖管理的控制权从开发者手里转移给容器。开发者不再主动new对象、不再自己管理依赖关系而是声明“我需要什么”容器负责组装好。依赖注入DI是控制反转的一种实现方式。它有三种主要形式构造器注入、setter注入、接口注入。构造器注入是官方推荐的方式因为它在对象构造阶段就完成依赖装配保证了对象的完整性。你回答这个问题时最好能随口带出一个对比案例例如“如果我自己new Service那么所有依赖都要自己set用了Spring之后我只需要加一个Autowired容器就会把依赖送过来。这背后是通过BeanDefinition和BeanPostProcessor实现的。”这样比干巴巴背诵概念要有说服力得多。6.2 Spring容器是怎么做到单例的Spring容器对单例Bean的管理核心结构就是三级缓存。当容器创建singleton Bean时会先加锁写入singletonObjects未创建完成时会通过earlySingletonObjects和singletonFactories暴露早期引用。这样既保证了单例Bean在容器范围内的唯一性也为循环依赖兜了底。你也可以顺着这个回答讲一下“单例Bean的线程安全”问题——Spring容器只保证Bean是单例的不保证Bean内部的共享状态是线程安全的。所以如果你在单例Bean里用了可变的成员变量就得自己加锁或者换成ThreadLocal。这个问题每年都会被翻出来问核心就是考察你对“容器职责边界”的理解。6.3 为什么Spring Boot启动时没有报错但调用接口时才发现Bean没注入这类问题的根源通常在两个方面一是Bean的初始化被延迟到了首次调用二是Bean的创建过程在启动阶段已经完成但代理对象在首次调用时才真正做一些初始化。大多数情况下Spring Boot默认会在启动阶段创建所有非懒加载单例Bean所以“启动不报错”说明Bean已经被成功创建了如果调用时才发现某些依赖没生效重点排查PostConstruct里调用了还没初始化的组件以及属性注入是否被代理或动态代理拦截。另一个容易被坑的是如果BeanPostProcessor在某个环节把原始对象替换成了代理对象你在PostConstruct里往原始对象存的某些状态可能在代理对象上根本不可见因为这完全是两个对象。6.4 手写一个简化的IoC容器需要几步很多人为了加深理解会尝试“手写Spring”。这个练习非常值得做它能把你对Bean生命周期的理解从“背概念”变成“能实现”。一个最简容器至少要能完成下面几件事扫描指定包下的类挑出带Component注解的类解析类的构造器依赖按类型匹配已有的Bean缓存已创建的Bean支持单例复用实现一个简单的getBean(Class?)方法你不需要真的实现三级缓存、AOP、事务管理但能把“扫描、注册、实例化、缓存、注入”这一条主链路跑通你对Spring的理解就已经超过很多只会用注解的人。这里我强烈建议每个搞Java后端的人哪怕不写框架也至少做一次这个练习。我自己当年做完之后回头再看Spring源码的doCreateBean根本不慌了因为心里有一条完整的“对象从无到有”的路径在指引排查方向。6.5 Spring Boot中常用的Bean注入控制手段在Spring Boot中注入控制主要体现在几个方面ConditionalOnProperty根据配置项决定是否创建BeanConditionalOnClass根据类是否在classpath中来决定是否创建BeanConditionalOnMissingBean容器里没有某个Bean时才创建默认实现Primary多个同类型Bean时声明优先选谁Qualifier指定Bean名称注入这几种条件装配机制在日常开发里非常实用。比如你要做一个“消息队列的默认实现”但当用户自定义了队列实现时默认实现就不该生效这时候用ConditionalOnMissingBean一句注解就搞定了。这类控制方式本质上是在BeanDefinition注册阶段做“是否注册”的判断而不是在创建阶段“拦截”。理解这一层你写出来的配置才不会是“凑巧能用”。7. 常见坑与排查思路整理7.1 循环依赖报错别急着加Lazy前面说了三级缓存能兜住setter注入导致的循环依赖但如果你用的是Spring Boot 2.6版本默认已经禁止循环依赖了——启动时直接抛BeanCurrentlyInCreationException。Spring Boot从2.6开始把spring.main.allow-circular-references默认改成false这是官方故意的为了逼开发者消除设计上的坏味道。如果不幸遇到循环依赖我建议按以下顺序处理先画出循环依赖的关系链找到最小闭环看能不能从设计层面拆掉这个环例如把依赖下沉、引入中间层确实拆不掉的考虑把构造器注入改成setter注入实在不行再加Lazy或者开启spring.main.allow-circular-referencestrue直接开全局开关是最后一步不建议一上来就干这事因为它会让容器在极端状态下工作埋下隐患。7.2 Async与循环依赖总会意外相遇Async注解的Bean如果与另一个Bean互相依赖即使都是setter注入也可能报循环依赖错误。原因是Async的代理模式比较特殊它需要提前生成代理对象并替换原始对象这个替换动作和三级缓存的提前暴露流程叠加在一起容易触发“Bean正在创建”的报错。网上有一种说法是给Async标注的Bean加Lazy去解决实测下来有效但要理解背后的代价——你只是延迟了问题并没有根治。如果系统里已经存在大量Async循环依赖建议优先重构成事件驱动模式把异步调用从直接依赖中抽离出来。7.3 手写测试时Bean注入不生效的常见原因很多人写单元测试时手写一个new UserService()然后发现里面用Autowired注入的Mapper是null。原因很简单没有Spring容器参与Autowired不会自动生效。你要么引入SpringBootTest让容器起来要么就手动set依赖——这侧面证明了为什么构造器注入更利于测试。还有一种情况是在工具类里封装了ApplicationContext静态引用但测试类没有初始化上下文就直接调用工具类的方法导致空指针。这种问题排查起来不复杂就是在测试里先把上下文准备好或者对工具类做空值保护。7.4 通过配置控制Bean初始化的经典场景配置文件中经常需要根据环境控制Bean的创建。最经典的是Dev环境和Prod环境下数据源、缓存中间件、第三方SDK的实现不同。用Profile注解可以轻松实现“多环境下注册不同Bean”但它与ConditionalOnProperty有差异Profile是按“环境标识”区分ConditionalOnProperty是按“配置项”区分。如果你是Spring Boot项目我推荐用ConditionalOnProperty来收敛“某配置打开时启用某Bean”这类逻辑语义更清晰。例如Bean ConditionalOnProperty(name app.push.enabled, havingValue true) public PushService pushService() { return new PushService(); }当配置项app.push.enabledtrue时这个Bean才会被创建。这在灰度发布、插件化设计里非常好用。8. 从原理到实战我的几条额外心得最后说点我自己的体会。第一不建议死记源码行号或者每个方法的调用路径。真正有用的东西是那些“为什么这样设计”的逻辑。比如说三级缓存你只要理解了“代理对象不能太早创建但循环依赖又要求提前暴露”你就不会再忘记三级缓存存在的意义。第二建议搭建一个最简单的Spring项目写两个互相依赖的类然后把日志级别调到DEBUG亲眼看看容器启动时输出了哪些日志。你会发现Spring在创建Bean的时候会打印Creating shared instance of singleton bean xxx在失败的时候会打印Bean with name xxx has been injected into other beans。这些日志不是废话它们就是排查循环依赖和Bean创建失败的线索。第三在生产排查线上问题上最实用的技能其实是“看堆栈”。当出现循环依赖、Bean创建异常、属性注入为null时别急着百度先定位到出问题的Bean名字然后再根据BeanDefinition、代码调用链、Autowired注解的注入目标一个个排除。这个过程不神秘就是利用“Bean生命周期”那张图做比对当前Bean走到哪一步了是哪一步出了问题只要你脑子里有一张清晰的Bean生命周期推进图排查效率会非常高。Spring Bean这套东西说穿了就是“容器负责生产对象、装配对象、管理对象”。搞懂了这套生产的全流程后面学Spring Boot自动配置、Spring Cloud的扩展机制、以及各种第三方Starter的接入原理都会轻松很多。希望这篇整理对你真正理解Bean的核心原理有实质性帮助。