Spring 到底是什么:从 EJB 之痛到 IoC 的诞生
一句话:Spring 是一个「帮你造对象、递对象、管对象」的容器。 你以前写 Java,要用谁就自己 new 一个;用 Spring 之后,你只写一句「我需要一个能查订单的东西」,剩下的「从哪来、怎么拼、什么时候销毁」全由容器安排。这一篇讲它为什么会长成这样:2003 年的 Java 圈被 EJB 那套「仪式代码」压得喘不过气,Rod Johnson 用一本技术书证明「普通 Java 类 + 依赖注入」就能干同样的活,Spring 由此出生,后来又长出 Boot(替你省配置)和 Cloud(管一堆服务)。
点外卖就是控制反转。 你想吃一盘番茄炒蛋,传统写法是自己买菜、洗菜、切菜、掌勺——你既当食客又当厨房;点外卖只需要下单(声明「我要这道菜」),火候、食材、装盒全由商家(容器)决定。商家哪天换厨师、换锅、换包装,你的订单一个字都不用改。所谓「反转」,反的就是这件事:做菜的主导权从你手里转到了商家手里。

自动售货机就是依赖注入。 你只按货道编号(构造器里写「我要 OrderRepository」),机器负责把饮料送到取货口——它从哪个仓库补货、里面是哪一批货、什么时候下架,你一概不用管。换成新机器(换实现)也不用改你的按钮。
学完这一篇,你要能回答三个问题:
- EJB 到底难用在哪?为什么说 Spring 的出发点是「无侵入」?
- 「控制反转」听起来玄,具体反转了哪四件事?它和「依赖注入」是什么关系?
- 已经会用 Spring Boot 了,为什么还要回头学 Spring 容器?
要理解 Spring 为什么伟大,得先知道它拯救的是什么。二十多年前,Java 企业开发的标准答案是 EJB(Enterprise JavaBeans)。它看起来很正规,写起来却很痛苦:
// EJB 2.x 的典型写法:必须实现容器接口、必须写一堆生命周期回调public class OrderServiceBean implements SessionBean { private SessionContext context; // 业务方法只有最后一行,其余全是「容器要求」的仪式 public void ejbCreate() {} public void ejbActivate() {} public void ejbPassivate() {} public void ejbRemove() {} public void setSessionContext(SessionContext ctx) { this.context = ctx; } public BigDecimal total(Order order) { return order.getItems().stream() .map(Item::getPrice) .reduce(BigDecimal.ZERO, BigDecimal::add); }}而要用这个 Bean,你不能 new,只能通过 JNDI 去容器里查:
// 想在别处使用,必须先查目录服务,还要处理一堆受检异常Context ctx = new InitialContext();OrderServiceHome home = (OrderServiceHome) ctx.lookup("java:comp/env/ejb/OrderService");OrderService service = home.create();这套模式的三个致命问题,今天看来触目惊心:
- 强侵入:业务类必须继承/实现容器的接口,代码与运行环境死死绑定
- 必须部署才能测:不启动笨重的 EJB 容器就一行都跑不了,单元测试几乎无从谈起
- 样板代码淹没业务:写 5 行业务,要配 50 行仪式,开发速度被拖垮
EJB 最大的罪状不是「复杂」,而是把简单问题复杂化。一个只需要「算总和」的方法,被强制包上容器接口、生命周期回调、远程查找——这正是 Rod Johnson 后来反击的靶子。

把这条演进线按「谁替你做了什么」再切一遍,就是开篇那张地图的放大版——Spring 管装配、Boot 管配置、Cloud 管服务之间的事:

