你写的「好代码」可能正在坑自己先别急着记定义。看一段你八成写过的代码OrderService里直接private UserService userService new UserService();。依赖在类内部写死OrderService 和 UserService 从出生就绑在一起。问题很快就来你想单测OrderService的订单逻辑结果UserService也跟着跑——它可能连数据库、发消息。你本来只想测一行订单计算却被迫把整条链路拉起来。单元测试名存实亡。更麻烦的是替换。哪天要把UserService换成远程调用版或者测试时想换个 Mock 实现你得改源码、重新编译。这种写法改一处牵全身。一句话把 IoC 说清楚IoC 全称 Inversion of Control控制反转。说人话就是把「对象谁来创建、依赖谁来装配」这件事从你的代码手里交还给框架容器。概念出处官方文档 · The IoC Container以前是你主动new对象的一生你亲自管现在是你在类里只声明「我需要什么」容器在启动时把东西准备好、主动塞给你。这不是新语法只是责任的搬家——你的代码从「我来做」变成「你给我」。控制反转反转的不是技术是「谁说了算」这件事。DI 是 IoC 的「实现姿势」之一IoC 是思想DIDependency Injection依赖注入是它的落地方式之一。容器把依赖「注射」进你的对象而不是让你自己去new。DI 模型出处官方文档 · 依赖注入注入有三种常见姿势但真正要懂的是它们各自的代价构造器注入官方首推依赖作为参数传进来字段可以标final对象一旦建好就完全初始化、不会出现「用到一半依赖还是 null」的状态。更关键的是——循环依赖在启动期就会被炸出来BeanCurrentlyInCreationException而不是等到某次请求偶发 NPE。官方文档在「依赖注入」一节对比了构造器与 setter 注入并给出推荐。setter 注入依赖可选、运行时可换但对象可能处于「半成品」状态被使用线程安全得自己兜。字段注入Autowired直接标字段写着最爽坑也最深。① 靠反射直接写私有字段绕过构造器封装被破坏② 字段不能final状态可变性失控③ 不启动容器就注入不了纯单元测试没法new出来直接测必须靠 Spring 或反射手动塞④ 它还会掩盖循环依赖——setter/字段注入下容器用三级缓存兜底问题被藏起来直到某天改成构造器注入才爆雷。Autowired 出处官方文档 · Autowired一句话能写构造器注入就别写字段注入。字段注入是「开发爽三天排错哭三夜」。循环依赖怎么发生的、构造器注入为何能提前暴露它——这都是字段注入的经典陷阱。三十秒验证可测性差在哪说法要能落地才算懂。同一个OrderService两种注入写法在单元测试里的差别// 构造器注入不启动容器new 出来就能测Mock 直接传classOrderServiceTest{Testvoidshould_calculate_total(){UserServicestubmock(UserService.class);// Mockito 手动构造when(stub.getLevel(1L)).thenReturn(VIP);OrderServiceservicenewOrderService(stub);// 纯 Java毫秒级assertThat(service.calcTotal(1L)).isEqualTo(90);}}// 字段注入测试代码写不出来——字段是 private 且没有 setter// 不上反射/反射工具ReflectionTestUtils就塞不进去// 而一旦依赖变成 5 个每个测试类都要复制 5 行反射塞值代码这就是「不启动容器就注入不了」的具象化构造器注入的测试是纯 Java 测试千个用例秒级跑完字段注入的测试要么拉起 Spring 上下文慢一个数量级要么反射到处飞脆。顺带一提Java 的record天然只有全参构造器——用 record 表达不可变协作对象时构造器注入是唯一选项这也是官方推荐在语言层面的一种呼应。顺带澄清DI 不是 Service Locator很多人把「向容器要东西」都叫 DI其实有个常被混淆的近亲——Service Locator。Service Locator对象自己向容器伸手userService context.getBean(UserService.class)。依赖藏在方法体里外部看不见想测就得先造一个容器。DI依赖由容器从外部推入对象被动接收。你的依赖写在构造器签名上看得见、可测、不污染业务代码。一句话Service Locator 是「我自己去拿」DI 是「你递给我」。Spring 以 DI 为核心装配模型官方文档 · Core TechnologiesService Locator 的辨析属通用设计模式补充非官方某节专门对比。看图就懂new 写法 vs 容器注入左边是硬编码时代OrderService自己new出UserService两者是「父子绑定」的关系换不掉、测不动。右边是容器时代ApplicationContext居中对象的创建和装配全归它管。它把UserService实例注入到OrderService的构造器里。三个角色各管各的想换实现、想 Mock都只动配置不动业务代码。底层一句话依赖到底怎么「打」进去的容器启动期读取你的类上构造器 /Autowired的元数据用反射把匹配类型的 Bean 实例化再传进构造器或设进字段。所以你的构造器参数列表就是一份依赖契约——这也是为什么构造器注入让依赖一目了然而字段注入把契约藏进了注解里。更深的反射调用的时机、三级缓存怎么兜底、BeanPostProcessor 在哪介入属于容器内部机制这一篇先把直觉建起来。追问链再往下会被问什么这篇的结论通常会引来三个追问先把答案的「钩子」备好「字段注入这么差为什么身边全是它」——历史惯性 IDE 模板 Autowired写起来最短。但 Spring 官方自己的代码与文档示例几乎全用构造器注入Boot 2.6 起还默认禁止循环依赖spring.main.allow-circular-referencesfalse方向已经很明确。「一个类几十个构造器参数是不是说明构造器注入不行」——恰恰相反构造器参数爆炸会把「单一职责被破坏」暴露在签名上字段注入把它藏起来而已。治理的是类不是注入方式。「Autowired 标在字段上到底发生了什么」——容器在后置处理阶段用反射field.set()强行写入私有字段连final语义都能被反射绕过部分场景这就是「封装被破坏」的技术含义。它到底帮你省了什么解耦依赖靠接口说话换实现不动业务代码。可测单元测试给个 Mock 就行不用拉起整条链路。集中管理对象的创建、生命周期、作用域都在容器里一处配置全局生效。一句话收束IoC 把「怎么造对象」这件脏活接走了让你把精力放在**「做什么」**而不是「怎么造」。这正是一个框架该有的样子。先别背记住这几句就够如果今天只带走几句话IoC 是「控制权交出去」DI 是「依赖从外面打进来」DI 不等于 Service Locator前者推、后者拿能写构造器注入就别写字段注入。三者一体两面别再分开背。 版本与出处说明本文基于Spring Framework 7.x 官方 Reference查阅时文档版本 7.0.102026-10撰写核心概念均对照官方文档提炼正文关键处标注了对应官方小节文末汇总链接。官方文档若改版以官网最新版为准。