
1. 从“手动组装”到“自动装配”IoC与DI到底解决了什么问题先从一个几乎所有Java开发者都写过的场景说起。假设你在做一个订单服务里面需要调用用户服务和库存服务最常见的写法是这样public class OrderService { private UserService userService new UserService(); private StockService stockService new StockService(); }看起来没啥问题但项目一旦变大麻烦就来了。UserService 的构造函数变了比如新增了一个必须传递的参数你是不是得跑到所有 new 过它的地方去改OrderService 要换成 UserService 的另一个实现类是不是得重新改代码再重新编译更不要说单元测试的时候想给 OrderService 注入一个 Mock 对象结果发现它内部写死了 new你根本无从下手。这个痛点就是 Spring IoCInversion of Control控制反转和 DIDependency Injection依赖注入存在的根本原因。我先用一句人话把这两个概念讲清楚IoC 是一种设计思想把“创建对象、管理对象生命周期”的主动权从你的代码手里交给一个外部容器DI 是这种思想的具体落地手段容器在创建对象的时候自动把它所依赖的其他对象“塞”进去。你可以把 IoC 容器想象成一个“对象托管中心”。以前你自己开公司需要什么岗位的人就自己去面试、招人、签合同、发工资、离职还要办手续——这就是 new 一个对象和等它被 GC 回收的全过程。现在你把这些全部外包给猎头公司容器你只需要说“我需要一个懂用户业务的 Object”容器就会把符合要求的对象送到你手上你只管用就行它什么时候创建、什么时候销毁、中间怎么维护状态统统不用你操心。搞清楚这个底层逻辑之后后面看源码、配置 Bean、排查循环依赖你都会觉得顺理成章。这篇文章我会带你从零开始把 IoC 和 DI 的原理、三种注入方式、Bean 的生命周期、循环依赖三级缓存、以及实际开发中容易踩的坑全部过一遍中间还会穿插我踩过的坑和实际项目里的调试思路。2. 核心概念深度拆解容器、Bean、装配方式2.1 IoC 容器的工作流程其实和“点菜上菜”一样很多初学者把 IoC 容器想得很玄其实它的工作流程特别简单就三步加载配置、解析注册、注入使用。第一步是读取配置信息。这个配置可以是 XML 文件可以是注解也可以是 JavaConfig 类。Spring 会把这些配置统一解析成一个个 BeanDefinition——你可以把它理解成“对象的简历”里面写清楚了这个 Bean 的类名、作用域singleton 还是 prototype、是否需要懒加载、构造函数参数、属性值、初始化/销毁方法等所有信息。第二步是注册。Spring 把解析出来的 BeanDefinition 放进一个叫 BeanFactory 的注册表里。注意这里只是“登记在册”还没有真正创建对象。真正的实例化发生在 getBean() 被调用的时候或者容器启动完预实例化单例 Bean 的时候。第三步是注入。容器创建好对象之后会扫描这个对象依赖了哪些其他 Bean然后从容器里找到对应的实例通过构造器、Setter 方法或字段反射的方式装配进去。这个“找依赖并装配”的过程就是 DI。整个流程里最容易被忽略的是第一步和第二步之间的“BeanDefinition 覆盖规则”。同一个类型的 Bean 如果配置了多个默认是后面定义的覆盖前面定义的。我早期就遇到过这种情况在 XML 里配置了一个 DataSource又在配置类里用 Bean 返回了另一个 DataSource结果容器里最终生效的是配置类里的那个查了半天才发现是覆盖规则在起作用。2.2 Bean 的创建时机单例、原型、懒加载Spring 默认的 Bean 作用域是 singleton也就是整个容器里同一个 Bean 只有一个实例。Spring 容器启动的时候会默认把所有非懒加载的单例 Bean 全部提前创建好这叫“预实例化”。这样设计的好处显而易见启动阶段就把所有“零件”装配完毕后续请求阶段就不需要再做 Bean 的初始化了性能更好而且能尽早发现配置错误。比如你的 Bean 构造函数里抛了个异常如果没开预实例化可能要等第一次调用才报错线上排查起来很头疼。和 singleton 相对的是 prototype每次 getBean() 或者注入的时候都会创建一个新的实例。这种作用域适合有状态的对象比如一次请求内的临时业务对象不适合 Service、DAO 这种无状态组件。实际项目中 90% 以上的 Bean 都是 singleton我几乎只在多线程安全要求极严、或者需要隔离状态的场景下才用 prototype。懒加载Lazy的作用则是延迟初始化。加了 Lazy 的 Bean 不会在容器启动时创建而是第一次被使用的时候才创建。这个特性在解决启动时的循环依赖问题上有奇效——后面讲循环依赖的时候我会专门展开。2.3 依赖注入的三种姿势构造器、Setter、字段注入Spring 官方文档里其实强烈推荐构造器注入我一开始也照做了但随着项目越做越多现在我的使用习惯是分场景选择。构造器注入的优点非常明显依赖关系在对象创建的时候就固定下来了绝对不可变而且能保证 Bean 在创建完成后一定处于完整可用状态不会出现“对象建好了但依赖还没设进去”的半成品状态。单元测试也方便直接 new 的时候把 Mock 传进去就行完全不需要 Spring 容器参与。缺点就是当依赖太多的时候构造函数参数会非常多代码看起来比较臃肿。如果一个类的构造器参数超过四五个你要警惕了这很可能是类职责过重该拆分了。Setter 注入的好处是灵活可以在对象创建之后动态改变依赖而且一个类有多个依赖时你可以选择只注入其中几个。缺点是没办法保证依赖关系不可变而且如果忘记注入某个依赖运行时会莫名其妙出现空指针编译期根本发现不了。字段注入Autowired 直接打在字段上是现在很多初学者最喜欢的方式因为写起来最省事。但我不建议在新项目里大量使用这种注入方式原因有三个依赖对外完全不可见外人看这个类根本不知道它依赖了什么东西和 Spring 容器强耦合脱离容器就没法对类进行单元测试而且字段注入无法标记 final依赖关系是可变的给了后续代码被偷偷篡改的可能性。当然在测试类、配置类、或者一些临时脚本里用字段注入问题不大别全局泛滥就行。3. 从 XML 到注解Spring IoC/DI 的演进脉络3.1 XML 时代是怎么玩的我最早接触 Spring 3.x 的时候XML 配置还是主流。当时一个项目的配置文件动辄几百上千行写起来是真的折磨bean iduserService classcom.example.service.UserService property nameuserDao refuserDao/ property nameconfig valuedefault_config/value /property /bean bean iduserDao classcom.example.dao.UserDao/这套玩法的好处是配置和代码完全分离改依赖关系不用改 Java 源码运维和开发的边界很清晰。但缺点也肉眼可见配置冗长、Java 代码和 XML 文件之间跳来跳去容易眼花、编译期检查缺失一个拼写错误就要等容器启动才能暴露。当时还有个常见的坑是 XML 里配的类路径写错了类名少写一个字母Spring 启动直接报 ClassNotFoundException。这种问题写代码的人往往还发现不了因为 IDE 对 XML 里 string 类型的类路径默认是不做校验的。3.2 注解驱动组件扫描 自动装配Spring 2.5 开始推出注解但真正大规模普及是 Spring 3.0 之后。核心就是两个注解Component 及其衍生注解Service、Controller、Repository加上 Autowired。前者负责把类注册成 Bean后者负责自动注入依赖。ComponentScan 组件扫描是这个机制的关键。你告诉 Spring“去这些包下面找带有 Component 的类”Spring 就会扫描这些包及其子包把符合条件的类自动注册成 Bean。这里衍生的一个常见问题是Spring 扫描包路径的时候默认只扫描指定包下的类如果 Bean 所在的包不在扫描范围内就会出现 NoSuchBeanDefinitionException。排查这个问题的第一反应应该是看包扫描范围是否正确尤其是启动类的位置因为 Spring Boot 的启动类默认扫描的就是它所在包及其子包。Autowired 的查找逻辑也值得好好理解。默认情况下Spring 会先按类型byType匹配找到唯一一个就直接用如果按类型找到多个就再按字段名或参数名byName匹配还找不到就会报错。搞清楚这个优先级顺序很多“有两个实现类导致注入失败”的问题就迎刃而解了。我之前在一个多商户项目里写过这样的代码Autowired private PaymentService paymentService;当时 PaymentService 有两个实现类AlipayPaymentService 和 WechatPaymentService。容器按类型找发现了两个候选再按字段名去匹配发现字段名恰好叫 paymentService谁都对不上启动直接报 NoUniqueBeanDefinitionException。解决办法要么把字段名改成 alipayPaymentService不推荐太脆弱要么用 Qualifier(alipayPaymentService) 明确指定。如果你也遇到类似的报错不用慌原因八成就是这个。3.3 JavaConfig兼顾类型安全与可读性的现代方案Spring 3.0 引入了基于 Java 的配置方式用 Configuration 标注配置类用 Bean 标注配置方法Configuration public class AppConfig { Bean public DataSource dataSource() { return new HikariDataSource(); } }这种方法最大的优势是类型安全。方法返回类型是编译期确定的写错了 IDE 直接报错不用等容器启动。而且可以在配置方法里写任意逻辑比如根据运行环境创建不同的 DataSource用 if-else 就能实现比 XML 里搞 profile 配置灵活得多。实际开发中现在最常用的组合是 Spring Boot 的自动配置 组件扫描 JavaConfig。XML 并不会完全绝迹但大多只出现在老系统维护、或者某些特殊场景比如第三方 jar 包内嵌的 XML 配置里。4. 手写一个精简版 IoC 容器彻底吃透 XML 到实例的流转4.1 为什么写了五遍 Spring 源码还是觉得没看懂我一直觉得真正理解一个框架最好的方式不是反复看别人写的源码讲解而是亲手把它最核心的机制复刻一遍。Spring 的 IoC 容器源码动辄上万行硬啃很容易迷失在层层抽象里。但它的骨架其实很稳定就四个环节解析配置、放入注册表、按照定义创建实例、创建时完成依赖注入。我自己在纸上和 IDE 里写过好几版 IoC 容器原型代码量不大但写完之后对 Spring 的敬畏感和理解都上了一个档次。下面把我写过的其中一版的核心逻辑分享出来专门演示 XML 到实例的流转过程。4.2 实现思路注册表 反射 递归注入先定义最简单的注册表public class SimpleBeanFactory { private MapString, Object singletonObjects new ConcurrentHashMap(); private MapString, BeanDefinition beanDefinitions new ConcurrentHashMap(); }BeanDefinition 里至少要保存类名、是否单例、依赖的属性名列表。解析 XML 的时候用 JDK 自带的 DOM 或者 SAX 把 标签读出来填充到 beanDefinitions 里。创建实例的核心逻辑分两步。第一步是反射实例化Class? clazz Class.forName(beanDef.getClassName()); Object instance clazz.getDeclaredConstructor().newInstance();第二步是遍历当前 Bean 的属性依赖递归调用 getBean()如果依赖的 Bean 还没创建就先去创建它然后通过反射调用 setter 方法把依赖塞进去。这一步本质上就是 DI。4.3 递归注入时的顺序陷阱写这个手写版容器时我踩过一个很有意思的坑如果 Bean A 依赖 Bean B、Bean B 又依赖 Bean A递归调用 getBean() 就会无限循环下去最终栈溢出。当时我的解决办法是加一个“创建中”的标记集合getBean() 进来时先判断当前 Bean 是不是已经在创建中集合里了如果是就直接抛异常提示循环依赖。这个朴素的处理方式让我意识到Spring 处理循环依赖用的“三级缓存”原理其实也是为了打破这种死循环而设计的——只不过它做得更巧妙不是直接报错而是提前暴露早期引用。所以看到 Spring 循环依赖相关的面试题时我一点都不慌因为我知道容器在“创建中”状态到底发生了什么。这个手写版虽然简陋但把 IoC 的“注册、创建、注入”三大动作完整跑了一遍每次写完后你对 Autowired 背后发生的事都会有更具体的画面感。5. 循环依赖与三级缓存Spring 进阶绕不开的硬核机制5.1 什么是循环依赖为什么构造器注入能天然规避循环依赖字面意思就是 Bean 之间互相依赖。A 依赖 BB 又依赖 A形成了一个环。这在复杂的业务系统里其实很难完全避免比如订单服务和物流服务互相查询对方的接口。Spring 解决循环依赖的思路并不是“没有办法解决就硬解”而是分情况处理。构造器注入的循环依赖无解因为创建 A 的时候需要传入 B 的实例创建 B 的时候又需要传入 A 的实例两边都无法先创建一个不完整的对象容器会直接抛出 BeanCurrentlyInCreationException。字段注入和 Setter 注入的循环依赖则可以解决因为对象可以先被创建出来属性的值暂时为空之后再把依赖填充进去。这就是为什么很多对 Spring 理解深入的人在实际开发里更倾向于字段注入或 Setter 注入的原因之一它们天然对循环依赖友好。当然这并不代表你应该刻意制造循环依赖架构上还是要尽量避免环的存在但理解这个特性有助于你在面对一些历史遗留代码时知道它能跑起来的原因。5.2 三级缓存到底在缓存什么Spring 内部维护了三级缓存来解决“提前暴露早期引用”的问题每一级的名字和作用如下第一级缓存singletonObjects存放已经完成完整初始化、可以直接使用的单例 Bean。第二级缓存earlySingletonObjects存放已经完成了实例化、但还没完成属性填充的“早期单例对象”即半成品 Bean。第三级缓存singletonFactories存放 ObjectFactory 对象通过它可以在需要的时候提前生成 Bean 的代理引用。很多人背了这三级的名字却不理解为什么需要三级而不是两级。要理解这个问题关键要抓住“AOP 代理创建时机”这个点。假设 A 依赖 B、B 依赖 A同时 A 需要被 AOP 代理。如果只有两级缓存存原始对象 最终对象那么当 B 在创建过程中引用 A 的时候A 还没有经过代理的步骤B 拿到的是一个没有被代理的普通 A 对象。之后 A 再被代理但 B 持有的依然是那个旧的原始对象代理就失效了。为了解决这个时序问题Spring 引入了第三级缓存。当 A 实例化后会提前把一个 ObjectFactory 放入第三级缓存。B 在创建过程中通过第三级缓存的 ObjectFactory 去获取 A这个 factory 会根据 A 是否需要 AOP 而决定返回“原始对象”还是“动态代理对象”。如果 A 需要 AOPB 拿到的直接就是代理对象等到 A 真正初始化完成第一级缓存里存的对象和 B 拿到的也能保持一致性。这就是三级缓存存在的深刻原因。5.3 实际排查循环依赖的经验我在现网遇到过几次循环依赖报错报错信息里一般会打出当前正在创建的 Bean 名字但很多时候不会直接告诉你环由哪几个 Bean 组成需要自己顺着依赖图找。我的排查思路是先从报错信息里拿到那个“正在创建”的 Bean 名字然后去它的属性里看有没有依赖别的 Bean一层层往下追直到追回来形成一个闭环。画脑图或者直接打印依赖关系都行核心是把环找出来。找到环之后有三种处理方案。第一种也是最推荐的就是从架构层面打破循环依赖把互相依赖的逻辑抽出来放到第三个组件里或者改成一个单向依赖的模式。第二种如果确认是 Setter/字段注入的循环依赖可以直接加 Lazy 注解让其中一个 Bean 延迟加载这样能绕开启动时的循环依赖检查。第三种在极少数情况下用 setter 注入把环保持住但代价是代码的可维护性变差不推荐长期保留。这里有个非常重要的提醒从 Spring Boot 2.6 开始官方默认禁止了循环依赖。如果项目是从 2.5 及以下版本升级上来的启动时可能会因为循环依赖直接报错需要把 spring.main.allow-circular-referencestrue 配回去才能恢复旧行为。但我不建议你直接加这个配置更好的做法是找到循环依赖存在的根因并重构掉否则带病运行迟早出事。6. Autowired 深入类型匹配、多个候选、必需属性6.1 Autowired 的三个核心查找规则Autowired 的注入逻辑可以总结成三条查找规则按优先级依次执行第一按类型byType查找。容器里只有一个匹配的 Bean直接注入成功。第二按名字byName查找。按类型找到多个候选 Bean 的时候会根据字段名或参数名再匹配一遍命中唯一一个则注入成功。第三处理多个候选的歧义。如果按名字也找不到就需要 Primary 或 Qualifier 介入否则抛 NoUniqueBeanDefinitionException。举一个实战例子。项目里定义了接口 MessageSender有 SmsMessageSender 和 EmailMessageSender 两个实现都注册成了 Bean。Component public class NotifyService { Autowired private MessageSender messageSender; }此时 Spring 按类型找到了两个候选按名字再找 messageSender 这个名字又对不上任何一个实现类名就会启动报错。如果你的字段名恰好叫 smsMessageSender那 Spring 按名字能找到 SmsMessageSender直接注入成功。这个“命名碰巧”看起来很巧妙但可读性和可维护性都很差不推荐依赖这种写法。6.2 Primary、Qualifier、Resource 怎么选遇到一个接口多个实现的场景我总结的选型建议是这样的Primary适用于“大多数时候我就想用这个实现类”的场景。比如在项目里只有一份默认的 RedisTemplate 或 ObjectMapper 时Primary 能避免到处都是 Qualifier。注意 Primary 只能标注一个实现多个 Primary 依然会报错。Qualifier适用于不同场景用不同实现的场景。可以把 qualifier 理解成给 Bean 起了一个可区分的别名注入时显式指定语义清晰。Resource是 JSR-250 的标准注解和 Autowired 最大的区别是它默认按名称byName查找如果找不到再按类型。在一些国产化、去 Spring 化的项目里Resource 的兼容性更好因为它是 Java 标准规范被 Spring 支持的同时也被很多其他容器支持。我自己写新代码时更倾向用 Autowired Qualifier 的组合因为 Autowired 支持 requiredfalse允许注入为 null以及 Primary 的同理心整体语义更贴近 Spring 生态。如果是做通用组件库或者中间件我会优先用 Resource避免和 Spring 的注解深度耦合。6.3 requiredfalse 与 Optional 注入的妙用默认情况下 Autowired 是“必须注入成功”的容器里找不到对应 Bean 就直接启动失败。这是好事能防止配置遗漏。但有些场景确实需要“找不到就算了”这时有两个选择Autowired(required false) private MessageSender messageSender; // 或者用 JDK8 的 Optional 包装 Autowired private OptionalMessageSender messageSender;两者都能在容器里没有 MessageSender 时让应用正常启动。区别在于 Optional 的语义更明确代码里一眼就能看出这个依赖是可选的而且可以优雅地做空判断比判断字段是否为 null 更安全。我一般只在插件化架构、功能开关、可选扩展点这类场景下用 optional 注入核心业务依赖一律保持默认的强依赖不让应用在“少了关键组件”的情况下带病启动。7. Bean 的生命周期从定义到销毁的完整旅程7.1 生命周期各阶段梳理Spring 中一个单例 Bean 的完整生命周期可以分为这么几个阶段按顺序记忆会更清晰阶段一解析和注册。容器读取配置生成 BeanDefinition 并登记。阶段二实例化。通过构造器或者工厂方法创建对象实例。此时对象已经存在但属性还没赋值。阶段三属性填充。也就是依赖注入发生的地方。容器根据 BeanDefinition 中的属性配置调用 setter 方法完成赋值。如果实例化后的 Bean 实现了 Aware 接口比如 BeanNameAware、ApplicationContextAware也会在这个阶段回调把 Bean 的名字、容器引用等信息告诉它。阶段四初始化。如果有 BeanPostProcessor会先执行 postProcessBeforeInitialization 方法然后调用 PostConstruct 标注的方法或者初始化方法 init-method最后执行 postProcessAfterInitialization 方法。AOP 代理的创建就发生在这个阶段这也是为什么循环依赖需要三级缓存来提前暴露代理的原因。阶段五使用。Bean 处于就绪状态可以被正常注入了。阶段六销毁。容器关闭时如果 Bean 实现了 DisposableBean 接口或者标注了 PreDestroy 方法都会被执行用于释放资源、关闭连接池、清理线程池等。7.2 BeanPostProcessor 为什么是 Spring 的“神级扩展点”如果你认真看 Spring 源码会发现整个框架的很多核心功能都是靠 BeanPostProcessor 实现的。比如 Autowired 的注入逻辑就是一个叫 AutowiredAnnotationBeanPostProcessor 的处理器在 postProcessPropertyValues 阶段完成的Async、Transactional 等等也是靠后置处理器生成代理的。所以你也可以把 BeanPostProcessor 理解成“Bean 从创建到使用之间的一道自定义关卡”。你可以在 postProcessBeforeInitialization 阶段偷偷替换掉 Bean 对象也可以在 postProcessAfterInitialization 阶段给 Bean 加上代理逻辑。这给了你极强的扩展能力比如在 Bean 初始化之后自动打印耗时日志、自动给特定类型的 Bean 加监控埋点。写自定义 BeanPostProcessor 时有个坑要特别注意BeanPostProcessor 本身是特殊的 BeanSpring 会在普通 Bean 创建之前就完成它的实例化。如果你的 postProcessor 注入了一个普通 Bean而那个普通 Bean 恰好又要依赖这个 postProcessor就很容易绕进循环依赖的坑里。7.3 初始化顺序的实战排查我在排查线上问题的时候经常会被问到这个顺序问题实现了 InitializingBean 接口、写了 PostConstruct 方法、又配置了 init-method到底哪个先执行答案是这样的PostConstruct 先执行然后才是 InitializingBean 的 afterPropertiesSet 方法最后才是 init-method 中指定的自定义方法。这个顺序在网上很多文章里都有提及但真实项目里容易忽略的是如果你同时用了 Spring 的 PostConstruct 和 JSR-250 的 PostConstruct注意只有一个生效两个注解在不同容器里的语义可能不完全一致。再加上 BeanPostProcessor 的 before 和 after 方法实际执行的链路比很多人想的要长很多。我给一个实用的排查思路在生命周期各阶段的关键回调里打点日志加上当前线程栈。这样能快速看清每个 Bean 的创建顺序和依赖关系排查初始化失败问题的时候比盲猜效率高得多。8. Spring Boot 场景下的 IoC/DI自动配置与常见开发姿势8.1 Spring Boot 怎么把 IoC 玩出花来的Spring Boot 的“自动配置”本质上还是 IoC 思想的一种极致应用。你引入一个 spring-boot-starter-webSpring Boot 就会通过 EnableAutoConfiguration 加载一大堆 XxxAutoConfiguration 配置类在配置类里面条件装配了大量默认 Bean。比如引入 Redis 依赖后Spring Boot 会判断你还没有手动定义 RedisTemplate就会自动注册一个默认的 RedisTemplate 给你用实现“开箱即用”。理解自动配置的关键是看 Conditional 系列注解。ConditionalOnClass、ConditionalOnMissingBean、ConditionalOnProperty 这些注解控制了“什么时候装配这个 Bean”。遇到自动配置不生效的情况优先去查这些条件是否满足而不是一上来就怀疑 Spring Boot 坏了。调试的时候可以在 application.properties 里打开 debugtrue启动日志里会明确告诉你“哪些自动配置生效了哪些被跳过以及原因是什么”。写自定义 Starter 或者封装公共模块的时候同样会用到自动配置。我建议把自动配置类和业务代码分开包路径避免被应用自身的组件扫描误扫到导致容器里出现两份一样的 Bean。8.2 实战用 ConfigurationProperties 批量注入配置值IoC/DI 在 Spring Boot 里最常见的应用场景之一就是把配置文件的属性批量注入到一个配置 Bean 里替代传统的 Value 一个字段一个字段地取。这样既干净又类型安全还能把复杂配置集中管理。Data Component ConfigurationProperties(prefix shop.payment) public class PaymentProperties { private String appId; private String privateKey; private String notifyUrl; }使用的时候把 PaymentProperties 注入到服务里Service public class PaymentService { private final PaymentProperties properties; public PaymentService(PaymentProperties properties) { this.properties properties; } }这里体现的是构造器注入的标准姿势字段用 final 修饰依赖不可变测试时直接传参。ConfigurationProperties 批量绑定比 Value 的优势在于多个相关配置项集中管理、IDE 能提供属性提示、并且支持类型转换和校验。在实际多环境中我还常常把前缀做成带环境名的比如 production.payment.url 和 test.payment.url配合 profile 灵活切换配置代码完全不用改。8.3 Spring Boot 启动时 Bean 装配失败的常见排查路径不管经验多丰富总会遇到“启动报错找不到某个 Bean”的情况。我总结了一套百试不爽的排查路径第一步看报错的完整堆栈找到是哪个类在注入时出了问题。如果错误信息带有 NoSuchBeanDefinitionException多半就是依赖的 Bean 没被注册。第二步确认这个 Bean 是不是真的存在。如果是自定义类检查有没有加 Component 或 Service 注解如果是通过 XXXAutoConfiguration 装配的检查自动配置生效条件。第三步检查包扫描路径。Spring Boot 启动类默认只扫描所在包及其子包如果 Bean 写在扫描范围之外容器永远找不到。把启动类移到项目根包下或者手动指定 scanBasePackages 可以解决。第四步如果确实有多个同类 Bean但没加 Qualifier启动报 NoUniqueBeanDefinitionException。按 6.1 节的规则去补 Qualifier 或 Primary 即可。第五步如果所有条件都正常检查是不是存在循环依赖以及 Spring Boot 2.6 之后是否在配置中显式放开了循环引用。如果是历史项目升级先看 release note 里的破坏性变更。这套步骤我走下来绝大多数启动装配问题都能在十分钟内定位。真正麻烦的反而是那种只在你本地能复现、测试环境不报错的诡异问题这时候往往和环境差异、配置生效顺序有关按照生命周期和自动配置条件逐一排查多半能水落石出。9. IoC 和 DI 的边界和 AOP、工厂模式的真实关系9.1 IoC 不只是“对象工厂”很多人把 IoC 容器和工厂模式混为一谈觉得 IoC 无非是“用工厂方法创建对象”。这种理解不能说全错但确实失之偏颇。工厂模式解决的主要是“创建逻辑的封装”帮你把“创建什么”和“怎么使用”解耦但 IoC 容器管的事远不止创建还包括了一整套生命周期管理、依赖解析、作用域管理、代理植入、事件发布等能力。一个典型的对比是你自己写个工厂类你依然需要知道“我需要 ProductA 的时候要调用 FactoryA”但用 IoC 容器你只需要声明“我需要一个 Product 类型的依赖”容器会根据类型、名称、主备策略、条件注解自动做出决策。而且 IoC 容器还会在背后帮你处理很多你根本没意识到的事——比如 Bean 的懒加载、AOP 代理的注入、Bean 销毁时的资源释放。9.2 IoC 给 AOP 提供了什么基础AOP面向切面编程一直和 IoC/DI 被并称 Spring 的两大核心。但它们不是并列关系而是“IoC 为 AOP 提供了运行时基础”的关系。AOP 动态代理要起作用前提是 Bean 的创建权在容器手里——容器在创建完目标 Bean 之后可以利用 BeanPostProcessor 机制生成代理对象并把这个代理对象作为最终的 Bean 暴露给使用者。如果你的对象都是自己 new 的、不经过容器AOP 根本无从下手。理解了这个关系再回头去看 Spring 的动态代理就有种豁然开朗的感觉为什么 Transactional 只对容器管理的 Bean 有效为什么自调用 this.method() 会让事务、缓存注解失效因为代理对象存在于容器中而 this.method() 调用的是目标对象的原始方法根本没有走代理链路。这也是 IoC 容器设计中“所有 Bean 必须统一经过容器代理”的根本原因。我在项目里遇到过一个经典案例一个 Service 类里方法 A 调用方法 BB 上有 Transactional 注解但调用后事务没生效。排查后发现问题出在 A 内部用 this.b() 直接调用而不是通过注入的代理 Bean 调用。改法很简单要么把 B 拆到另一个 Bean 里要么通过 ApplicationContext 获取代理对象再调用。理解了代理与容器之间的关系你就能一眼看出这个问题的本质。9.3 手写 IoC 容器时AOP 的最小实现思路如果你想进一步加深理解可以试着在自写的简易 IoC 容器里实现一个接口级别的 JDK 动态代理。核心逻辑就是在 Bean 创建并初始化完成后判断它是否匹配切点规则如果匹配就用 Proxy.newProxyInstance() 生成一个代理对象替换掉原来要放入缓存的原对象。这样调用方拿到的就是代理对象在方法调用前后就可以插入日志、权限校验等逻辑。这个过程很直观地展示了 IoC 容器如何通过一个扩展点BeanPostProcessor完成拦截逻辑和业务逻辑的解耦。写完之后你会对“Spring 借助 IoC 容器管理代理注入”有更深的理解以后再看到 Transactional 或 Async 失效的面试题也能从容分析风险点。10. 知识自查清单把这些点过一遍才算真的懂 IoC 和 DI我用这套思路带过不少新人每次培训结束都会给一个自查清单这里也分享给你。你可以逐条检验自己是否真的掌握了 Spring IoC/DI能不能用一句话说清楚什么是控制反转什么是依赖注入两者什么关系能不能画出 Bean 从解析配置到初始化完成再使用的完整时序关键回调在哪个阶段执行知不知道构造器注入和 Setter 注入在循环依赖场景下的区别为什么构造器注入无法解决循环依赖循环依赖三级缓存的每一级各自存放什么AOP 代理创建时机和三级缓存的关系是什么Autowired 按类型、按名称匹配的完整规则是什么多个候选 Bean 时如何优雅解决歧义Primary、Qualifier、Resource 三者的使用场景和优先级如何选择BeanPostProcessor 在整个生命周期里处于什么位置为什么它是 AOP 和 Autowired 的基石Spring Boot 自动配置的原理是什么看到一个自动配置不生效第一步该查什么对象自己 new 和交给容器管理在 AOP 场景下有什么本质区别这九条如果你都能不翻资料、用自己的话讲清楚面对大部分 Spring 相关的技术面试和日常开发问题基本都不会卡壳。如果在某一条上卡住了建议回到上面对应的章节带着问题重新读一遍相关的源码和配置会比单纯背答案有效得多。最后再补充一句技术这东西只有自己动手写出来、踩过坑、排查过问题才能算真正长在自己身上。