Spring 到底是什么:从 EJB 之痛到 IoC 的诞生

bee2026-10-0846 分钟0 次阅读
为什么一个 2003 年的框架能统治 Java 二十年?从 EJB 的历史教训讲到 IoC 的直觉解释,再看 Spring 全家桶版图与 Spring / Spring Boot 的本质区别。
1 / 135
小节
〇、30 秒看懂
2 / 135

一句话:Spring 是一个「帮你造对象、递对象、管对象」的容器。 你以前写 Java,要用谁就自己 new 一个;用 Spring 之后,你只写一句「我需要一个能查订单的东西」,剩下的「从哪来、怎么拼、什么时候销毁」全由容器安排。这一篇讲它为什么会长成这样:2003 年的 Java 圈被 EJB 那套「仪式代码」压得喘不过气,Rod Johnson 用一本技术书证明「普通 Java 类 + 依赖注入」就能干同样的活,Spring 由此出生,后来又长出 Boot(替你省配置)和 Cloud(管一堆服务)。

3 / 135
类比

点外卖就是控制反转。 你想吃一盘番茄炒蛋,传统写法是自己买菜、洗菜、切菜、掌勺——你既当食客又当厨房;点外卖只需要下单(声明「我要这道菜」),火候、食材、装盒全由商家(容器)决定。商家哪天换厨师、换锅、换包装,你的订单一个字都不用改。所谓「反转」,反的就是这件事:做菜的主导权从你手里转到了商家手里。

4 / 135
架构图
图 · 三步演进:Spring → Boot → Cloud
图 · 三步演进:Spring → Boot → Cloud
5 / 135
类比

自动售货机就是依赖注入。 你只按货道编号(构造器里写「我要 OrderRepository」),机器负责把饮料送到取货口——它从哪个仓库补货、里面是哪一批货、什么时候下架,你一概不用管。换成新机器(换实现)也不用改你的按钮。

6 / 135

学完这一篇,你要能回答三个问题:

7 / 135
  1. EJB 到底难用在哪?为什么说 Spring 的出发点是「无侵入」?
  2. 「控制反转」听起来玄,具体反转了哪四件事?它和「依赖注入」是什么关系?
  3. 已经会用 Spring Boot 了,为什么还要回头学 Spring 容器?
8 / 135
小节
一、先回到 2000 年代初的开发现场
9 / 135

要理解 Spring 为什么伟大,得先知道它拯救的是什么。二十多年前,Java 企业开发的标准答案是 EJB(Enterprise JavaBeans)。它看起来很正规,写起来却很痛苦:

10 / 135
java
// 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);    }}
11 / 135

而要用这个 Bean,你不能 new,只能通过 JNDI 去容器里查:

12 / 135
java
// 想在别处使用,必须先查目录服务,还要处理一堆受检异常Context ctx = new InitialContext();OrderServiceHome home = (OrderServiceHome) ctx.lookup("java:comp/env/ejb/OrderService");OrderService service = home.create();
13 / 135

这套模式的三个致命问题,今天看来触目惊心:

14 / 135
  • 强侵入:业务类必须继承/实现容器的接口,代码与运行环境死死绑定
  • 必须部署才能测:不启动笨重的 EJB 容器就一行都跑不了,单元测试几乎无从谈起
  • 样板代码淹没业务:写 5 行业务,要配 50 行仪式,开发速度被拖垮
15 / 135
坑

EJB 最大的罪状不是「复杂」,而是把简单问题复杂化。一个只需要「算总和」的方法,被强制包上容器接口、生命周期回调、远程查找——这正是 Rod Johnson 后来反击的靶子。

16 / 135
小节
二、一个程序员对复杂度的反击
17 / 135
架构图
图 1 · Java 企业开发的二十年演进
图 1 · Java 企业开发的二十年演进
18 / 135

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

19 / 135
架构图
图 · 三步演进:Spring → Boot → Cloud 的分工
图 · 三步演进:Spring → Boot → Cloud 的分工
20 / 135

