
上一期我们把Spring IoC和DI的基本用法捋了一遍怎么配Bean、怎么注入、Component、Autowired、Configuration这些注解分别是什么、应该用在哪。但说实话能熟练使用这些注解和真正理解Spring容器中间还隔着一条鸿沟——只不过大多数项目里你不需要踏过去。直到某天遇到循环依赖报错、字段注入却拿到null、或者想自己写个通用日志注解时这条鸿沟才会把你绊个跟头。这一期我们往容器内部走一圈把Bean的一生、三级缓存、后处理器、自动装配的决策链条全部摊开。目标很直接从会用容器变成理解容器最后能做到手写一个像样的容器。适合已经能熟练使用Spring注解、但对容器内部机制停留在面试题背诵阶段的Java开发者。读完之后你再回去看Spring Boot的自动配置源码会发现很多之前看不懂的步骤其实都是这套基础机制的组合拳。1. 先从用到懂Bean在容器里的完整一生很多人用Spring写了好几年问到 Bean 是什么时候创建的、什么时候销毁的、容器里存的到底是一个对象还是一个模板答案就含糊了。这一节我们把这套生命周期完整走一遍。1.1 容器里真正存的不是对象而是BeanDefinition一个常见的误解是Spring容器启动时会立刻把所有单例Bean的对象new出来放进Map里。这个说法大方向对但漏了最关键的一步——容器里真正管理的第一等公民是BeanDefinition而不是对象本身。BeanDefinition可以理解成Bean的生产图纸。它里面记录了这个Bean的类路径、是否懒加载、scope是单例还是原型、初始化方法叫什么、属性值需要填多少、依赖哪些其他Bean。Spring启动时先通过各种方式XML解析、Component扫描、Bean方法把这套图纸收集齐全部注册进容器到了实例化阶段再拿着图纸去反射创建真正的对象。为什么要绕这么一层因为只有图纸先行才能解决依赖先后的问题。比如A依赖BB依赖C如果没有图纸阶段扫描到一个就创建一个扫描到A时B还不存在就会直接报错。有了BeanDefinition容器可以先把所有有哪些Bean、谁依赖谁的元信息收集完毕再统一安排创建顺序。这一层理解非常重要。后面讲BeanFactoryPostProcessor时你会发现Spring允许你在对象还没创建、只有图纸的阶段去修改图纸很多魔法比如配置文件占位符替换都是在图纸阶段完成的。1.2 实例化≠初始化这两件事差了十万八千里Bean的创建过程官方文档给了一长串生命周期回调核心可以拆成四段。第一段是实例化Instantiation。这一步Spring通过反射调用构造器new出一个对象来。注意此时对象只是一个空壳属性全是null依赖还没注入。如果你在这里打断点看到的对象是不完整的。第二段是属性填充Populate。Spring扫描这个Bean的所有字段发现标了Autowired、Value、Resource的就去找对应的依赖对象或配置值通过反射set进去。字段注入、setter注入、构造器注入本质都是在这一步或者构造阶段完成的。第三段是初始化Initialization。属性填充完毕Bean已经能正常工作了Spring再给你一些初始化后处理的钩子。常用的有三个优先级从高到低分别是PostConstruct注解方法、实现InitializingBean接口的afterPropertiesSet()方法、XML或Bean注解里配置的initMethod方法。实际项目中这三个混用的情况很少但你要知道它们的执行顺序。我见过有人同时用了PostConstruct和initMethod结果初始化的东西被覆盖排查了半天才意识到是执行顺序的问题。经验之谈新代码统一用PostConstruct简洁直观要兼容老框架的时候才去考虑后两种。第四段是销毁Destruction。单例Bean在容器关闭时销毁触发顺序和初始化相反先执行PreDestroy方法再执行DisposableBean接口的destroy()方法最后执行自定义的destroyMethod。这里有个特别容易忽略的坑原型prototypeBean默认不执行销毁回调。因为Spring只管创建原型Bean创建完就撒手不管了容器关闭时根本不知道哪些原型Bean还活着。如果你在原型Bean里配了PreDestroy做清理指望容器关闭时自动执行那基本等于白写。原型Bean的清理要么自己手动做要么用代理对象包一层。1.3 单例与原型不止是创建几次的区别表面上看singleton是容器里只有一个实例prototype是每次getBean都新建一个。但这两个scope背后牵动的机制差异远比想象中复杂。单例Bean容器会帮你管好整个生命周期创建一次、缓存起来、依赖注入、容器关闭时统一销毁。原型Bean则半管半不管每次创建都是走完图纸上的完整创建流程但销毁不归容器管。除此之外原型Bean里注入的单例Bean会正常工作单例Bean里注入一个原型Bean就有问题了——因为单例Bean只在创建时注入一次那个原型Bean在单例Bean的整个生命周期里都只有那一份每次调用拿到的都是同一个实例。如果你确实需要在单例中每次拿到不同的原型实例通常的解法是注入ObjectFactory或者Provider调用时再通过getObject()拿新实例。这也是方法注入Method Injection的一种平替方案比Spring官方推荐的Lookup更直观好理解。顺带提一句Lazy的作用。它不只是延迟到第一次使用时才创建很多时候它是用来打破依赖初始化顺序的。比如A和B互相依赖或者A初始化时要访问B的某个状态而B还没准备好给注入点加LazySpring会注入一个代理对象等你真正调用方法时才去触发目标Bean的创建和调用等于把依赖关系延后到了运行期。2. 循环依赖与三级缓存为什么三级缓存刚刚好循环依赖是Spring面试的高频题也是实际开发中绕不开的难题。这一节我们不说面试口诀直接从问题本质出发把这三个缓存的作用和边界讲透。2.1 循环依赖到底难在哪先看一个最典型的结构A里面注入了BB里面注入了A形成环形依赖。如果你用构造器注入写这段代码容器创建A时发现A的构造器需要B于是去创建B创建B时发现B的构造器需要A而A还在创建过程中、还没有成品于是直接报BeanCurrentlyInCreationException。这就是为什么Spring官方一直推荐构造器注入倒不是因为它比字段注入性能好而是因为构造器注入天然暴露循环依赖提示你代码设计有问题。但如果是setter注入或者字段注入情况就不一样了。创建A时先把A的实例new出来此时A是半成品依赖还没填接着填充B创建B时先把B的实例new出来填充A——这时发现A已经在创建中了而且有一个半成品实例可以直接用B就顺利完成B完成后回填到A里A也顺利完成。整个过程能够走通的关键在于Spring允许提前暴露半成品对象给其他Bean使用。2.2 从一级到三级每一级缓存解决什么问题第一个缓存是singletonObjects存的是真正创建完毕、属性全部填充好、初始化也完成的成品Bean。这是日常getBean能拿到的对象。第二个缓存是earlySingletonObjects存的是提前暴露的半成品Bean。所谓半成品就是这个对象已经被new出来了但属性还没填充完、初始化也没执行。它在解决循环依赖时负责提供中间态对象。第三个缓存是singletonFactories存的是ObjectFactory类型的工厂对象。很多人不理解三级缓存的意义既然二级缓存里已经有半成品了为什么还要三级缓存这一层工厂关键在于代理。如果A在创建过程中需要被AOP代理比如类上有Transactional或自定义切面那么B拿到的不应该是一个原始半成品而应该是一开始就被代理包裹的对象。但问题在于A的创建流程还没走到AOP代理那一步此刻直接给B一个原始对象后面代理逻辑执行完就白干了。三级缓存的ObjectFactory就是为了解决这个时间差——它内部保存了完整的创建回调在B真正向容器索要A时通过getEarlyBeanReference()提前执行代理逻辑生成代理对象放进二级缓存再把代理对象交给B。这样无论走到哪个阶段拿到手里都是正确的对象形态。所以三级缓存的设计本质是把创建普通对象和决定要不要生成代理对象这两件事解耦让代理决策可以延迟到真正有人需要引用的那一刻。没有循环依赖时代理逻辑正常走完整个初始化流程再生成有循环依赖时代理逻辑提前到半成品阶段执行。这就是为什么循环依赖处理是三级缓存而不是简简单单一张Map的原因。2.3 三级缓存的适用边界哪些循环依赖救不了三级缓存不是万能的至少三类循环依赖它解决不了。第一类是构造器注入的循环依赖。原因很直接构造器注入要求在Bean还没实例化之前就要拿到依赖对象而三级缓存暴露的是已经实例化完成的半成品。A的构造器需要BB的构造器需要AA连构造器都没执行完半成品根本不存在没有任何缓存能帮忙。解决思路只有两个改成setter或字段注入或者重新审视依赖结构把环拆掉。第二类是原型Bean的循环依赖。三级缓存的机制是建立在单例Bean缓存复用这个前提下的——半成品A之所以能被B引用是因为A最终只有一个实例提前暴露也不会产生多个实例。而原型Bean每次获取都创建新实例提前暴露一个半成品返回给调用方之后这个半成品却不会被缓存下来等于每次创建都失败。所以Spring对原型Bean的循环依赖直接拒绝处理。第三类是Async注解标注的Bean。Async会通过AOP生成代理对象由于代理的创建时机特殊加上Spring对这类代理对象有额外的状态判断涉及Async的循环依赖经常报错。经验做法带Async的Bean尽量不要作为循环依赖链上的一环把异步方法抽到单独的Service里结构更干净。还有一个容易踩的坑Spring Boot 2.6之后默认禁止了循环依赖启动时直接报错提示你设置spring.main.allow-circular-referencestrue才能放行。这其实是Spring官方在推进消除循环依赖的代码规范。老项目升级Boot版本后突然启动失败十有八九是这个开关。我的建议是存量项目先显式打开开关平稳过渡但新代码严格禁止写循环依赖宁可多抽一层Service。3. BeanFactory与ApplicationContext看清容器的两层皮肤容器这两个概念在面试里被反复问但很多人只是背定义。我换个角度从代码职责来拆。3.1 BeanFactory是容器本体ApplicationContext是增强版外壳BeanFactory是Spring IoC容器的根接口它定义了容器的核心能力getBean()拿对象、containsBean()查是否存在、isSingleton()判断scope、getType()获取Bean类型。就这么点东西没有花哨功能但一切容器能力都是从这个接口生长出来的。ApplicationContext继承了BeanFactory同时扩展了一大堆能力接口MessageSource国际化消息、ApplicationEventPublisher事件发布、ResourcePatternResolver资源加载、EnvironmentCapable环境配置读取等等。换句话说ApplicationContext是一个带更多基础设施的BeanFactory。你可以这样理解BeanFactory是发动机ApplicationContext是整车。发动机负责最核心的生产和管理对象这件事整车把发动机装进去再配上仪表盘、空调、音响这些扩展设施。大多数项目中你直接使用ApplicationContext但如果你想写一个极简容器从BeanFactory这个思路入手更容易看清本质。3.2 容器启动时到底做了几件事refresh()方法的隐藏故事ApplicationContext启动的核心在AbstractApplicationContext.refresh()方法里。这个方法看起来只是一个模板但每个步骤都有明确的意图。挑重点说四步。第一步prepareBeanFactory给容器配置默认的环境变量、类加载器、表达式解析器注册一些容器自身的基础Bean。第二步invokeBeanFactoryPostProcessors执行所有BeanFactoryPostProcessor对BeanDefinition做批量修改。这是一个非常关键的扩展点后面专门讲。第三步registerBeanPostProcessors把所有BeanPostProcessor注册到容器里。注意只是注册还没执行执行要等每个Bean创建时触发。第四步finishBeanFactoryInitialization这步最耗时。把所有非懒加载的单例Bean全部创建出来填属性、做初始化、处理AOP增强。项目启动慢通常就是这一阶段在大量创建Bean、构建代理、初始化数据库连接池。搞懂refresh()的流程你能解释很多现象。比如为什么某个BeanPostProcessor不生效答它没被实际注册上前。为什么启动时占位符${}没有替换答处理占位符的BeanFactoryPostProcessor执行阶段出了问题。建议有时间把refresh()的十几个方法逐个打一遍断点比读十篇源码分析文章都管用。3.3 手写一个迷你容器300行代码读懂依赖注入理解的最好方式是亲手写一遍。我建议你自己动手实现一个极简版本的IoC容器核心就三个组件一个Map存BeanDefinition图纸一个Map存单例对象一个getBean()方法负责按需创建。实现细节不复杂先扫描指定包下的所有类找到标了MyComponent注解的类生成BeanDefinition塞入图纸MapgetBean()时检查单例池没有就反射创建对象创建完扫描字段上标了MyInject的依赖递归调用getBean()取依赖并反射注入最后把对象放进单例池返回。写完之后你会发现循环依赖的问题在你这个迷你容器里根本处理不了因为你的容器没有三级缓存这一层。这时候你再回头看Spring的设计就会明白一级缓存解决复用二级缓存解决提前暴露半成品三级缓存解决代理和普通对象的统一口径每一步都不是多余的。市面上那些手写Spring的教程项目核心就是考你这套容器骨架把骨架构出来后面加注解、加AOP、加自动配置都是往上面挂肉。4. 在容器身上开刀两大后处理器决定了Spring的上限如果说BeanDefinition和缓存是容器的骨架那BeanFactoryPostProcessor和BeanPostProcessor就是容器的灵魂。Spring之所以能从一个反射工厂变成无所不能的生态底座靠的就是这两个扩展点。4.1 BeanFactoryPostProcessor在图纸阶段动手BeanFactoryPostProcessor的执行时机非常早在容器里所有Bean尚未创建、只有BeanDefinition图纸的阶段。它做的是对图纸本身进行修改。最典型的例子是PropertySourcesPlaceholderConfigurer它扫描所有BeanDefinition里属性值的${...}占位符从环境变量、配置文件、系统属性中取值并回填。再比如ConfigurationClassPostProcessor它负责解析Configuration、Bean、ComponentScan这些注解把它们翻译成真正的BeanDefinition。Spring Boot的自动配置本质上也是通过一个处理自动配置类集合的后置处理器在图纸阶段按条件装配出成百上千个BeanDefinition。自定义一个BeanFactoryPostProcessor非常简单实现接口在postProcessBeanFactory()方法里拿到ConfigurableListableBeanFactory遍历getBeanDefinitionNames()调用getBeanDefinition()拿到图纸修改属性或者调整顺序。有一个实用场景在启动时检查所有BeanDefinition给某个遗留的旧DAO统一设置默认的懒加载属性避免它每次启动都初始化连接池。这类全局性的体检和补丁放在这个阶段做最合适。一个非常重要的注意点BeanFactoryPostProcessor执行时绝大部分Bean还没创建所以你在里面不能调用getBean()去拿业务对象否则会提前触发对象创建打乱整个容器的创建顺序。想拿业务对象请用下面的BeanPostProcessor。4.2 BeanPostProcessor在对象诞生之后做手脚BeanPostProcessor的粒度到了单个Bean层面。它在每个Bean初始化前后各留了一个钩子postProcessBeforeInitialization()和postProcessAfterInitialization()。听名字就知道beforeInitialization是在初始化回调比如PostConstruct之前执行afterInitialization是在初始化完成之后执行。很多核心机制都挂在这两个钩子上处理Autowired的AutowiredAnnotationBeanPostProcessor处理Resource的CommonAnnotationBeanPostProcessor以及生成AOP代理对象的AnnotationAwareAspectJAutoProxyCreator。我用一个生活化的类比BeanFactoryPostProcessor是建筑图纸审核员在房子盖起来之前改图纸BeanPostProcessor是精装修师傅在房子框架完成之后根据住户需求给墙刷漆、装暖气、放大门让每个房子呈现不同的功能。没有这套精装修所有Bean都是白胚房。4.3 实战自己写一个日志注入器理解BeanPostProcessor的力量想看这东西到底能有啥用不如动手写一个。假设项目里大量Controller需要注入一个统一的OperationLogger对象用来记录操作日志。传统做法是每个Controller都写一个字段Autowired private OperationLogger operationLogger代码冗余还容易忘。用BeanPostProcessor可以把这件事彻底自动化。实现思路自定义一个注解AutoLogger标在某个类上写一个BeanPostProcessor实现在postProcessAfterInitialization()里检查Bean的Class上是否标了这个注解如果标了就反射创建一个OperationLogger实例用ReflectionTestUtils或者FieldUtils把它set进对应字段。这段代码总共不到40行。你用上之后再看Spring源码会发现所谓的Autowired处理、AOP代理生成原理完全一样只不过人家处理得更通用、更健壮。这就是理解容器和会用容器的分水岭你开始能够在容器创建Bean的过程中插入自定义逻辑了。我记得有一次需要在老系统中给所有标了某个自定义注解的Mapper统一做结果集加密当时就是在BeanPostProcessor的afterInitialization里对Bean做代理包装调用时先解密再返回。整个过程没有改任何业务代码只加了一个后处理器堪称中庸之道。5. 自动装配的决策链条Autowired到底在找什么很多工作三五年的开发碰到有两个同类型Bean报错时还只会加Qualifier但并不知道这背后Spring到底是怎么找的。这一节把决策链完整讲清以后面试再多聊几层都不慌。5.1 类型优先名字兜底byType与byName的配合逻辑Autowired默认是byType按类型查找。Spring拿到依赖的声明类型去容器里找所有类型匹配的Bean。这里分三种情况。情况一只有一个匹配Bean直接注入没任何问题这是绝大多数场景。情况二没有匹配Bean。Spring抛出NoSuchBeanDefinitionException。这里有一个绕不开的细节如果目标字段不是必须的可以给Autowired加requiredfalse这样找不到时字段保持null不报错。但我不建议日常使用因为会掩盖配置错误。情况三有多个匹配Bean。Spring并不立刻报错而是进入第二层判断——按名称兜底。如果字段名和某个Bean的name完全一致就命中那个Bean如果字段名匹配不上任何一个才抛出NoUniqueBeanDefinitionException。举个例子接口Animal有两个实现类Dog和Cat。某个Service里写了Autowired private Animal dogSpring会发现类型匹配有两个接着拿字段名dog去找正好有beanName叫dog那就把Dog这个Bean注入进去。如果字段名写成myPet两个都对不上才报错。理解了这层逻辑你就可以解释一些玄学现象为什么换个字段名代码就正常了为什么字段名叫animal就报错叫dog就不报错全部是byName兜底机制在起作用。5.2 Primary、Qualifier、Resource的取舍遇到多个候选Bean时有几种手段可以解决。Primary标注在某个实现类上表示它是同类型Bean中的首选。Autowired按类型查找发现多个时会优先选择标了Primary的那个。这适合默认选A、特殊场景选B的场景。但注意Primary只能解决一个首选如果两个都标了Primary冲突依旧。Qualifier是最精确的指定方式。Autowired下面再跟一行Qualifier(dogService)Spring直接按beanName去精确匹配。它的本质是把按类型按名字兜底的模糊查找变成按名字直接指定的确定性查找。和Qualifier配合时Primary的优先级反而不起作用因为Qualifier已经指定死了。Resource是JDK自带的注入注解行为和Autowired完全不同。Resource先按名称字段名查找找不到再按类型查找而且它不支持Primary。两个注解在项目里混用时很容易出问题同一个字段用Autowired不报错改成Resource就报错或者反过来。建议团队里统一用一个别混着来。我倾向于新项目统一用Autowired。还有一个容易被忽略的问题Autowired能用于构造器、setter、字段甚至方法参数Resource主要是字段和setter。如果类只有一个构造器且参数都带AutowiredSpring会自动认为你需要构造器注入连注解都能省略这是Spring 4.3之后的行为。新版项目推荐的注入方式其实是构造器final字段便于单元测试和防止字段被篡改但很多老项目习惯字段注入这个问题属于项目规范范畴想清楚再统一。5.3 从报错信息反推容器状态一套排查套路依赖注入失败时Spring的报错信息看起来很长但核心其实就几种。学会看报错遇到大部分注入问题都能快速定位。NoSuchBeanDefinitionException容器里压根没有这个类型的Bean。检查Bean是否真的被扫描到、Component/Service注解是否标了、包扫描路径是否覆盖。NoUniqueBeanDefinitionException类型匹配有多个名字兜底也没命中。打开报错信息里的available expected列表看看到底是哪几个Bean冲突了然后考虑Primary或Qualifier。UnsatisfiedDependencyException依赖注入不满足这是最外层包装异常真正的根因往往在caused by里。看到这种异常往下翻caused by比在开头瞎猜有效得多。嵌套异常可能指向构造器参数注入失败、循环依赖、或者某个依赖Bean初始化异常逐层往下剥洋葱就好。BeanCreationException创建Bean时出错了范围很广可能是初始化方法抛异常、事务代理生成失败、或者被前面的后置处理器拦截了。同样看caused by。我养成了一个习惯遇到DI报错第一件事是看异常信息末尾的caused by部分第二件事是查看Error creating bean with name xxx里xxx是谁第三件事是确认xxx的依赖路径。这样一个报错三分定位很少需要从头读到尾。6. 容器之外从Spring容器思维看系统设计的下一站学完容器内部机制我想最后把视野拉高一点。理解Spring IoC容器对理解更大范围的系统设计也有帮助现代技术栈里的容器概念其实共享同一套思维模型。6.1 Spring容器与操作系统容器的对应关系隔离与装配很多人一听到容器就想到Docker、K8s其实两者在抽象层面上惊人地相似。Spring容器的核心是管理对象的创建与依赖装配实现组件间的松耦合操作系统的容器Docker核心是管理进程的运行环境与资源隔离实现应用间的强隔离。前者的隔离单位是Bean后者的隔离单位是进程。进一步看对应关系Spring的BeanDefinition对应Docker的镜像配置文件都描述了要创建的东西长什么样Spring的单例池对应Docker的运行实例Spring的依赖注入对应Docker的网络与存储编排——容器A需要数据库不用自己装数据库驱动连接外部服务即可Spring的BeanPostProcessor对应容器平台的Init容器和Sidecar模式都是在主进程创建前后附加额外能力。如果你的思维能在这两个层面自由切换那么学K8s、学云原生会有一种豁然开朗的感觉因为它们讲的都是定义和装配这件事。反过来理解了Spring容器怎么管理依赖、怎么控制生命周期你在设计微服务架构时也会知道哪些依赖应该放在同一进程里用Spring管理哪些应该拆到不同容器里靠平台编排。6.2 容器化部署下的Spring配置经验说回实操。如果你的Spring Boot应用要部署到容器里有几点经验值得记一下。第一配置信息尽量通过环境变量注入而不是写死在application.yml里。数据库密码、API密钥这些敏感信息一旦被写进镜像整个镜像就没法安全分发。Spring对环境变量的优先级高于配置文件所以部署时用环境变量覆盖即可代码里保持占位符写法。第二健康检查端口和Actuator要配好。容器平台判断应用是否存活依赖的是健康检查接口Spring Boot的/actuator/health默认帮了你大忙但别忘了把Actuator依赖加上并暴露健康端点。第三JVM参数要在容器层面设置而不是写在应用的启动脚本里硬编码。容器平台可以通过参数调整堆内存写死了容易造成容器内存超限被杀。第四日志要输出到标准输出而不是文件。容器平台收集日志的标准方式是从stdout读Spring Boot默认日志输出到控制台所以这个习惯基本正确但如果你改了logging.file配置让日志写文件在容器里就找不到了。6.3 常见问题的定位清单启动慢、代理失效、配置不生效最后我整理了一份在容器化Spring应用里高频出现的问题清单每一条都对应前面的某个机制。启动慢先看是不是Bean扫描范围太大。SpringBootApplication默认扫描当前包和子包如果你把无关的依赖类也放进来容器启动时会创建大量用不到的Bean。用SpringBootApplication里显式指定扫描包或者把启动类放在项目根包下都能缓解。AOP代理失效优先检查是不是类内部调用了this方法。Spring的AOP基于代理代理对象会拦截外部调用但类内部this.xx()调用走的是原始对象切面根本不生效。解决办法是把调用改成注入自身代理或者拆到另一个Bean里。这个问题在Service内部方法之间互相调用尤其常见。配置不生效检查是不是环境变量命名和配置文件里的key不对应。Spring的环境变量映射规则比较复杂比如SERVER_PORT对应server.port但中间有个大小写转换细节。建议都测试环境下先跑一遍容器再上生产别依赖文档猜。6.4 进阶路线从手写Spring到读源码的路径如果你消化完这一期下一步的进阶路线建议这样走。第一步把手写迷你容器完整写一遍把BeanDefinition、单例池、字段注入、构造器注入都实现出来代码量很小但写完你对容器基本盘的认知就牢固了。第二步给迷你容器加上三级缓存解决循环依赖实现BeanPostProcessor机制。完成这一步Spring核心原理的70%你已经亲手验证过。第三步对着Spring源码读refresh()和getBean()的完整流程不用逐行读关注方法名和调用链即可。你会发现前面所有手工实现的内容都能和源码一一对上。第四步去看Spring Boot的自动配置源码理解Condition机制ConditionalOnClass、ConditionalOnMissingBean怎么在容器启动时动态装配Bean。这是你从Spring使用者变成Spring生态理解者的最后一跳。我自己当初就是靠这个路线学下来的。每一步都踩过坑但也正因为踩过坑之后再遇到Spring的疑难杂症脑子里天然有一个容器模型可以用来推理而不是靠搜索引擎碰运气。这也是这一期标题说从会用容器到成为容器的意思——容器不是黑盒它只是一套你终于看清了内部结构的精密机械。看清楚之后剩下的就是在合适的场景里恰当地使用它。