2002 年,澳大利亚开发者 Rod Johnson 写了一本书:《Expert One-on-One J2EE Design and Development》。这本书没有教人「如何更好地用 EJB」,而是做了一件更大胆的事:论证很多场景根本不需要 EJB。
他在书里附了一套约 3 万行的示例代码,用最朴素的 Java 对象(POJO)加依赖注入,就实现了 EJB 提供的核心能力。读者来信不断追问代码在哪、如何复用——这套代码后来演进成了 Spring Framework,2003 年正式开源。
这个故事最值得记住的一点是:Spring 的出发点不是「发明新技术」,而是「用普通 Java 类解决企业级问题」。 它的信条是「无侵入」:你的业务类就是一个普通的 class,不需要继承任何东西,扔进 Spring 能跑,脱离 Spring 也能跑,也随时能被普通 JUnit 测试。
Spring 这个名字来自「给 J2EE 带来春天」的说法。它当年的定位是一句口号——「J2EE without EJB」,即用轻量方式获得 EJB 的能力。
Spring 的核心叫 IoC(Inversion of Control,控制反转),这个术语吓退了无数初学者。其实它描述的事情非常朴素。
打个比方:你要做一道番茄炒蛋。 传统做法是——自己买菜、自己洗菜、自己切好,最后才下锅炒。你要为「把菜搞到手」这件事操一大堆心。而 IoC 的做法是——你只写菜谱(声明「我需要鸡蛋和番茄」),自然有厨房(容器)把处理好的食材送到手边,你专心炒菜。
对应到代码,先看"自己 new"的世界:
// 反例:依赖被硬编码在类内部public class OrderController { // 这个类「决定」了要用哪个实现,业务代码被实现细节绑架 private final OrderService service = new OrderServiceImpl(new MysqlOrderRepository()); public BigDecimal checkout(Long orderId) { return service.total(orderId); }}问题一目了然:想换成 RedisOrderRepository?回来改 OrderController 的源码。测试想注入一个假的 service?做不到,因为它自己 new 死的。
再看 IoC 的世界:
// 正例:只声明"我需要一个 OrderService",谁来给、怎么造,不关我事@Servicepublic class OrderController { private final OrderService service; // 构造器就是一份契约:容器看到它,就知道要注入一个 OrderService public OrderController(OrderService service) { this.service = service; } public BigDecimal checkout(Long orderId) { return service.total(orderId); }}- 类里没有任何
new,也就没有任何「用哪个实现」的决定权 - 决定权上交给容器,由它读懂你的构造器,把合适的实现注入进来
- 结果:想换实现,只要容器里有另一个
OrderService的 Bean;测试想传假的,直接new OrderController(mockService)就行
这就是「反转」两个字的含义:当你不再自己 new,而是声明依赖、由外部给你,控制对象创建的权利就交出去了。
术语和它的白话含义之间,隔着一层「老师不解释」的墙。玩一局配对:左列是文档里的词,右列是你将来在报错现场真正会看到的东西——配错会当场告诉你差在哪:

「控制反转」听起来抽象,拆成四个可观察的维度就清楚了:
| 维度 | 反转前(自己 new) | 反转后(容器注入) |
|---|---|---|
| 创建权 | 由使用方 new | 容器负责实例化 |
| 装配权 | 硬编码具体实现 | 容器按声明装配 |
| 生命周期 | 使用方自生自灭 | 容器统一管理(单例、销毁回调) |
| 替换实现 | 必须改源码 | 改注解 / 配置即可 |
| 可测试性 | 差,依赖写死 | 好,可注入 Mock |
一句话总结这张表:反转的不是「谁调用谁」,而是「谁负责把对象造出来、拼装好、管到底」。 业务代码只保留「我要什么」,其余全部外包给容器。
面试里常被追问「IoC 和 DI 是一回事吗」。准确说是:IoC 是思想(把控制权交出去),DI(依赖注入)是实现手段(通过构造器、Setter、字段把依赖送进来)。Spring 用 DI 实现了 IoC。
找装修公司也是同一回事。反转前你自己既是业主又是包工头:自己买水泥、自己请瓦工、自己盯工期、房子漏了自己修;反转后你只跟装修公司签合同(写构造器参数),公司负责调建材、排工序、验收、售后(容器负责创建、装配、生命周期、销毁)。你交出的是「谁来干活、怎么干活」的决定权,换回来的是「我只提要求」的轻松——这就是那四行表格说的全部内容。
把装修公司那份合同拆开看,它一共接走了六件事。这段动画按顺序演一遍,注意第 ③ 帧——缺依赖是在造之前当场爆掉的,不是等你调用的时候:

第六件事(销毁回调)是新手最容易当成「反正 JVM 会管」的那一件。切到「观察销毁回调」,你会看到容器关闭时那些回调到底有没有被调用、原型 Bean 为什么不在名单里:
Spring 不是单一框架,而是一整套家族。按你将来会接触的深度排出来是这样:
| 模块 | 解决的问题 | 你会用到它的场景 |
|---|---|---|
| spring-core / beans | IoC 容器与依赖注入的内核 | 一切的基础 |
| spring-context | ApplicationContext、事件、国际化 | 读取配置、发布事件 |
| spring-aop | 面向切面编程 | 日志、权限、声明式能力的基础 |
| spring-tx | 声明式事务 | @Transactional |
| spring-jdbc / orm | 数据访问与 ORM 集成 | JdbcTemplate、JPA 整合 |
| spring-web / webmvc | Web 与 MVC 分层 | @RestController、@GetMapping |
| spring-security | 认证与授权 | 登录、权限控制 |
| spring-test | 测试支持 | @SpringBootTest |
| spring-boot | 自动装配与起步依赖 | 现代项目的默认入口 |
记住一条主线:越往上走越贴近业务,越往下走越接近内核。 遇到「为什么注解生效」「为什么事务没回滚」这类问题,答案往往在下面几层(aop / tx / context)。
学 Spring 不要一上来就啃全家桶。先把 core + context(就是 IoC) 吃透,后面所有模块都是在这套容器上加能力——本质没变,只是能力变多。
这是面试和初学者最常混淆的一点。把 Spring Boot 理解成「脚手架的脚手架」:它不是一个替代 Spring 的新框架,而是建立在 Spring 之上、帮你省去配置的启动器。
| 对比项 | Spring Framework | Spring Boot |
|---|---|---|
| 定位 | 提供 IoC、AOP、MVC 等核心能力 | 在 Spring 之上做自动配置与打包 |
| 配置方式 | 需手动写 XML / Java 配置类 | 约定优于配置,开箱即用 |
| 依赖管理 | 自己挑兼容版本 | 起步依赖(starter)一次搞定 |
| 启动 | 需外部容器或手动集成 | 内嵌 Tomcat,main 方法直接跑 |
| 关系 | 地基 | 盖在地基上的样板房 |
三句话说清 Boot 到底帮你做了什么:
- 自动配置(auto-configuration):看到 classpath 里有
spring-webmvc,就自动帮你配好 DispatcherServlet - 起步依赖(starter):一个
spring-boot-starter-web拉齐 Web 开发要的一整套依赖,且版本互相兼容 - 内嵌容器:Tomcat 打进了 jar 里,
java -jar app.jar就能起服务,不再需要外部部署
「用了 Spring Boot 就不用懂 Spring 了」是最大的误解。自动配置替你做了决定,但出问题时你必须能看懂它做了什么。比如想知道某个 bean 为什么被创建,你要回到 spring-context 的知识去调试,而不是期望 Boot 告诉你答案。
这条演进线在第二节是五个名词,但每一环真正值得记的是「它替你省了什么、又让你欠下什么」。点着看一遍,尤其是最后两格——那就是你今天所在的位置:
「会用注解不就行了?」这是新手最常见的疑问。三个真实理由:
- 排查问题:报错说循环依赖、说 AOP 代理失效、说事务没回滚——不懂容器机制,你只能靠搜索和试错,懂了就能直接定位
- 读源码与看文档:Spring 的官方文档、社区文章默认你懂 IoC / Bean 生命周期;不懂内核,很多内容读了也无法落地
- 面试硬指标:
Bean 生命周期、循环依赖三级缓存、AOP 代理方式、事务传播是 Java 后端面试的必考题,且几乎都以「原理」形式提问
亲手体验一下容器是怎么把对象装配起来的——切换右下角的条件开关,观察 Bean 的装配结果如何变化:
Spring 的学习曲线是「先会用、再懂原理、最后能debug」。跳过中间那步,你会永远卡在「照抄能跑、报错不会」的状态——这正是本教程坚持「先讲为什么」的原因。
如果你选的是 B,那第一步之后就是「这个工程要哪些依赖」。别抄别人的 pom——勾一遍,生成器每勾一项都会附一句「为什么它在这儿、去掉会怎样」:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version> <!-- 版本由 BOM 统管,子依赖不写 version -->
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>demo-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>上面说的「EJB 很重、Spring 很轻」都是历史结论。真正能让你记住的,是自己数一遍步骤。下面这个实验有四个档位,请按顺序切:先点 「手写 12 步」,看不用框架时你到底要写多少行;再点 「交给容器」,看同样一件事被压缩成哪几步;然后点 「再进一步:Boot」,看连配置都想省掉时会发生什么;最后点 「代价与收益」,看框架到底让你付出了什么。别急着往下读,先把这四档各按一次。
看完实验,对照这张对错图记结论:左边那栏你写的每一行都在「伺候环境」,右边那栏你写的每一行都在表达业务。