2002 年,澳大利亚开发者 Rod Johnson 写了一本书:《Expert One-on-One J2EE Design and Development》。这本书没有教人「如何更好地用 EJB」,而是做了一件更大胆的事:论证很多场景根本不需要 EJB。

21 / 135

他在书里附了一套约 3 万行的示例代码,用最朴素的 Java 对象(POJO)加依赖注入,就实现了 EJB 提供的核心能力。读者来信不断追问代码在哪、如何复用——这套代码后来演进成了 Spring Framework,2003 年正式开源。

22 / 135

这个故事最值得记住的一点是:Spring 的出发点不是「发明新技术」,而是「用普通 Java 类解决企业级问题」。 它的信条是「无侵入」:你的业务类就是一个普通的 class,不需要继承任何东西,扔进 Spring 能跑,脱离 Spring 也能跑,也随时能被普通 JUnit 测试。

23 / 135
说明

Spring 这个名字来自「给 J2EE 带来春天」的说法。它当年的定位是一句口号——「J2EE without EJB」,即用轻量方式获得 EJB 的能力。

24 / 135
小节
三、IoC 的直觉解释
25 / 135

Spring 的核心叫 IoC(Inversion of Control,控制反转),这个术语吓退了无数初学者。其实它描述的事情非常朴素。

26 / 135

打个比方:你要做一道番茄炒蛋。 传统做法是——自己买菜、自己洗菜、自己切好,最后才下锅炒。你要为「把菜搞到手」这件事操一大堆心。而 IoC 的做法是——你只写菜谱(声明「我需要鸡蛋和番茄」),自然有厨房(容器)把处理好的食材送到手边,你专心炒菜。

27 / 135

对应到代码,先看"自己 new"的世界:

28 / 135
java
// 反例:依赖被硬编码在类内部public class OrderController {    // 这个类「决定」了要用哪个实现,业务代码被实现细节绑架    private final OrderService service = new OrderServiceImpl(new MysqlOrderRepository());    public BigDecimal checkout(Long orderId) {        return service.total(orderId);    }}
29 / 135

问题一目了然:想换成 RedisOrderRepository?回来改 OrderController 的源码。测试想注入一个假的 service?做不到,因为它自己 new 死的。

30 / 135

再看 IoC 的世界:

31 / 135
代码对照
代码java
// 正例:只声明"我需要一个 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) 就行
32 / 135

这就是「反转」两个字的含义:当你不再自己 new,而是声明依赖、由外部给你,控制对象创建的权利就交出去了。

33 / 135

术语和它的白话含义之间,隔着一层「老师不解释」的墙。玩一局配对:左列是文档里的词,右列是你将来在报错现场真正会看到的东西——配错会当场告诉你差在哪:

34 / 135
配对闯关
闯关Spring 术语 ↔ 报错现场的大白话已配对 0/6 · 配错 0
六个词都是第一篇到第三篇会反复出现的。别靠位置猜,两列都打乱了
先点左边一个
35 / 135
小节
四、控制反转到底反转了什么
36 / 135
原理动画
动图 · 反转前后:对象的两种命运
动图 · 反转前后:对象的两种命运
37 / 135

「控制反转」听起来抽象,拆成四个可观察的维度就清楚了:

38 / 135
对照表
维度反转前(自己 new)反转后(容器注入)
创建权由使用方 new容器负责实例化
装配权硬编码具体实现容器按声明装配
生命周期使用方自生自灭容器统一管理(单例、销毁回调)
替换实现必须改源码改注解 / 配置即可
可测试性差,依赖写死好,可注入 Mock
39 / 135

一句话总结这张表:反转的不是「谁调用谁」,而是「谁负责把对象造出来、拼装好、管到底」。 业务代码只保留「我要什么」,其余全部外包给容器。

40 / 135
要点

面试里常被追问「IoC 和 DI 是一回事吗」。准确说是:IoC 是思想(把控制权交出去),DI(依赖注入)是实现手段(通过构造器、Setter、字段把依赖送进来)。Spring 用 DI 实现了 IoC。

41 / 135
类比

