国庆节10月1日
--


讲清"为什么要把代码拆成 controller / service / dao 三层"——从上一篇那个"一个方法干了三件事"的接口说起,按单一职责原则拆出三层,写清每层的职责、包结构与代码骨架,再用拆分前后的复用性与维护性对比说明这样拆到底赚在哪,附拆分后接口响应不变的实测
.webp)
上一篇(SpringBoot Web案例-用户列表渲染)把用户列表接口做出来了:访问 /list 能拿到用户 JSON,页面也能渲染成表格。但 PPT 第 47 页在那段代码旁边留了两个词——复用性差、难以维护。
这一篇就动手解决它:把代码拆成 三层架构(PPT 第 48-54 页)。第 48 页是这一节的开场导航(还是那四块内容),第 49 页则把”分层解耦”这一节的五个话题列全了(三层架构 → 分层解耦 → IOC & DI 入门 → IOC 详解 → DI 详解)——本篇讲第一个,也是后面三篇的地基。
PPT 第 50 页把上一篇的 UserController 原样放出来,然后在方法体里标出三种颜色的职责:
| 方法里的一段 | 它实际在干的事 | PPT 的标注 |
|---|---|---|
getResourceAsStream("user.txt") + IoUtil.readLines(...) | 把文件读进来 | 数据访问 |
lines.stream().map(line -> {...}) | 把文本解析成对象、组装成集合 | 逻辑处理 |
@RequestMapping("/list") + return userList; | 接收请求、响应数据 | 接收请求、响应数据 |