手写版不是「不该学」。恰恰相反——亲手做一次 JDBC 全链路,你才知道 Spring 到底替你删掉了什么。本课程后面第 27 篇还会回去写它。
第四节说「反转的是创建权」,但创建权交出去以后,对象具体怎么拼装?这个实验把三种写法摆在一起:构造器注入(提车时发动机必须在场)、setter 注入(先提空车再装轮胎)、字段注入(有人趁你不注意往车里塞了个零件)。最后切到 「遇到循环依赖」,看同一种死结在三种写法下各自的下场——为什么构造器当场炸、字段和 setter 却还能启动。
这三种写法的区别,就像取快递的三种姿势——构造器注入是「当面签收,货不齐不签」(快速失败);setter 注入是「先放门口,回头再装」(可以补装也能换);字段注入是「不知谁把东西塞进了你家」(最省事,但你完全看不见过程)。
「当面签收」这一步到底怎么发生的?把它摊成一次单步执行:左边是 main 里那五行,右边同步刷新「此刻的变量」和「容器正在哪一层」。连点下一步,重点在第 3、4 拍——对象是在第 3 拍就被造好的,第 4 拍只是从池子里拿:
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);// ① 读配置:把每个 @Service 转成一份 BeanDefinition// ② refresh():预实例化所有非懒加载单例// ③ 造 orderController:先问它构造器要什么// ④ 按类型找候选 → 命中 orderService → 递归先造它HelloController h = ctx.getBean(HelloController.class); // 只是从池里取| ctx | AnnotationConfigApplicationContext |
| 已注册的图纸 | 0 → 待读 |
new AnnotationConfigApplicationContext再把最常见的一次真实改动放进动画里看一遍:产品要求订单仓储从 MySQL 换成 Redis。左边那条路(手写)要翻遍每一个 new,漏一处就是运行期的 NullPointerException;右边那条路(容器)只动声明,剩下的由容器在启动时校验。