找装修公司也是同一回事。反转前你自己既是业主又是包工头:自己买水泥、自己请瓦工、自己盯工期、房子漏了自己修;反转后你只跟装修公司签合同(写构造器参数),公司负责调建材、排工序、验收、售后(容器负责创建、装配、生命周期、销毁)。你交出的是「谁来干活、怎么干活」的决定权,换回来的是「我只提要求」的轻松——这就是那四行表格说的全部内容。

42 / 135

把装修公司那份合同拆开看,它一共接走了六件事。这段动画按顺序演一遍,注意第 ③ 帧——缺依赖是在造之前当场爆掉的,不是等你调用的时候:

43 / 135
原理动画
动图 · 容器接手一个对象的六份工作
动图 · 容器接手一个对象的六份工作
44 / 135

第六件事(销毁回调)是新手最容易当成「反正 JVM 会管」的那一件。切到「观察销毁回调」,你会看到容器关闭时那些回调到底有没有被调用、原型 Bean 为什么不在名单里:

45 / 135
内核实验
TeaVMBean 的一生:容器到底管到哪一步未启动
先点单例建立基准,再切原型看差异,最后用销毁回调看清「容器管造、也管死」这句话的边界
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
46 / 135
小节
五、Spring 版图:它到底包含什么
47 / 135

Spring 不是单一框架,而是一整套家族。按你将来会接触的深度排出来是这样:

48 / 135
对照表
模块解决的问题你会用到它的场景
spring-core / beansIoC 容器与依赖注入的内核一切的基础
spring-contextApplicationContext、事件、国际化读取配置、发布事件
spring-aop面向切面编程日志、权限、声明式能力的基础
spring-tx声明式事务@Transactional
spring-jdbc / orm数据访问与 ORM 集成JdbcTemplate、JPA 整合
spring-web / webmvcWeb 与 MVC 分层@RestController、@GetMapping
spring-security认证与授权登录、权限控制
spring-test测试支持@SpringBootTest
spring-boot自动装配与起步依赖现代项目的默认入口
49 / 135

记住一条主线:越往上走越贴近业务,越往下走越接近内核。 遇到「为什么注解生效」「为什么事务没回滚」这类问题,答案往往在下面几层(aop / tx / context)。

50 / 135
提示

学 Spring 不要一上来就啃全家桶。先把 core + context(就是 IoC) 吃透,后面所有模块都是在这套容器上加能力——本质没变,只是能力变多。

51 / 135
小节
六、Spring Framework 与 Spring Boot 的关系
52 / 135

这是面试和初学者最常混淆的一点。把 Spring Boot 理解成「脚手架的脚手架」:它不是一个替代 Spring 的新框架,而是建立在 Spring 之上、帮你省去配置的启动器。

53 / 135
对照表
对比项Spring FrameworkSpring Boot
定位提供 IoC、AOP、MVC 等核心能力在 Spring 之上做自动配置与打包
配置方式需手动写 XML / Java 配置类约定优于配置,开箱即用
依赖管理自己挑兼容版本起步依赖(starter)一次搞定
启动需外部容器或手动集成内嵌 Tomcat,main 方法直接跑
关系地基盖在地基上的样板房
54 / 135

三句话说清 Boot 到底帮你做了什么:

55 / 135
  • 自动配置(auto-configuration):看到 classpath 里有 spring-webmvc,就自动帮你配好 DispatcherServlet
  • 起步依赖(starter):一个 spring-boot-starter-web 拉齐 Web 开发要的一整套依赖,且版本互相兼容
  • 内嵌容器:Tomcat 打进了 jar 里,java -jar app.jar 就能起服务,不再需要外部部署
56 / 135
坑

「用了 Spring Boot 就不用懂 Spring 了」是最大的误解。自动配置替你做了决定,但出问题时你必须能看懂它做了什么。比如想知道某个 bean 为什么被创建,你要回到 spring-context 的知识去调试,而不是期望 Boot 告诉你答案。

57 / 135

这条演进线在第二节是五个名词,但每一环真正值得记的是「它替你省了什么、又让你欠下什么」。点着看一遍,尤其是最后两格——那就是你今天所在的位置:

