国庆节10月1日
--


讲清耦合与内聚这对概念、软件设计原则"高内聚低耦合",以及 Spring 给出的两把钥匙——控制反转(IOC,把对象创建权交给容器)与依赖注入(DI,容器在运行时把依赖送进来);再照着课程把三层里的三处 new 换成 @Component + @Autowired,附改造后接口不变的实测
.webp)
上一篇(三层架构)把代码按职责拆成了三层,接口没变、代码也清爽了。但拆完的三层之间还留着三处”焊死的接头”:
1private UserService userService = new UserServiceImpl(); // controller 里2private UserDao userDao = new UserDaoImpl(); // service 里这一篇就处理它们:分层解耦 + IOC & DI 入门(PPT 第 55-63 页)。PPT 第 55 页那张小目录(三层架构 → 分层解耦 → IOC & DI 入门 → IOC 详解 → DI 详解)里,本篇覆盖中间两个,前面讲”为什么要解耦”,后面讲”代码怎么改”;下一篇再钻注解与注入方式的细节。
PPT 第 56 页给了定义,都很短,但要能翻译成自己的话:
耦合:衡量软件中各个层/各个模块的依赖关联程度。
内聚:软件中各个功能模块内部的功能联系。
软件设计原则:高内聚低耦合。
| 词 | 在说什么 | 高一点好还是低一点好 |
|---|---|---|
| 耦合 | 模块之间牵手的紧密程度——A 有多依赖 B 的具体实现 | 越低越好(换掉 B 时尽量不用动 A) |
| 内聚 | 一个模块内部的各部分是不是在干同一件事 | 越高越好(一件事的代码都在一处) |
对照这个案例:
UserServiceImpl 这个具体类名,service 知道 UserDaoImpl 这个具体类名。
UserServiceImpl2 里写着 private final UserDao userDao = new UserDaoImpl();:业务层自己创建数据访问层的具体实现类(它的兄弟 UserServiceImpl 里是同样的写法),这就是”层与层耦合”的现场PPT 第 57 页画了一张示意图:一堆 1、2 的圈圈(代表各个对象)被画进一个容器里,旁边写着控制反转、依赖注入两个词,还有两个问号——意思是”这些对象的创建和依赖关系,到底该由谁来管?”
先把”现在是谁在管”看清楚。三层里每一处 new,都是一条写死的依赖:
1UserController ──new──▶ UserServiceImpl ──new──▶ UserDaoImpl2 (控制层) (业务层) (数据访问层)自己 new 意味着:用哪个实现类,是在写下这一行的时候就定死的。于是就有了这些麻烦:
| 场景 | 现在会发生什么 |
|---|---|
业务规则变了,新写了 UserServiceImpl2,想用它 | 得改 UserController 的源码、重新编译(把 new UserServiceImpl() 换成 new UserServiceImpl2()) |
数据来源要从文件换成数据库,写了 UserDaoImplForMySQL | 得改 UserServiceImpl 的源码 |
| 想给某个实现类加一层包装/换个实现做测试 | 同样得改上层源码 |
一句话:上层不但”用”下层,还管起了下层”怎么出生”——这不合理。合理的分工是:上层只管”我需要一个能干这些活的对象”,至于”谁来做、怎么创建”,交给一个专门的容器。
PPT 第 58 页把答案一次给全:
控制反转:Inversion Of Control,简称 IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转。
依赖注入:Dependency Injection,简称 DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入。
Bean 对象:IOC 容器中创建、管理的对象,称之为 Bean。
拆开看这三句话:
| 概念 | 关键动作 | 谁变了 |
|---|---|---|
| IOC(控制反转) | 对象的创建控制权从”程序自己”转移给”外部容器” | 以前是 new UserServiceImpl(),现在由容器负责创建对象 |
| DI(依赖注入) | 容器在运行时把对象所需要的资源提供给它 | 以前 private UserDao userDao = new UserDaoImpl();,现在容器把 userDao 塞进来 |
| Bean | IOC 容器里创建、管理的那些对象 | UserServiceImpl、UserDaoImpl 这些对象从此叫 Bean |
两个词其实是同一件事的两面:“创建权交出去”是 IOC,“送进来”是 DI。站在对象的角度想最直观——UserServiceImpl 不再自己去找 UserDao,而是”张嘴等着”容器把 UserDao 送进来。
| PPT 的问题 | 答案 |
|---|---|
| 实现分层解耦的思路是什么? | ① 将项目中的类交给 IOC 容器管理(IOC,控制反转);② 应用程序运行时需要什么对象,直接依赖容器为其提供(DI,依赖注入) |
PPT 第 61、62 两页把改造归纳成两件事:
① 将 Dao 及 Service 层的实现类,交给 IOC 容器管理。 ② 为 Controller 及 Service 注入运行时所依赖的对象。
落到代码上,就是”加一个注解交出创建权”和”加一个注解等依赖送上门”。
1package com.itheima.dao.impl;2
3import cn.hutool.core.io.IoUtil;4import com.itheima.dao.UserDao;5import org.springframework.stereotype.Component;6
7import java.io.InputStream;8import java.util.ArrayList;9import java.util.List;10
11@Component // 将当前类交给 Spring 管理,声明为 Spring 容器中的 bean 对象12public class UserDaoImpl implements UserDao {13
14 @Override15 public List<String> list() {16 // 读取 user.txt 中的数据17 InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");18 return IoUtil.readUtf8Lines(in, new ArrayList<>());19 }20}1package com.itheima.service.impl;2
3import com.itheima.dao.UserDao;4import com.itheima.pojo.User;5import com.itheima.service.UserService;6import org.springframework.beans.factory.annotation.Autowired;7import org.springframework.stereotype.Component;8
9import java.time.LocalDateTime;10import java.time.format.DateTimeFormatter;11import java.util.List;12
13@Component // 交给 IOC 容器管理14public class UserServiceImpl implements UserService {15
16 @Autowired // 自动装配:应用程序在运行时,会自动的从容器中找到该类型的对象,并赋值给该变量17 private UserDao userDao;18
19 @Override20 public List<User> list() {21 // 1. 调用 dao 层,获取数据22 List<String> lines = userDao.list();23
24 // 2. 业务逻辑处理:解析数据,封装 User 对象 → List<User>25 List<User> userList = lines.stream().map(line -> {26 String[] split = line.split(",");27 return new User(28 Integer.parseInt(split[0]), split[1], split[2], split[3],29 Integer.parseInt(split[4]),30 LocalDateTime.parse(split[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));31 }).toList();32 return userList;33 }34}1package com.itheima.controller;2
3import com.itheima.pojo.User;4import com.itheima.service.UserService;5import org.springframework.beans.factory.annotation.Autowired;6import org.springframework.web.bind.annotation.RequestMapping;7import org.springframework.web.bind.annotation.RestController;8
9import java.util.List;10
11@RestController12public class UserController {13
14 @Autowired // 运行时由容器把 UserService 的对象送进来15 private UserService userService;16
17 @RequestMapping("/list")18 public List<User> list() {19 // 1. 调用 service,查询用户信息20 List<User> userList = userService.list();21
22 // 2. 响应数据23 return userList;24 }25}改的只是三行代码,但性质完全变了:
| 改造前 | 改造后 | |
|---|---|---|
| controller 里的 service | private UserService userService = new UserServiceImpl(); | @Autowired + private UserService userService; |
| service 里的 dao | private UserDao userDao = new UserDaoImpl(); | @Autowired + private UserDao userDao; |
| dao 实现类 | 无注解 | 类上加 @Component |
| service 实现类 | 无注解 | 类上加 @Component |
| 谁创建对象 | 程序自己 new | IOC 容器(创建权交出去了 → 控制反转) |
| 依赖怎么来 | 自己 new 出来 | 容器运行时注入(→ 依赖注入) |
| 换一个实现类 | 改上层源码 | 应用层代码不用动,交给容器决定(下一篇细讲怎么指定) |
两个注解的位置别放错:
@Component 加在实现类上(UserDaoImpl、UserServiceImpl),不是接口上——容器创建的是对象,接口是不能被实例化的;@Autowired 加在需要依赖的那个成员变量上(controller 里的 userService、service 里的 userDao)——它说的是”我这里要一个对象,容器帮我找找”。| PPT 的问题 | 答案 |
|---|---|
| 如何将一个类交给 IOC 容器管理? | @Component(注意:是加在实现类上,而非接口上) |
| 如何从 IOC 容器中找到该类型的 bean,然后完成依赖注入? | @Autowired |
改造是”换内部实现”,对外表现必须一样。本机把课程最终代码(三层 + IOC/DI 改造后)跑起来:
本机实测(Spring Boot 3.2.8 / 内嵌 Tomcat / JDK 17)
1$ curl -si "http://localhost:8080/list" | head -42HTTP/1.1 2003Content-Type: application/json4Transfer-Encoding: chunked页面 user.html 照旧渲染出用户表格——没有 new 之后,功能一点没少:容器把 UserDaoImpl 创建好、注入 UserServiceImpl,再把 UserServiceImpl 创建好、注入 UserController,这一串都在应用启动时自动完成。
顺手记一个实测细节,它是下一篇的引子:交给容器的 bean,名字默认是”类名首字母小写”——UserServiceImpl 在容器里叫 userServiceImpl、UserDaoImpl 叫 userDaoImpl(本机实测里 @Qualifier("userServiceImpl")、@Resource(name = "userServiceImpl2") 能生效,靠的就是这条默认命名规则)。
改造后如果启动报”找不到 bean”(类似 No qualifying bean of type 'com.itheima.dao.UserDao' available),先回头检查两件事:① 实现类上的 @Component 是不是漏了(或者错加到了接口上);② 那个类是不是放在启动类所在包之外——这两条下一篇都会亲手做实验。
| 问题 | 答案 |
|---|---|
| 耦合是什么? | 衡量软件中各个层/各个模块的依赖关联程度(越低越好) |
| 内聚是什么? | 软件中各个功能模块内部的功能联系(越高越好) |
| 软件设计原则? | 高内聚低耦合 |
| 现在代码的耦合点在哪? | 三处 new:controller 里 new UserServiceImpl()、service 里 new UserDaoImpl();换实现类就得改上层源码 |
| IOC 是什么? | 控制反转(Inversion Of Control)——对象的创建控制权由程序自身转移到外部(容器) |
| DI 是什么? | 依赖注入(Dependency Injection)——容器为应用程序提供运行时所需要的依赖资源 |
| Bean 是什么? | IOC 容器中创建、管理的对象 |
| 分层解耦的思路? | ① 把项目中的类交给 IOC 容器管理(IOC);② 运行时需要什么对象,直接依赖容器提供(DI) |
| 改造的两件事? | ① 将 Dao 及 Service 层的实现类交给 IOC 容器管理(实现类上加 @Component);② 为 Controller 及 Service 注入运行时所依赖的对象(成员变量上加 @Autowired) |
| 改造后有什么变化? | 三处 new 全没了;对象由容器创建、依赖由容器注入;对外接口不变(实测 /list 仍是 200 + application/json) |
new——controller 里 new UserServiceImpl()、service 里 new UserDaoImpl();上层不但”用”下层的功能,还决定了下层”怎么出生”(具体用哪个实现类、什么时候创建),用哪个实现类是写死的,换实现就得改上层源码、重新编译UserServiceImpl、UserDaoImpl)@Component 加在实现类上(不是接口上)——声明这个类交给容器管理;@Autowired 加在需要依赖的成员变量上——运行时由容器按类型找到对应的 bean 注入进来new 全部消失,变成”接口类型 + @Autowired”;对象由容器创建、依赖由容器注入;对外接口与响应完全不变(实测 /list 仍是 HTTP/1.1 200 + Content-Type: application/json)UserServiceImpl → userServiceImpl,UserDaoImpl → userDaoImpl(下一篇的 @Qualifier("userServiceImpl")、@Resource(name = ...) 用的就是它)2-1 找出这几行代码里的耦合点 下面是一个三层架构工程的三个片段(都是改造前的写法):
1// 片段一2@RestController3public class UserController {4 private UserService userService = new UserServiceImpl();5 // ……6}7
8// 片段二9public class UserServiceImpl implements UserService {10 private UserDao userDao = new UserDaoImpl();11 // ……12}请回答:
UserServiceImpl2,想换掉原来的实现。改造前你要改哪些文件?改造后(把依赖交给容器)又要改哪些?把这个对比写出来。
(练习文件 test_38_找出耦合点.java 里给了写作区。)一级 · 思路:盯住每个成员变量的等号右边——那是”具体用谁”的决定权;再想”这个决定权出现在哪一层”是不是合适
二级 · 方法:耦合点就是两处 new XxxImpl();改造后这两行只剩”接口类型 + @Autowired”,具体实现由容器决定
三级 · 骨架:改造后应当长成 @____ private UserService userService; / @____ private UserDao userDao;
1private UserService userService = new UserServiceImpl();2private UserDao userDao = new UserDaoImpl();UserService / UserDao),可等号右边把”具体用哪个实现类”也写死了——上层替下层做了”用谁、怎么创建”的决定。上层原本只需要知道”有这么一个能干活的接口”,现在却知道了下层的类名和构造方式;下层一换(或想换),上层的源码就得跟着改。这就是”依赖关联程度高”(耦合高)。| 要改的地方 | |
|---|---|
| 改造前 | 要改 UserController 的源码(把 new UserServiceImpl() 换成 new UserServiceImpl2()),然后重新编译运行 |
| 改造后(依赖交给容器) | 上层代码不用动(只有接口类型 + @Autowired);换成哪个实现由容器决定——在实现类上标 @Primary、或注入处用 @Qualifier / @Resource 指定(下一篇实测三招) |
一句话:改造前是”上层指哪打哪”,改造后是”上层提需求、容器给货”。
2-2 给三层加上 IOC/DI 改造
下面是一个能跑的三层工程(拆过层,但依赖全是自己 new 的):
1// dao 层实现类2public class UserDaoImpl implements UserDao {3 @Override4 public List<String> list() {5 InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");6 return IoUtil.readUtf8Lines(in, new ArrayList<>());7 }8}1// service 层实现类2public class UserServiceImpl implements UserService {3 private UserDao userDao = new UserDaoImpl();4 @Override5 public List<User> list() {6 List<String> lines = userDao.list();7 return lines.stream().map(line -> { /* 解析封装…… */ }).toList();8 }9}1// controller2@RestController3public class UserController {4 private UserService userService = new UserServiceImpl();5 @RequestMapping("/list")6 public List<User> list() {7 return userService.list();8 }9}要求(题面只说需求,两个注解的名字自己想):
UserController 里还剩几个 new?
(练习文件 test_38_分层解耦改造.java 里按 dao / service / controller 三块给了写作区。)一级 · 思路:两件事分开做——“谁可以被容器创建”(在实现类上标一个注解)、“谁需要别人送我对象”(在成员变量上标一个注解)
二级 · 方法:把类交给容器用 @Component(加在实现类的 public class 上一行);要依赖用 @Autowired(加在成员变量上一行);成员变量只留”接口类型 + 名字”,等号右边整个删掉
三级 · 骨架:@____ public class UserDaoImpl implements UserDao { ... } / @____ private UserDao userDao; / @____ private UserService userService;
1// ============ dao 层实现类:交给容器 ============2@Component // 原来没人管,现在交给 IOC 容器创建、管理(成为 bean)3public class UserDaoImpl implements UserDao {4 @Override5 public List<String> list() {6 InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");7 return IoUtil.readUtf8Lines(in, new ArrayList<>());8 }9}1// ============ service 层实现类:交给容器 + 注入 dao ============2@Component // 交给 IOC 容器管理3public class UserServiceImpl implements UserService {4
5 @Autowired // 原来是自己 new UserDaoImpl(),现在由容器按类型找到 UserDaoImpl 的 bean 送进来6 private UserDao userDao;7
8 @Override9 public List<User> list() {10 List<String> lines = userDao.list();11 return lines.stream().map(line -> { /* 解析封装…… */ }).toList();12 }13}1// ============ controller:注入 service ============2@RestController3public class UserController {4
5 @Autowired // 原来是自己 new UserServiceImpl(),现在由容器注入6 private UserService userService;7
8 @RequestMapping("/list")9 public List<User> list() {10 return userService.list();11 }12}UserDaoImpl、UserServiceImpl 创建成 bean,再把它们分别注入需要的地方;之后每次请求只是正常调用方法,不会再重新创建对象);② 改造后 UserController 里 new 的数量是 0——这正是”控制反转”的直观标志。2-3 改错题:三处改造没改对 小李照着步骤做分层解耦改造,写了下面三处代码,结果工程要么启动报错、要么运行起来空指针。请逐条指出错在哪、会出什么现象、怎么改:
1// 第 ① 处2@Component3public interface UserDao {4 public List<String> list();5}6
7// 第 ② 处8public class UserDaoImpl implements UserDao {9 @Override10 public List<String> list() { /* ……照常读文件…… */ }11}12
13// 第 ③ 处14public List<User> list() {15 @Autowired16 UserService userService; // 写在方法里的局部变量上17 return userService.list();18}(练习文件 test_38_改错题.java 里给了三处代码和写作区。)
一级 · 思路:第 ① 处把注解加在了”不能创建对象的东西”上;第 ② 处少了让容器认领这个类的标记;第 ③ 处注解的位置超出了它管得着的范围
二级 · 方法:@Component 要加在实现类上(接口上加等于没加,容器造不出 bean);@Autowired 要加在成员变量(或构造方法、setter 方法)上,不能标在方法体内的局部变量上
三级 · 骨架:@Component public class UserDao____ implements UserDao {...} / @Autowired private ____ userService;(放在类里,方法外面)
① @Component 加在了接口上:接口不能被实例化,容器不会为它创建 bean。现象:上层注入 UserDao 类型时找不到可用的 bean,启动直接失败(报 No qualifying bean of type 'com.itheima.dao.UserDao' available 这一类错误)。改法:把注解挪到实现类上——
1public interface UserDao { List<String> list(); } // 接口不需要注解2
3@Component4public class UserDaoImpl implements UserDao { ... } // 注解加在实现类上② UserDaoImpl 漏了 @Component:这个类根本没交给容器,容器里没有它的 bean。现象和 ① 一样——注入时报”找不到类型为 UserDao 的 bean”(如果 service 里也还是 new UserDaoImpl(),那就是”改造没生效”,注入不生效的问题被掩盖了)。改法:在 UserDaoImpl 类上加 @Component。
③ @Autowired 写在了方法体内的局部变量上:容器只认识”类里的成员”(成员变量、构造方法、setter 方法),管不到方法执行到那一行时的局部变量。现象:注解完全无效,userService 是 null,调用 userService.list() 时抛 NullPointerException。改法:把依赖声明成成员变量,注解也标在成员变量上——
1@RestController2public class UserController {3
4 @Autowired // 成员变量上(类里、方法外)5 private UserService userService;6
7 @RequestMapping("/list")8 public List<User> list() {9 return userService.list();10 }11}3-1 给三层架构案例加上 IOC/DI 改造 在上一篇(37 篇)拆好三层的工程上继续做,每一步都保留”能跑”的状态:
curl -si http://localhost:8080/list,把改造前的状态码与 Content-Type 抄下来(作为基准);@Component,交出创建权;private UserDao userDao = new UserDaoImpl(); 改成”接口类型 + @Autowired”,并在实现类上补 @Component;private UserService userService = new UserServiceImpl(); 改成”接口类型 + @Autowired”;curl -si .../list 看状态码与 Content-Type、浏览器打开 user.html 看表格、在启动日志/断点里确认对象是容器给的(不是 null);new 从几处变成几处、对外响应有没有变;UserServiceImpl2(实现同一个接口,把返回的 id 都加 200),类上也标 @Component,重启后观察接口返回的 id——如果启动直接失败,把报错抄到练习文件末尾(这个报错正是下一篇要处理的问题)。
(练习文件 test_38_综合_IOC-DI改造.java 里按这 7 步给了写作区。)涉及知识点
| 知识点 | 在这里的应用 |
|---|---|
| 耦合/内聚 | 三处 new 是耦合点;把创建权交出去降低耦合 |
| IOC | 实现类上加 @Component,对象由容器创建 |
| DI | 成员变量上加 @Autowired,容器运行时注入 |
| Bean | 被容器创建/管理的 UserDaoImpl、UserServiceImpl 都成了 bean |
| 接口与实现 | 成员变量类型仍是接口,实现由容器决定 |
| 重构验证 | 对外接口与响应与改造前一致 |
一级 · 思路:改造永远从最底层往上做(先 dao、再 service、最后 controller),每改一层就把依赖它的那一层跟着改掉 new
二级 · 方法:实现类加 @Component;需要依赖的成员变量改成 @Autowired + 接口类型(删掉等号右边);验证照旧用 curl -si(看头)和浏览器(看页面)
三级 · 骨架:@____ public class UserServiceImpl implements UserService { @____ private UserDao userDao; ... } / 换实现后的报错关键词 = required a single bean, but 2 were found
3-1 分步结果:
1HTTP/1.1 2002Content-Type: application/json3Transfer-Encoding: chunked@Component public class UserDaoImpl implements UserDao { ... }1@Component2public class UserServiceImpl implements UserService {3 @Autowired4 private UserDao userDao;5 // ……6}1@RestController2public class UserController {3 @Autowired4 private UserService userService;5 // ……6}/list 仍是 200 + application/json;user.html 表格仍是 8 行;userService、userDao 都不是 null(能被容器注入,说明它们确实是 bean)。| 改造前 | 改造后 | |
|---|---|---|
| 改动文件 | — | 3 个(UserDaoImpl、UserServiceImpl、UserController) |
new 的处数 | 2 | 0 |
| 对外响应 | 200 + application/json | 完全一样 |
1Field userService in com.itheima.controller.UserController required a single bean, but 2 were found:2 - userServiceImpl: defined in URL [...]3 - userServiceImpl2: defined in URL [...]@Autowired 默认按类型注入遇到”同类型两个 bean”时的正常反应——本机实测的完整日志见下一篇,那里会给三个解决方案(@Primary / @Qualifier / @Resource)。如果你喜欢,那么欢迎来到我的世界!
了解更多暂未播放



"所有的相遇都是久别重逢。"—— 未知