很多人以为「<bean> 标签一写,对象就自己冒出来了」。其实中间有明确的两段路:先把 XML 读成一份份图纸(BeanDefinition——描述「这个对象该怎么造」的元数据,还不是对象本身),再照着图纸把对象造出来。下面这个实验分四档:读取并解析、注册 BeanDefinition、getBean 现场、写错 class 会怎样。第四档尤其值得点一次——它会让你明白为什么 XML 里的类名写错,编辑器不报错、启动才炸。
这三段路在第 5 篇会完整展开成你自己的第一个可运行工程。这里只要建立一个印象:注解和 XML 只是两种写法,最终都变成同一份图纸。
前面三个实验讲的是「流程」,这一个讲的是「决定」。下面这块沙盘是真的在你的浏览器里跑一个 Java 写的小容器:打开或关掉某个条件开关,容器会实时重新装配 Bean,并把「为什么这个 Bean 没建」写在日志里。试着把数据源那个开关关掉,看看下游那些依赖它的 Bean 会发生什么。
四个实验做完,换成命令行自己敲。这台控制台连着浏览器里的同一个容器,答案全部由内核算出来——先敲 beans 看容器里到底有几个对象,再敲 di 看装配关系,最后逐条 lab:
beans 之后再敲一次 di,你会看到同一批对象的两种视图——「有哪些」和「谁依赖谁」。第四节那张表说的「装配权」,就是这第二张视图。
「要不要用 Spring」不是信仰问题,而是算术题。下面这个沙盘给你三个变量:协作对象的数量、是否需要换实现、是否需要单元测试。调一调,右侧会直接给出结论与代价估算。
# 只有 3 个对象、不换实现、不写测试手工组装成本:约 10 分钟(new 三行写完)引入容器成本:约 2 天(依赖 + 配置 + 团队学习)结论:不值得 —— 直接用 new
判断依据只有一个问题——你的代码里是否存在「多个需要协作的对象」。有,容器就在替你省胶水代码;没有,硬上容器只会拖慢冷启动、增加黑盒。
第一题是最基础的直觉题,答对说明「点外卖」这个类比你已经接住了:
第二题偏综合,把第六节和第七节的取舍串起来:
这一节是你之后的「自救入口」。报错原文请照抄去搜索,不要意译——搜索引擎认的是片段。
| 报错原文(片段) | 真实原因 | 30 秒自救 | 深挖看第几篇 |
|---|---|---|---|
Exception in thread "main" java.lang.NullPointerException at com.example.OrderController.checkout(OrderController.java:14) | 你压根没 new 过这个对象,或者指望容器帮你填字段,但这个类根本没进容器 | 顺着栈顶那一行找到字段:它是 null 就说明依赖没装上。要么老老实实 new,要么让容器来造这个类(加 @Service 并从容器取) | 本篇第四节、第 6 篇 |
@Autowired 标注的字段运行时是 null,且启动没有任何报错 | 这个对象是你 new XxxService() 出来的,没经过容器,自然没人给它注入 | 全局搜一下这个类名前面的 new。凡是需要注入/事务/切面的类,一律从容器取,绝不自己 new | 第 5 篇第八节、第 6 篇 |
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.OrderService' available | 容器里没有这个类型的 Bean:类忘了标 @Service/@Component,或者包不在扫描路径内 | 先看类上有没有组件注解,再看主类所在包是否是它的父包。两个都对,就去 getBeanNamesForType 打一行日志确认 | 第 5 篇、第 7 篇 |
org.springframework.beans.factory.NoUniqueBeanDefinitionException: ... expected single matching bean but found 2: mysqlOrderRepository,redisOrderRepository | 同一个接口有两个实现都被扫进来了,容器不知道该给你哪个 | 给注入点加 @Qualifier("mysqlOrderRepository") 点名,或在默认实现上标 @Primary | 第 6 篇第三节、第 7 篇 |
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'orderService': Requested bean is currently in creation: Is there an unresolvable circular reference? | A 的构造器要 B,B 的构造器又要 A,两边都在等对方先出生 | 先别开 allow-circular-references。看能不能把公共逻辑抽成第三个 Bean,或给其中一个注入点加 @Lazy | 第 10 篇 |
java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver | 手写 JDBC 时代的第一课:驱动 jar 没进 classpath,或没触发驱动注册 | 检查依赖坐标(mvn dependency:tree),确认打包后的 jar 里有这个类;用 Spring/JdbcTemplate 时这一步由模板代管 | 第 2 篇、第 27 篇 |
「我该用 BeanFactory 还是 ApplicationContext?」(选择困难,不是异常) | 两者不是新旧关系而是层次关系:前者是最底层接口(用到才造,懒加载),后者在它之上加了事件、国际化、资源与自动代理,并且启动时就预实例化所有非懒单例 | 实际项目一律用 ApplicationContext。只有嵌入式、冷启动极度敏感的场景才考虑裸 BeanFactory;报错误判时记得:启动即炸的那批异常正是预实例化带来的好处 | 第 5 篇第六节、第 8 篇 |
搜报错时只搜冒号后面的第一段英文原文(例如 expected single matching bean but found 2),命中率远高于搜整句——不同版本的尾部话术会变。
上面第三行那条 NoSuchBeanDefinitionException 是几乎每个人遇到的第一个 Spring 报错。别背它,先亲手点一次凶手行——注意栈最上面那行不是答案:
你按第四节的写法给 OrderService 加了构造器注入,项目一启动就整屏红字,而且你的代码一行都没执行过。
目标:五分钟跑通「点外卖」的最小版本——同一个接口,两份实现,由你自己充当容器决定给哪一个。不需要 Maven,JDK 装好就能跑。
package com.example.warmup;// ① 抽象:你只认识这张「菜单」interface CoffeeMaker { String make();}// ② 实现一:门店咖啡机class ShopCoffeeMaker implements CoffeeMaker { @Override public String make() { return "门店现磨"; }}// ③ 实现二:外卖咖啡class DeliveryCoffeeMaker implements CoffeeMaker { @Override public String make() { return "外卖送到"; }}// ④ 业务类:只声明需要什么,不做任何选择class Customer { private final CoffeeMaker maker; // 构造器注入,字段可以 final Customer(CoffeeMaker maker) { this.maker = maker; } String drink() { return "我喝到:" + maker.make(); }}public class Main { public static void main(String[] args) { // ⑤ 此刻「容器」就是你手写的这一段:决定权在使用者之外 CoffeeMaker chosen = new DeliveryCoffeeMaker(); Customer customer = new Customer(chosen); System.out.println(customer.drink()); // ⑥ 换实现:业务类一个字都没改 Customer another = new Customer(new ShopCoffeeMaker()); System.out.println(another.drink()); }}编译并运行:
javac -d out src/com/example/warmup/Main.javajava -cp out com.example.warmup.Main预期输出:
我喝到:外卖送到我喝到:门店现磨对照检查三件事:① Customer 里没有任何 new CoffeeMaker,它只认接口;② 换实现只动了 Main,业务类零改动;③ maker 是 final,构造完就不可变——这正是第 6 篇推荐构造器注入的两个理由。
目标:亲眼看见「忘注入」是什么后果。
做法:把 Main 里那段「手写容器」删掉,改成在 Customer 内部直接写字段 private CoffeeMaker maker = null;,其余不动,然后调用 customer.drink()。
你会观察到:程序照常编译、照常启动,直到调用那一刻才炸出 NullPointerException。把这段栈信息抄进笔记,并在心里给它贴个标签——它就是本节报错速查第一行的原型。再进一步的变体:给 Customer 加一个 setMaker(...),先在构造后立刻调用 drink()(炸),再补一句 setMaker(...)(活)。你会发现 setter 注入的代价就是「对象可能存在半成品状态」。
目标:做一个「三家餐厅 + 一位食客」的小项目,体会容器真正替你做了什么。
要求:定义 Restaurant 接口(dish() 方法),写 HotpotRestaurant、SushiRestaurant、CanteenRestaurant 三个实现;再写一个极简的 MyContainer,内部用一个 Map<String, Restaurant> 存「名字 → 实例」,对外只暴露 pick(String name)。Customer 通过 MyContainer 拿餐厅,完全不 new 任何实现。
验收清单:
- [ ]
Customer源码里搜不到任何具体实现类名(只有接口名) - [ ] 新增第四家餐厅时,只需在
MyContainer注册,业务类零改动 - [ ]
MyContainer.pick("not-exist")有明确处理,你能说清这相当于 Spring 的哪个异常 - [ ] 你能对着这张表讲清四件事分别被谁接管:创建权、装配权、生命周期、替换实现
- [ ] 写下你的判断:你这个
MyContainer缺了 Spring 的哪三项能力(提示:作用域、初始化回调、按类型查找)
不看资料,用「点外卖」把控制反转讲给一个没写过代码的人听,讲清楚「你交出的是什么、留下的是什么」。
第四节那张表列了反转的四个维度,你能默写出其中三个吗?缺一即回到第四节。
IoC 与 DI 谁是谁的手段?请用一句话回答,且这句话里必须出现「思想」与「实现手段」。
BeanFactory 与 ApplicationContext 最实际的差别是什么?关键词应该是「实例化时机」,而不是「新旧」。
报错速查里 NoUniqueBeanDefinitionException 那条,你能说出两种解法分别在什么场景用吗?(提示:一个是「这次特殊一点」,一个是「平时都以它为准」。)
点菜不下厨,反转交出创建权;图纸(BeanDefinition)先入册,容器随后送对象。
Spring 用二十年证明了它的价值观——无侵入、可测试、面向接口。它的核心 IoC 只做一件事:把你从「自己 new 一切」中解放出来,让你只声明依赖,把创建与装配交给容器。理解了这一层,Spring 的所有模块(AOP、事务、MVC、Boot、Cloud)都只是这套容器上叠加的能力,而不再是一堆需要死记的注解。