58 / 135
交互图解
流程五步演进:每一步省了什么、又欠了什么1 / 5
从 ① 点到 ⑤,前两格是历史,第三格是你面试要答的,后两格是你每天用的
→
→
→
→
① EJB:必须继承容器接口
省掉的是事务和远程调用的手写代码,欠下的是「你的业务类只能是 EJB」——不能脱离容器测试,类结构被框架绑架。这是 Spring 诞生的直接原因。
全部看懂了每一层都在替你省样板代码,同时把复杂度往更深处挪——你省下的配置,最后都会变成看不懂的行为。
59 / 135
小节
七、为什么今天仍要学 Spring 的原理
60 / 135
小节
七、为什么今天仍要学 Spring 的原理
61 / 135

「会用注解不就行了?」这是新手最常见的疑问。三个真实理由:

62 / 135
  1. 排查问题:报错说循环依赖、说 AOP 代理失效、说事务没回滚——不懂容器机制,你只能靠搜索和试错,懂了就能直接定位
  2. 读源码与看文档:Spring 的官方文档、社区文章默认你懂 IoC / Bean 生命周期;不懂内核,很多内容读了也无法落地
  3. 面试硬指标:Bean 生命周期、循环依赖三级缓存、AOP 代理方式、事务传播 是 Java 后端面试的必考题,且几乎都以「原理」形式提问
63 / 135

亲手体验一下容器是怎么把对象装配起来的——切换右下角的条件开关,观察 Bean 的装配结果如何变化:

64 / 135
内核实验
65 / 135
要点

Spring 的学习曲线是「先会用、再懂原理、最后能debug」。跳过中间那步,你会永远卡在「照抄能跑、报错不会」的状态——这正是本教程坚持「先讲为什么」的原因。

66 / 135
小节
八、决策卡与总结
67 / 135
决策
决策你加入一个新团队,发现这是个全新的 Spring Boot 项目,配置文件里没有一行 XML。有同事说「既然都用注解了,XML 配置相关的知识可以直接跳过」。这个说法对吗?
68 / 135
决策
决策你是刚学完 Java 语法的小白,接下来想「学会 Spring」。第一条路该走哪边?
69 / 135

如果你选的是 B,那第一步之后就是「这个工程要哪些依赖」。别抄别人的 pom——勾一遍,生成器每勾一项都会附一句「为什么它在这儿、去掉会怎样」:

70 / 135
生成器
生成器决定上容器之后:第一份 pom 该有什么pom.xml2 / 7
先只勾 Web 与 Test,看一份能跑能测的最小工程;再按需加 JPA / MySQL。注意 MySQL 生成的是 scope=runtime、Test 是 scope=test——这正是第 2 篇第四节要讲的东西
产物
<?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>
勾了这些,代价与理由在这里
parent继承 3.3.4 的 starter-parent 之后,所有 spring-boot-starter-* 都不用写版本号;一旦有人手写给某个 starter 加 version,就以那条为准——这是依赖版本漂移最常见的原因。
Web做接口就绕不开它: DispatcherServlet、内嵌 Tomcat、JSON 序列化全在这个 starter 里。
Testscope=test;@SpringBootTest、MockMvc、AssertJ 都在里面,漏了就找不到 @Test。
71 / 135
小节
十一、内核实验一:亲手把「手写十二步」走一遍
72 / 135

上面说的「EJB 很重、Spring 很轻」都是历史结论。真正能让你记住的,是自己数一遍步骤。下面这个实验有四个档位,请按顺序切:先点 「手写 12 步」,看不用框架时你到底要写多少行;再点 「交给容器」,看同样一件事被压缩成哪几步;然后点 「再进一步:Boot」,看连配置都想省掉时会发生什么;最后点 「代价与收益」,看框架到底让你付出了什么。别急着往下读,先把这四档各按一次。

73 / 135
内核实验
TeaVM不用 Spring vs 用 Spring:十二步是怎么消失的未启动
依次切 raw → spring → boot → cost,重点看步骤数量与剩下的代码里还有没有业务以外的东西
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
74 / 135