@RestController 的 UserController 里,读文件、解析封装、返回数据全挤在一个 list2() 方法里;右边配的评语是”复用性差、难以维护”,正是这段代码要被拆掉的原因PPT 第 50 页把这种做法判为违反 单一职责原则:
单一职责原则——一个类(一个方法)只负责一件事。
一个”负责接收请求”的类,却知道”文件在哪、编码是 UTF-8、时间格式是 yyyy-MM-dd HH:mm:ss”,这就是职责串味了。后果上一篇算过账:
PPT 第 51 页给了三层架构的标准定义,一句话一层:
| 层 | 名字 | 职责(PPT 原文) |
|---|---|---|
| controller | 控制层 | 接收前端发送的请求,对请求进行处理,并响应数据 |
| service | 业务逻辑层 | 处理具体的业务逻辑 |
| dao | 数据访问层(Data Access Object),也叫持久层 | 负责数据访问操作,包括数据的增、删、改、查 |
调用方向是单向的一条线:
1浏览器 ──请求──▶ Controller(接收请求、响应数据)2 │3 ▼4 Service(业务逻辑处理)5 │6 ▼7 Dao(数据访问:读/写数据)对着上一篇那段代码”对号入座”:
| 原来挤在一起的三块 | 拆完以后归谁 |
|---|---|
读 user.txt | dao(“数据访问操作”) |
把每行文本解析成 User 对象、装成 List | service(“业务逻辑处理”) |
@RequestMapping("/list")、把集合返回给浏览器 | controller(“接收请求、响应数据”) |
三层是逻辑上的划分,不一定等于”三个工程”。这个案例里三层都在同一个 SpringBoot 工程里,只靠包分开。
拆完之后的包结构(课程代码 springboot-web-demo):
1src/main/java/com/itheima/2├── controller/ ← 控制层3│ └── UserController.java (处理请求、响应数据)4├── service/ ← 业务逻辑层5│ ├── UserService.java (接口:定义"能做什么业务")6│ └── impl/7│ └── UserServiceImpl.java (实现类:业务逻辑怎么写)8├── dao/ ← 数据访问层9│ ├── UserDao.java (接口:定义"要哪些数据操作")10│ └── impl/11│ └── UserDaoImpl.java (实现类:真正去读/写数据)12├── pojo/ ← 实体类13│ └── User.java14└── SpringbootWebDemoApplication.java两个约定:
UserService / UserServiceImpl,UserDao / UserDaoImpl)——上一层的代码只依赖接口,不依赖具体实现(这一条在下一篇 分层解耦里会变成主角);XxxController(控制层)、XxxService + XxxServiceImpl(业务层)、XxxDao + XxxDaoImpl(数据访问层)。看到类名就知道该去哪一层找它。dao 层的活最单纯:读数据、写数据,不掺业务。这个案例的数据在 user.txt 里,所以 dao 就负责把文件读成”一行一行”的文本。
接口(定义要做的事):
1package com.itheima.dao;2
3import java.util.List;4
5public interface UserDao {6
7 /**8 * 加载用户数据9 */10 public List<String> findAll();11
12}实现类(真正做事的地方):
1package com.itheima.dao.impl;2
3import cn.hutool.core.io.IoUtil;4import com.itheima.dao.UserDao;5
6import java.io.InputStream;7import java.util.ArrayList;8import java.util.List;9
10/**11 * 数据访问 - 数据的增删改查12 */13public class UserDaoImpl implements UserDao {14
15 @Override16 public List<String> findAll() {17 // 读取 user.txt 中的数据18 InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");19 ArrayList<String> lines = IoUtil.readUtf8Lines(in, new ArrayList<>());20 return lines;21 }22}
UserDaoImpl 里只剩”读文件”这一件事;以后数据来源换成数据库,改的就是这个类(换掉读文件那两行),而 Controller、Service 一行都不用动IoUtil.readUtf8Lines(in, new ArrayList<>()) 是 IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>()) 的简写版(默认就是 UTF-8),上一篇用的是完整写法,两种等价。课程代码在”三层架构拆分”这一版用的是简写,最终代码用的是完整写法。
service 层的活是处理业务。这个案例里,“把一行行文本解析成一个个 User 对象”就是业务逻辑——因为”用户长什么样、年龄是数字、时间是 LocalDateTime”属于业务规则。
接口:
1package com.itheima.service;2
3import com.itheima.pojo.User;4
5import java.util.List;6
7public interface UserService {8
9 /**10 * 查询所有用户11 */12 public List<User> list();13
14}实现类(调 dao 拿数据 + 解析封装):
1package com.itheima.service.impl;2
3import com.itheima.dao.UserDao;4import com.itheima.dao.impl.UserDaoImpl;5import com.itheima.pojo.User;6import com.itheima.service.UserService;7
8import java.time.LocalDateTime;9import java.time.format.DateTimeFormatter;10import java.util.List;11
12public class UserServiceImpl implements UserService {13
14 private UserDao userDao = new UserDaoImpl(); // 它需要 dao 帮它拿数据15
16 @Override17 public List<User> list() {18 // 1. 调用 dao 层,获取数据19 List<String> lines = userDao.list();20
21 // 2. 业务逻辑处理:解析数据,封装 User 对象 → List<User>22 List<User> userList = lines.stream().map(line -> {23 String[] split = line.split(",");24 return new User(25 Integer.parseInt(split[0]),26 split[1],27 split[2],28 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}
UserServiceImpl 先调 dao 拿到文本(userDao.list()),再把文本解析封装成 List<User>;读文件的活已经不在它这儿了,它只关心”数据长什么样、怎么加工”service 层一共就三个动作,读代码时按这个顺序看:
1① List<String> lines = userDao.list(); ← 要数据(找 dao)2② lines.stream().map(...) ← 加工(业务逻辑)3③ return userList; ← 交差(交给 controller)上一层的 private UserDao userDao = new UserDaoImpl(); 就是耦合的样子(自己 new 出下一层的实现类)——先记住它,下一篇第一个要收拾的就是它。
到了控制层,代码就”薄”下来了:接收请求 → 调 service → 返回结果,一行多余的事都不做。
1package com.itheima.controller;2
3import com.itheima.pojo.User;4import com.itheima.service.UserService;5import com.itheima.service.impl.UserServiceImpl;6import org.springframework.web.bind.annotation.RequestMapping;7import org.springframework.web.bind.annotation.RestController;8
9import java.util.List;10
11/**12 * 用户信息Controller13 */14@RestController15public class UserController {16
17 private UserService userService = new UserServiceImpl(); // 它需要 service 帮它处理业务18
19 @RequestMapping("/list")20 public List<User> list() {21 // 1. 调用 service22 List<User> userList = userService.list();23
24 // 2. 响应数据25 return userList;26 }27}
UserController 只剩三样东西:@RestController(这是个请求处理类)、@RequestMapping("/list")(访问路径)、一句 userService.list();读文件与解析的代码全都不见了对比一下拆分前后的同一个方法——从 13 行变成 3 行:
1// 拆分前:读文件 + 解析 + 返回,全在 Controller(第 53 页左边)2List<User> userList = lines.stream().map(line -> {3 String[] parts = line.split(",");4 Integer id = Integer.parseInt(parts[0]);5 /* …… 还有 8 行 …… */6}).collect(Collectors.toList());7return userList;8
9// 拆分后:只留"调用 + 返回"(第 53 页右边)10List<User> userList = userService.list();11return userList;PPT 第 53 页用一张 VS 图给结论:
拆分前(一个 UserController 全包) | 拆分后(controller / service / dao 三层) | |
|---|---|---|
| 复用性 | 差:别的接口要用户数据只能复制粘贴 | 强:UserService 谁的能调、UserDao 谁的能调,一处写好处处能用 |
| 维护性 | 差:换数据来源、改时间格式都要动 Controller | 方便:换数据来源只动 dao;改业务规则只动 service;改接口地址/请求方式只动 controller |
| 职责 | 一个类三件事,违反单一职责原则 | 一层一件事,各管各的 |
把”改哪里”这件事具体化(这也是面试里最常问的”分层的好处”):
| 需求变化 | 拆分前要改 | 拆分后要改 |
|---|---|---|
用户数据从 user.txt 换成数据库 | Controller | UserDaoImpl(数据访问层) |
| 解析规则变了(比如匿名化密码) | Controller | UserServiceImpl(业务层) |
接口路径从 /list 改成 /users | Controller | UserController(控制层) |
| 新增一个”按 id 查用户”的接口 | 再抄一份读文件+解析 | UserController 加个方法,service/dao 里的活能复用就复用 |
三层架构是”内部整理”,对外表现必须原封不动。本机把课程最终代码(在三层架构基础上还做了 IOC/DI 改造,见下一篇)跑起来,/list 的响应是:
本机实测(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状态码、响应头、JSON 结构都和拆分前完全一样(36 篇里那条”实测”就是这个接口)。分层是重构,不是改需求——只要对外的接口合同不变,前端页面一行都不用动。
| PPT 的问题 | 答案 |
|---|---|
| 为什么要对代码进行拆分? | 遵循单一职责原则,便于复用、后期维护 |
| 拆分为了哪三层?每一层的职责是什么? | controller:接受请求,响应数据;service:逻辑处理;dao:数据访问 |
| 问题 | 答案 |
|---|---|
| 为什么要拆? | 原来一个 Controller 方法干了三件事(数据访问 / 逻辑处理 / 接收请求响应数据),复用性差、难以维护;拆层是为了单一职责原则,便于复用与维护 |
| 三层各管什么? | **controller(控制层)**接收前端请求、处理并响应数据;**service(业务逻辑层)**处理具体业务逻辑;**dao(数据访问层 / 持久层)**负责数据访问操作(增删改查) |
| 调用方向? | 浏览器 → Controller → Service → Dao(单向,dao 不会反过来调 service) |
| 工程里怎么落地? | 靠包分开:controller / service + service.impl / dao + dao.impl / pojo;每层先定义接口再写实现类(UserService / UserServiceImpl) |
| dao 层写什么? | 只做数据访问:这个案例就是 getResourceAsStream("user.txt") + IoUtil 按行读,返回 List<String> |
| service 层写什么? | 调 dao 拿数据 → 业务处理(stream().map(...) 解析封装成 List<User>)→ 返回 |
| controller 层写什么? | @RestController + @RequestMapping("/list"),方法里只留 userService.list() 和 return |
| 拆完效果如何? | 换数据来源只动 dao、改业务规则只动 service、改接口只动 controller;对外接口与响应完全不变(实测 /list 仍是 200 + application/json) |
com.itheima 下 controller(控制层)、service + service.impl(业务层接口与实现)、dao + dao.impl(数据访问层接口与实现)、pojo(实体类);类名体现层:XxxController、XxxService/XxxServiceImpl、XxxDao/XxxDaoImplgetResourceAsStream("user.txt") + IoUtil.readUtf8Lines(in, new ArrayList<>())——把数据读出来,返回 List<String>(一行一个元素),不做任何业务加工List<String> lines = userDao.list(); 拿数据 → lines.stream().map(line -> {...}).toList() 解析封装成 List<User> → return userList;(业务逻辑就在 map 里)@RestController 标识请求处理类、@RequestMapping("/list") 定路径,方法体只留两行——List<User> userList = userService.list(); 和 return userList;/list 仍是 HTTP/1.1 200 + Content-Type: application/json2-1 把”全塞在 Controller 里”的代码拆成三层 下面这个类是上一个练习写出来的(能跑,但所有事都挤在一起):
1@RestController2public class UserController {3
4 @RequestMapping("/list")5 public List<User> list() throws Exception {6 // 1. 读文件7 InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");8 ArrayList<String> lines = IoUtil.readLines(in, StandardCharsets.UTF_8, new ArrayList<>());9
10 // 2. 解析封装11 List<User> userList = lines.stream().map(line -> {12 String[] parts = line.split(",");13 Integer id = Integer.parseInt(parts[0]);14 String username = parts[1];15 String password = parts[2];16 String name = parts[3];17 Integer age = Integer.parseInt(parts[4]);18 LocalDateTime updateTime = LocalDateTime.parse(parts[5],19 DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));20 return new User(id, username, password, name, age, updateTime);21 }).collect(Collectors.toList());22
23 // 3. 响应数据24 return userList;25 }26}请把它按三层架构拆开,写出拆分后每一层的代码(含包名):
package 上一行用中文注释写清”这是哪一层、职责是什么”。
(练习文件 test_37_三层架构拆分.java 里按 dao / service / controller 三块给了写作区。)一级 · 思路:先按”读文件 → 加工 → 返回”把原方法切成三段,再从下往上写:最底下放”读文件”,中间放”加工”,最上面只留”调用 + 返回”
二级 · 方法:包用 com.itheima.dao(.impl) / com.itheima.service(.impl) / com.itheima.controller;dao 接口 UserDao 的方法返回 List<String>(一行一条),实现类 UserDaoImpl 里读文件;service 接口 UserService 的方法返回 List<User>,实现类 UserServiceImpl 里 new UserDaoImpl() + 解析;controller 里 new UserServiceImpl() + 调 list();注解仍然是 @RestController + @RequestMapping("/list")
三级 · 骨架:dao:public interface UserDao { List<String> findAll(); } + class UserDaoImpl implements UserDao { ...读文件... };service:public interface UserService { List<User> findAll(); } + class UserServiceImpl implements UserService { private UserDao userDao = new ____(); ... };controller:class UserController { private UserService userService = new ____(); @RequestMapping("/list") public List<User> list(){ List<User> userList = ____.findAll(); return ____; } }
拆完是四个文件(包名按课程习惯):
1// ============ 数据访问层 dao ============2package com.itheima.dao;3
4import java.util.List;5
6public interface UserDao {7 /** 加载用户数据(一行一条文本) */8 public List<String> findAll();9}1// ============ 数据访问层 dao 的 实现 ============2package com.itheima.dao.impl;3
4import cn.hutool.core.io.IoUtil;5import com.itheima.dao.UserDao;6
7import java.io.InputStream;8import java.util.ArrayList;9import java.util.List;10
11public class UserDaoImpl implements UserDao {12 @Override13 public List<String> findAll() {14 InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");15 return IoUtil.readUtf8Lines(in, new ArrayList<>());16 }17}1// ============ 业务逻辑层 service 的 实现 ============2package com.itheima.service.impl;3
4import com.itheima.dao.UserDao;5import com.itheima.dao.impl.UserDaoImpl;6import com.itheima.pojo.User;7import com.itheima.service.UserService;8
9import java.time.LocalDateTime;10import java.time.format.DateTimeFormatter;11import java.util.List;12
13public class UserServiceImpl implements UserService {14
15 private UserDao userDao = new UserDaoImpl(); // 业务层需要 dao 帮它取数据16
17 @Override18 public List<User> findAll() {19 List<String> lines = userDao.findAll(); // ① 要数据20 List<User> userList = lines.stream().map(line -> { // ② 业务加工21 String[] parts = line.split(",");22 return new User(23 Integer.parseInt(parts[0]), parts[1], parts[2], parts[3],24 Integer.parseInt(parts[4]),25 LocalDateTime.parse(parts[5], DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));26 }).toList();27 return userList; // ③ 交差28 }29}1// ============ 控制层 controller ============2package com.itheima.controller;3
4import com.itheima.pojo.User;5import com.itheima.service.UserService;6import com.itheima.service.impl.UserServiceImpl;7import org.springframework.web.bind.annotation.RequestMapping;8import org.springframework.web.bind.annotation.RestController;9
10import java.util.List;11
12@RestController13public class UserController {14
15 private UserService userService = new UserServiceImpl(); // 控制层需要业务层帮它办事16
17 @RequestMapping("/list")18 public List<User> list() {19 List<User> userList = userService.findAll(); // ① 调 service20 return userList; // ② 响应数据21 }22}(UserService 接口就是 public interface UserService { List<User> findAll(); },包在 com.itheima.service。)
检查点:① 控制层的方法体只剩”调 service + return”,读文件的代码一行都不剩;② user.txt 只被 dao 层提起(成立的话说明”数据访问”职责已经归位);③ 启动工程访问 /list,返回的 JSON 和拆分前一模一样。
2-2 这段话该写在哪个文件里 一个”员工管理”的三层架构工程,现在要加下面这几个功能。请为每条写出:改哪一个包(层)里的哪个文件(接口还是实现类也要写清),并说明理由:
List 改成从 emp.txt 读;GET /emps,把员工列表返回给浏览器;test_37_每段代码属于哪一层.java 里给了表格与写作区。)一级 · 思路:对每一条问一句”这件事是跟数据存取有关,还是跟业务规则有关,还是跟请求响应有关”
二级 · 方法:碰数据来源/读写方式 → dao 包(XxxDao / XxxDaoImpl);碰”数据怎么算、怎么加工” → service 包(XxxService / XxxServiceImpl);碰”暴露什么地址、收什么参数、返回什么” → controller 包(XxxController)
三级 · 骨架:com.itheima.____.Xxx____Impl;如果是”新增一个接口”,除了实现类,接口里也要加方法签名
| 需求 | 改哪一层 | 具体文件 | 理由 |
|---|---|---|---|
① 数据来源从内存 List 改成读 emp.txt | dao(数据访问层) | com.itheima.dao.impl.EmpDaoImpl(实现类里换读法,接口 EmpDao 的方法签名通常不用改) | “数据从哪来、怎么读写”是数据访问的职责 |
| ② 姓名统一去空格 | service(业务逻辑层) | com.itheima.service.impl.EmpServiceImpl | 这是”业务规则”(数据要怎么处理),不是存取问题 |
③ 新增接口 GET /emps | controller(控制层) | com.itheima.controller.EmpController 加一个方法 | “暴露什么地址、把结果响应出去”是控制层的职责(如果 service 还没有对应方法,EmpService/EmpServiceImpl 里也要补) |
| ④ 薪资的分转成元 | service(业务逻辑层) | com.itheima.service.impl.EmpServiceImpl | 单位换算是业务规则 |
| ⑤ 按入职时间倒序返回 | service(业务逻辑层) | com.itheima.service.impl.EmpServiceImpl | “结果怎么排”属于业务逻辑(如果排序要下推到数据库,也可以让 dao 直接按条件查) |
| 一句话检验:dao 只管”把数据搬进搬出”,service 管”数据怎么算”,controller 管”对外的接口长什么样”。 |
2-3 为什么要先写接口再写实现类
课程代码里每一层都是”一个接口 + 一个实现类”(UserService / UserServiceImpl,UserDao / UserDaoImpl)。请回答:
UserServiceImpl2),依赖接口的写法能帮你省掉什么麻烦?test_37_接口与实现类.java 里给了写作区。)一级 · 思路:盯住那一行成员变量:等号左边的类型和等号右边 new 出来的东西,分别是什么?
二级 · 方法:左边是接口类型(UserService),右边是实现类(new UserServiceImpl())——左边写的是”我需要一个能干这些活的东西”,右边写的是”具体用谁”
三级 · 骨架:private ____ userService = new ____();(左接口、右实现)
1private UserService userService = new UserServiceImpl();2// ↑ 接口类型(能干什么) ↑ 具体实现(谁来干)UserService;new 出来的才是实现类 UserServiceImpl。UserService),只需要改”new 的那一下”——上游的方法调用(userService.list())全部照旧,编译不会因为”类型不对”而全线报错。new UserServiceImpl())。下一篇要做的分层解耦就是把这个 new 从上层代码里拿掉,交给 IOC 容器去创建、用依赖注入送进来:那时这行只剩左边的接口类型 + 一个注解。3-1 照着课程把用户列表案例拆成三层 拿上一篇(36 篇)做出来的”单文件版”工程,动手拆成三层,每拆一步都跑一次工程确认接口没坏:
controller、service + service.impl、dao + dao.impl、pojo(实体类 User 如果还没建就先建);UserDao 里定义”取数据”的方法(返回 List<String>),实现类 UserDaoImpl 里把读 user.txt 的代码搬过来;UserService 里定义”查询所有用户”的方法(返回 List<User>),实现类 UserServiceImpl 里调 dao 拿文本、用 stream().map(...) 解析封装;new UserServiceImpl() 里拿 service;curl -si http://localhost:8080/list(看响应头和状态码)、浏览器打开 http://localhost:8080/user.html(看表格还是 8 行);user2.txt(复制一份数据、改几个名字),只改这一个文件,重启后看页面变化——体会”改数据来源只动 dao”。
(练习文件 test_37_综合_把案例拆成三层.java 里按这 6 步给了写作区。)涉及知识点
| 知识点 | 在这里的应用 |
|---|---|
| 单一职责原则 | 一个类只负责一件事:取数据 / 算业务 / 对接口 |
| 三层职责 | controller 接收请求响应数据、service 业务逻辑、dao 数据访问 |
| 包与命名 | controller / service(.impl) / dao(.impl) / pojo;XxxController / XxxService(+Impl) / XxxDao(+Impl) |
| 接口与实现 | 上层依赖接口类型,new 出来的才是实现类 |
| 调用链 | Controller → Service → Dao 单向 |
| 重构验证 | 接口路径与响应数据必须不变(200 + application/json) |
一级 · 思路:从下往上搬——先把”读文件”搬到最底层,再把”解析”搬到中间,最后让最上层只剩”调用 + 返回”;每搬一次就编译一次,别一口气全改完
二级 · 方法:UserDao.findAll() 返回 List<String>;UserService.findAll() 返回 List<User>;Controller 里 private UserService userService = new UserServiceImpl();;@RequestMapping("/list") 的方法体只剩 List<User> userList = userService.findAll(); return userList;
三级 · 骨架:dao interface UserDao { List<String> ____(); } / service interface UserService { List<User> ____(); } / controller @____ public class UserController { private UserService userService = new ____(); @____("/list") public List<User> list(){ return ____.findAll(); } }
3-1 六步的落地结果:
1com/itheima/2├── controller/UserController.java3├── service/UserService.java + service/impl/UserServiceImpl.java4├── dao/UserDao.java + dao/impl/UserDaoImpl.java5└── pojo/User.java1public interface UserDao { List<String> findAll(); }1public class UserDaoImpl implements UserDao {2 @Override3 public List<String> findAll() {4 InputStream in = this.getClass().getClassLoader().getResourceAsStream("user.txt");5 return IoUtil.readUtf8Lines(in, new ArrayList<>());6 }7}List<User> findAll();;实现类 private UserDao userDao = new UserDaoImpl(); + userDao.findAll() 拿到文本 + stream().map(...) 解析成 List<User> 返回。private UserService userService = new UserServiceImpl();,方法体只剩:
1@RequestMapping("/list")2public List<User> list() {3 List<User> userList = userService.findAll();4 return userList;5}1$ curl -si "http://localhost:8080/list" | head -42HTTP/1.1 2003Content-Type: application/json4Transfer-Encoding: chunkeduser.html 仍是 8 行数据——分层改的是内部结构,接口和页面一行没变。UserDaoImpl 里的文件名改成 user2.txt(先复制一份 user.txt 改个名,再改几条数据),重启后页面立刻跟着变——只动了 dao 层一个文件,这就是”换数据来源只改数据访问层”的体感。练习文件里记下你改的文件名和页面变化即可。如果你喜欢,那么欢迎来到我的世界!
了解更多暂未播放



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