看完实验,对照这张对错图记结论:左边那栏你写的每一行都在「伺候环境」,右边那栏你写的每一行都在表达业务。

75 / 135
架构图
图 · 手写十二步 vs 交给容器
图 · 手写十二步 vs 交给容器
76 / 135
说明

手写版不是「不该学」。恰恰相反——亲手做一次 JDBC 全链路,你才知道 Spring 到底替你删掉了什么。本课程后面第 27 篇还会回去写它。

77 / 135
小节
十二、内核实验二:依赖到底是怎么「递到手里」的
78 / 135

第四节说「反转的是创建权」,但创建权交出去以后,对象具体怎么拼装?这个实验把三种写法摆在一起:构造器注入(提车时发动机必须在场)、setter 注入(先提空车再装轮胎)、字段注入(有人趁你不注意往车里塞了个零件)。最后切到 「遇到循环依赖」,看同一种死结在三种写法下各自的下场——为什么构造器当场炸、字段和 setter 却还能启动。

79 / 135
内核实验
TeaVM三种注入的装配时序未启动
按 constructor → setter → field → cycle 的顺序切,注意每一步 orderRepository 什么时候才有值
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
80 / 135
类比

这三种写法的区别,就像取快递的三种姿势——构造器注入是「当面签收,货不齐不签」(快速失败);setter 注入是「先放门口,回头再装」(可以补装也能换);字段注入是「不知谁把东西塞进了你家」(最省事,但你完全看不见过程)。

81 / 135

「当面签收」这一步到底怎么发生的?把它摊成一次单步执行:左边是 main 里那五行,右边同步刷新「此刻的变量」和「容器正在哪一层」。连点下一步,重点在第 3、4 拍——对象是在第 3 拍就被造好的,第 4 拍只是从池子里拿:

82 / 135
单步调试台
单步台一次 getBean 之前发生了什么:容器是怎么把依赖递到手的1 / 6
六拍。第 2 拍决定启动快慢,第 5 拍决定你测试时要不要起容器
被调试的代码
1ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
2// ① 读配置:把每个 @Service 转成一份 BeanDefinition
3// ② refresh():预实例化所有非懒加载单例
4// ③ 造 orderController:先问它构造器要什么
5// ④ 按类型找候选 → 命中 orderService → 递归先造它
6HelloController h = ctx.getBean(HelloController.class); // 只是从池里取
此刻的变量
ctxAnnotationConfigApplicationContext
已注册的图纸0 → 待读
调用栈
1new AnnotationConfigApplicationContext
1构造函数这一行做完全部工作:读配置、注册图纸、refresh。此刻你手上还什么都没有,但容器已经知道自己该造几个对象。
83 / 135

再把最常见的一次真实改动放进动画里看一遍:产品要求订单仓储从 MySQL 换成 Redis。左边那条路(手写)要翻遍每一个 new,漏一处就是运行期的 NullPointerException;右边那条路(容器)只动声明,剩下的由容器在启动时校验。

84 / 135
原理动画
动图 · 一次「换实现」的两种命运
动图 · 一次「换实现」的两种命运
85 / 135
小节
十三、内核实验三:容器读一个 XML 文件时到底在干什么
86 / 135

很多人以为「<bean> 标签一写,对象就自己冒出来了」。其实中间有明确的两段路:先把 XML 读成一份份图纸(BeanDefinition——描述「这个对象该怎么造」的元数据,还不是对象本身),再照着图纸把对象造出来。下面这个实验分四档:读取并解析、注册 BeanDefinition、getBean 现场、写错 class 会怎样。第四档尤其值得点一次——它会让你明白为什么 XML 里的类名写错,编辑器不报错、启动才炸。

87 / 135
内核实验
TeaVMXML 容器启动全过程未启动
先看 parse 与 register 两段(此时还没有任何对象),再看 getbean 如何命中单例池,最后用 typo 复现新手第一个报错
场景参数
点「运行演示」,在浏览器内真实执行 Java 编译出的内核算法,逐步看它怎么跑。
88 / 135
提示

这三段路在第 5 篇会完整展开成你自己的第一个可运行工程。这里只要建立一个印象:注解和 XML 只是两种写法,最终都变成同一份图纸。

89 / 135
小节
十四、内核实验四:装配沙盘——改一个条件,看连锁反应
90 / 135

前面三个实验讲的是「流程」,这一个讲的是「决定」。下面这块沙盘是真的在你的浏览器里跑一个 Java 写的小容器:打开或关掉某个条件开关,容器会实时重新装配 Bean,并把「为什么这个 Bean 没建」写在日志里。试着把数据源那个开关关掉,看看下游那些依赖它的 Bean 会发生什么。

91 / 135
内核实验
92 / 135

四个实验做完,换成命令行自己敲。这台控制台连着浏览器里的同一个容器,答案全部由内核算出来——先敲 beans 看容器里到底有几个对象,再敲 di 看装配关系,最后逐条 lab:

93 / 135
内核控制台
94 / 135
提示

beans 之后再敲一次 di,你会看到同一批对象的两种视图——「有哪些」和「谁依赖谁」。第四节那张表说的「装配权」,就是这第二张视图。

95 / 135
小节
十五、沙盘:我这个项目到底该不该上容器
96 / 135

「要不要用 Spring」不是信仰问题,而是算术题。下面这个沙盘给你三个变量:协作对象的数量、是否需要换实现、是否需要单元测试。调一调,右侧会直接给出结论与代价估算。

97 / 135
沙盘
沙盘要不要上容器:三档开关定夺
运行结果
# 只有 3 个对象、不换实现、不写测试
手工组装成本:约 10 分钟(new 三行写完)
引入容器成本:约 2 天(依赖 + 配置 + 团队学习)
结论:不值得 —— 直接用 new
典型:一次性脚本、CLI 小工具、单个 main 跑完就退出。容器没有可插手的余地。
98 / 135
要点

判断依据只有一个问题——你的代码里是否存在「多个需要协作的对象」。有,容器就在替你省胶水代码;没有,硬上容器只会拖慢冷启动、增加黑盒。

99 / 135
小节
十六、随堂自测
100 / 135

第一题是最基础的直觉题,答对说明「点外卖」这个类比你已经接住了:

101 / 135
随堂自测
随堂自测同事把一段 Java 代码里的 `new MysqlOrderRepository()` 改成了「只在构造器参数里声明需要一个 OrderRepository,具体谁给不管」。这个改动最主要买到了什么?
先自己选一个,选中立刻告诉你对不对
102 / 135

第二题偏综合,把第六节和第七节的取舍串起来:

103 / 135
随堂自测
随堂自测一个团队新开内部小工具:一个 main 方法读 CSV、清洗后打印,全程没有数据库、没有 Web,预计三个月后就废弃。关于要不要用 Spring Boot,最合理的判断是?
先自己选一个,选中立刻告诉你对不对
104 / 135
小节
十七、常见报错速查
105 / 135

这一节是你之后的「自救入口」。报错原文请照抄去搜索,不要意译——搜索引擎认的是片段。

106 / 135
对照表
报错原文(片段)真实原因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 篇
107 / 135
提示

搜报错时只搜冒号后面的第一段英文原文(例如 expected single matching bean but found 2),命中率远高于搜整句——不同版本的尾部话术会变。

108 / 135

上面第三行那条 NoSuchBeanDefinitionException 是几乎每个人遇到的第一个 Spring 报错。别背它,先亲手点一次凶手行——注意栈最上面那行不是答案:

109 / 135
报错急救
报错急救NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.OrderRepository'
容器说「我这里没有这种 Bean」:从下往上读,答案在最后一句

你按第四节的写法给 OrderService 加了构造器注入,项目一启动就整屏红字,而且你的代码一行都没执行过。

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'orderService' defined in class path resource: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'com.example.OrderRepository' available
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:797)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:238)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1204)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:599)
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.OrderRepository' available: expected at least 1 bean which qualifies as autowire candidate
点你认为的「凶手行」(可反复试)
不会也没关系:先猜异常名,再猜哪一行在做决定。
110 / 135
小节
十八、动手练习
111 / 135
小节
第一档 · 照做
112 / 135

目标:五分钟跑通「点外卖」的最小版本——同一个接口,两份实现,由你自己充当容器决定给哪一个。不需要 Maven,JDK 装好就能跑。

113 / 135
java
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());    }}
114 / 135

编译并运行:

115 / 135
bash
javac -d out src/com/example/warmup/Main.javajava -cp out com.example.warmup.Main
116 / 135

预期输出:

117 / 135
text
我喝到:外卖送到我喝到:门店现磨
118 / 135

对照检查三件事:① Customer 里没有任何 new CoffeeMaker,它只认接口;② 换实现只动了 Main,业务类零改动;③ maker 是 final,构造完就不可变——这正是第 6 篇推荐构造器注入的两个理由。

119 / 135
小节
第二档 · 变体
120 / 135

目标:亲眼看见「忘注入」是什么后果。

121 / 135

做法:把 Main 里那段「手写容器」删掉,改成在 Customer 内部直接写字段 private CoffeeMaker maker = null;,其余不动,然后调用 customer.drink()。

122 / 135

你会观察到:程序照常编译、照常启动,直到调用那一刻才炸出 NullPointerException。把这段栈信息抄进笔记,并在心里给它贴个标签——它就是本节报错速查第一行的原型。再进一步的变体:给 Customer 加一个 setMaker(...),先在构造后立刻调用 drink()(炸),再补一句 setMaker(...)(活)。你会发现 setter 注入的代价就是「对象可能存在半成品状态」。

123 / 135
小节
第三档 · 造一个
124 / 135

目标:做一个「三家餐厅 + 一位食客」的小项目,体会容器真正替你做了什么。

125 / 135

要求:定义 Restaurant 接口(dish() 方法),写 HotpotRestaurant、SushiRestaurant、CanteenRestaurant 三个实现;再写一个极简的 MyContainer,内部用一个 Map<String, Restaurant> 存「名字 → 实例」,对外只暴露 pick(String name)。Customer 通过 MyContainer 拿餐厅,完全不 new 任何实现。

126 / 135

验收清单:

127 / 135
  • [ ] Customer 源码里搜不到任何具体实现类名(只有接口名)
  • [ ] 新增第四家餐厅时,只需在 MyContainer 注册,业务类零改动
  • [ ] MyContainer.pick("not-exist") 有明确处理,你能说清这相当于 Spring 的哪个异常
  • [ ] 你能对着这张表讲清四件事分别被谁接管:创建权、装配权、生命周期、替换实现
  • [ ] 写下你的判断:你这个 MyContainer 缺了 Spring 的哪三项能力(提示:作用域、初始化回调、按类型查找)
128 / 135
小节
十九、要点自查
129 / 135
自检

不看资料,用「点外卖」把控制反转讲给一个没写过代码的人听,讲清楚「你交出的是什么、留下的是什么」。

130 / 135
自检

第四节那张表列了反转的四个维度,你能默写出其中三个吗?缺一即回到第四节。

131 / 135
自检

IoC 与 DI 谁是谁的手段?请用一句话回答,且这句话里必须出现「思想」与「实现手段」。

132 / 135
自检

BeanFactory 与 ApplicationContext 最实际的差别是什么?关键词应该是「实例化时机」,而不是「新旧」。

133 / 135
自检

报错速查里 NoUniqueBeanDefinitionException 那条,你能说出两种解法分别在什么场景用吗?(提示:一个是「这次特殊一点」,一个是「平时都以它为准」。)

134 / 135
口诀

点菜不下厨,反转交出创建权;图纸(BeanDefinition)先入册,容器随后送对象。

135 / 135
总结

Spring 用二十年证明了它的价值观——无侵入、可测试、面向接口。它的核心 IoC 只做一件事:把你从「自己 new 一切」中解放出来,让你只声明依赖,把创建与装配交给容器。理解了这一层,Spring 的所有模块(AOP、事务、MVC、Boot、Cloud)都只是这套容器上叠加的能力,而不再是一堆需要死记的注解。