重点是对于单一职责原则,三层架构的理解。采用非常通俗的方式讲清楚具体概念。
分层解耦 ------ 单一职责、三层架构、IoC与DI
本文以项目中的 Emp 模块为例,从"一碗面条"讲到"三层分离",把分层解耦的核心理念讲清楚。
一、从一个生活场景讲起:兰州拉面馆 vs 五星级酒店
1.1 兰州拉面馆(没有分层)
你走进一家小面馆,喊一声"老板,来碗牛肉面"。
老板一个人干了所有事:招呼你坐下 → 跑进后厨和面拉面 → 煮面捞面 → 端到你面前 → 收钱找零。
老板就是服务员,就是厨师,就是收银员。你觉得面太咸了想换一碗,老板得停下所有活来单独处理你。
对应到代码------没有分层的系统:
java
// 一个方法干所有事:解析数据 + 处理业务 + 返回结果
@GetMapping("/emp/list")
public Result list() {
// 1. 自己解析 XML(数据访问的事)
List<Emp> empList = XmlParserUtils.parse();
// 2. 自己过滤(业务逻辑的事)
// 3. 自己排序(业务逻辑的事)
// 4. 返回(Controller 本职)
return Result.success(empList);
}
1.2 五星级酒店(标准三层)
你走进一家五星级酒店餐厅:
- 服务员只负责接单、上菜、微笑
- 厨师只负责按菜单做菜,从仓库拿食材
- 采购员只负责从市场买菜,放进仓库
你想把牛排从七分熟改五分熟,只需要告诉服务员,服务员通知厨师,采购员完全不受影响。明天采购员从"本地菜市场"换成"冷链配送",厨师照样做菜,你照样吃------你和服务员完全无感知。
对应到代码------标准三层架构:
服务员(Controller)→ 厨师(Service)→ 采购员(Mapper)
三个角色各司其职,任何一个角色的变化都不会牵连另外两个。
二、单一职责原则(SRP ------ Single Responsibility Principle)
2.1 核心定义
一个类应该只有一个引起它变化的原因。
或者说:一个类只干一件活,只对一个角色负责。
如果类 A 同时做了"接收 HTTP 请求"和"解析 XML 文件"两件事,那"改接口格式"和"换数据源"这两个完全不相干的理由都会导致你修改类 A。这就违反了 SRP。
2.2 正面对比:违反 SRP vs 遵守 SRP
违反 SRP 的写法(重构前):
java
@RestController
public class EmpController {
@GetMapping("/emp/list")
public Result list() {
List<Emp> empList = XmlParserUtils.parse(); // 干了数据访问的活!
return Result.success(empList); // 干了自己本职的活
}
}
Controller 同时干了两件事:
- 接收 HTTP 请求、返回响应(Controller 本职)
- 调用数据获取逻辑(应该是 Service 的活,Service 调 Mapper)
遵守 SRP 的写法(重构后):
java
@RestController
public class EmpController {
private final EmpService empService;
public EmpController(EmpService empService) {
this.empService = empService;
}
@GetMapping("/emp/list")
public Result list() {
try {
return Result.success(empService.list()); // 只调 Service
} catch (Exception e) {
return Result.error("获取员工列表失败: " + e.getMessage());
}
}
}
Controller 现在只知道 EmpService 这个接口。数据从 XML 来还是从数据库来?它一概不关心。这就是 SRP。
2.3 重构后每个文件的职责清单
EmpMapper.java ------ 职责:解析 XML 文件,返回 List<Emp>
java
@Component
public class EmpMapper {
public List<Emp> list() throws Exception {
List<Emp> empList = new ArrayList<>();
InputStream inputStream = EmpMapper.class
.getClassLoader().getResourceAsStream("emp.xml");
DocumentBuilder builder = DocumentBuilderFactory
.newInstance().newDocumentBuilder();
Document document = builder.parse(inputStream);
NodeList nodeList = document.getElementsByTagName("emp");
for (int i = 0; i < nodeList.getLength(); i++) {
Element element = (Element) nodeList.item(i);
Emp emp = new Emp();
emp.setName(getText(element, "name"));
emp.setAge(Integer.parseInt(getText(element, "age")));
emp.setImage(getText(element, "image"));
emp.setGender(Integer.parseInt(getText(element, "gender")));
emp.setJob(Integer.parseInt(getText(element, "job")));
empList.add(emp);
}
return empList;
}
private String getText(Element element, String tagName) {
return element.getElementsByTagName(tagName).item(0).getTextContent();
}
}
| 维度 | 说明 |
|---|---|
| 它关心什么 | XML 文件在哪、怎么解析、节点名是什么 |
| 它不关心什么 | HTTP 请求、业务规则、返回给前端什么格式 |
| 什么时候改它 | XML 路径变了、XML 标签名变了、数据格式变了 |
| 绝不该改它的需求 | "只显示男员工"、"加分页"、"接口改 POST" |
与重构前相比,最大的变化是:异常不再被 吞掉 ,而是往上抛。重构前的 catch (Exception e) { e.printStackTrace(); } 让上层完全不知道出了问题,返回了一个空列表糊弄过去。现在异常层层上抛,由 Controller 统一处理。
EmpService.java(接口)------ 职责:定义契约
java
public interface EmpService {
List<Emp> list() throws Exception;
}
接口本身不干活,它只是一份合同 。合同上写着:"不管谁来当这个 Service,都得提供 list() 方法,返回员工列表。"
EmpServiceImpl.java ------ 职责:执行业务逻辑
java
@Service
public class EmpServiceImpl implements EmpService {
private final EmpMapper empMapper;
public EmpServiceImpl(EmpMapper empMapper) {
this.empMapper = empMapper;
}
@Override
public List<Emp> list() throws Exception {
return empMapper.list(); // 业务逻辑的预留位置
}
}
| 维度 | 说明 |
|---|---|
| 它关心什么 | 如何组织业务流程:调哪个 Mapper、要不要过滤、要不要排序 |
| 它不关心什么 | 数据存在 XML 还是 MySQL、HTTP 请求怎么处理 |
| 什么时候改它 | 业务规则变了 |
示例:如果需求变成"只显示年龄小于 60 的员工",只改这一个文件:
java
@Override
public List<Emp> list() throws Exception {
return empMapper.list().stream()
.filter(e -> e.getAge() < 60) // 只加这一行
.toList();
}
Controller 和 Mapper 毫无感知。改一个需求只动一处代码------这就是 SRP 的威力。
EmpController.java ------ 职责:接收请求、返回响应
java
@RestController
public class EmpController {
private final EmpService empService;
public EmpController(EmpService empService) {
this.empService = empService;
}
@GetMapping("/emp/list")
public Result list() {
try {
return Result.success(empService.list());
} catch (Exception e) {
return Result.error("获取员工列表失败: " + e.getMessage());
}
}
}
| 维度 | 说明 |
|---|---|
| 它关心什么 | 请求路径 /emp/list、请求方式 GET、返回格式 Result |
| 它不关心什么 | 数据怎么来的、业务规则是什么 |
| 什么时候改它 | 接口层面的东西变了:路径变了、请求方法变了、返回格式变了 |
2.4 SRP 终极检验表
如果一个需求只能落到一个文件需要修改,SRP 就通过了:
需求变更 改哪个文件?
───────────────────────────────── ──────────────────
XML 路径从 emp.xml 改成 staff.xml EmpMapper.java
只显示年龄 < 60 的员工 EmpServiceImpl.java
接口路径从 /emp/list 改成 /staff EmpController.java
增加"学历"字段 Emp.java + emp.xml + EmpMapper.java
返回格式加 "timestamp" 字段 Result.java 或 EmpController.java
数据源从 XML 换成 MySQL EmpMapper.java(换实现)+ 可能改 Service
三、三层分层架构
3.1 什么是三层
三层是按抽象层次来划分的,不是按文件数量:
第 1 层:表现层(Controller)------ 跟用户打交道
↓ 只能往下调
第 2 层:业务逻辑层(Service)------ 处理规则和流程
↓ 只能往下调
第 3 层:数据访问层(Mapper)------ 跟数据源打交道
核心规则只有两条:
- 上层可以调下层,下层绝不能调上层(单向依赖)
- 同层之间尽量不互相调(水平隔离)
3.2 每一层看到的"世界"不同
这就是分层的精髓------每一层只看到自己需要的抽象,屏蔽无关细节。
Controller 看到的
EmpService 接口(只知道这个)
Result(统一的返回格式)
HTTP 请求和响应
看不到的东西:
✗ EmpMapper(根本不知道它的存在)
✗ emp.xml(更不知道数据存在哪)
✗ XML 解析的细节
✗ 数据库(如果以后换了,Controller 不 care)
Controller 的代码里没有出现过 EmpMapper、emp.xml、DocumentBuilder 这些字眼。它完全不知道底层是 XML 文件还是 MySQL。这就是分层带来的隔离。
Service 看到的
EmpMapper(一个能给我数据的东西)
EmpService 接口(我要遵守的合同)
业务规则
看不到的东西:
✗ HTTP 请求 / @GetMapping(什么鬼,不关我事)
✗ Result(返回什么格式,那是 Controller 的事)
✗ emp.xml(我在哪取数据?我不关心,Mapper 帮我搞定)
Service 的代码里没有出现过 HTTP 相关的东西。谁来调它都行------可以是 Controller,可以是定时任务,可以是命令行。它是可复用的。
Mapper 看到的
emp.xml 文件
Emp 实体类
DOM 解析 API
看不到的东西:
✗ HTTP 请求(完全不知道有这回事)
✗ Result 对象(那是什么?)
✗ 业务规则(过滤、排序------不关我事)
✗ Controller 和 Service(谁在调我?我不知道也不关心)
Mapper 是一个纯粹的数据搬运工:把 XML 变成 Java 对象,仅此而已。
3.3 依赖方向图
┌─────────────────────────────────────────────────┐
│ │
│ EmpController ──依赖──→ EmpService(接口) │
│ ↑ ↑ │
│ │ 接口在上 │
│ │ 实现类在下 │
│ │ │ │
│ │ EmpServiceImpl ──依赖──→ EmpMapper
│ │ │
│ │ 不依赖任何人
│ │ 只依赖 emp.xml
│ │ │
│ 依赖方向永远从上往下 │
│ 永远不反向依赖 │
│ │
└─────────────────────────────────────────────────┘
如果把依赖图画反------比如 Mapper 返回 Result 对象(那是 Controller 层的东西),或者 Controller 直接读 XML(跨过两层)------那就是依赖倒置 或跨层调用,分层就被破坏了。
3.4 为什么要有接口(EmpService)
接口 = 合同。Controller 只跟"合同"打交道,不跟具体的人打交道。
EmpController 依赖的是 EmpService(接口)
↑
│ 实现
│
EmpServiceImpl(具体类)
好处:将来把数据源从 XML 换成数据库,只需要写一个新的实现类,Controller 一行不用改:
java
// 新的数据库实现
@Service
public class EmpServiceImplDB implements EmpService {
private final EmpDao empDao; // 操作数据库
@Override
public List<Emp> list() {
return empDao.findAll();
}
}
// Controller 完全不变!因为它只依赖 EmpService 接口
这就是面向接口编程的好处:具体实现可以随意替换,调用方毫无感知。
四、控制反转(IoC)与依赖注入(DI)
4.1 从一个故事开始:你 vs 外卖
没有 IoC/DI 的世界(你自己掌控一切):
你想喝咖啡,得自己去超市买咖啡豆、买磨豆机、买咖啡机,然后回来自己磨、自己煮。每一次想喝咖啡都是一个浩大的工程。你想换一种豆子?再跑一趟超市。
对应到代码:
java
// 你自己(Controller)手动 new 所有依赖
public class EmpController {
private EmpService empService;
public EmpController() {
EmpMapper mapper = new EmpMapper(); // 先自己创建 Mapper
this.empService = new EmpServiceImpl(mapper); // 再自己创建 Service
}
}
有 IoC/DI 的世界(你坐下来,咖啡自动端来):
你走进咖啡店,坐下。咖啡就端来了。咖啡怎么做的、用什么豆子、谁磨的------你一概不用管。
java
// 你(Controller)声明你需要什么,Spring 帮你送过来
@RestController
public class EmpController {
private final EmpService empService;
public EmpController(EmpService empService) { // Spring 自动把咖啡端给你
this.empService = empService;
}
}
4.2 什么是控制反转(IoC ------ Inversion of Control)
反转的是"创建对象的控制权"。
传统方式 ------ 控制权在你手里:
你的代码 ──主动创建──→ new EmpMapper()
你的代码 ──主动创建──→ new EmpServiceImpl()
你的代码 ──主动创建──→ new EmpController()
你的代码决定什么时候创建、按什么顺序创建
IoC 方式 ------ 控制权在 Spring 容器手里:
Spring 启动 ──扫描注解──→ 创建所有 Bean
Spring ──按依赖顺序──→ 先创建 EmpMapper(它不依赖别人)
Spring ──按依赖顺序──→ 再创建 EmpServiceImpl(注入 EmpMapper)
Spring ──按依赖顺序──→ 最后创建 EmpController(注入 EmpService)
你的代码只管用,创建的事完全不用操心
用一句话说:以前你主动去菜市场买菜,现在你在手机上下单,菜自动送到家门口。控制的主动权从你手里反转到了系统手里。
4.3 什么是依赖注入(DI ------ Dependency Injection)
DI 是 IoC 的具体实现方式。IoC 是思想,DI 是手段。
"注入"这个词很形象------就像打针一样,把依赖"打"进需要它的对象里。
EmpController 需要 EmpService → EmpService 是 Controller 的"依赖"
EmpServiceImpl 需要 EmpMapper → EmpMapper 是 Service 的"依赖"
4.4 Spring 是如何实现注入的------逐步拆解
第 1 步:注解标记 ------ "把我管起来"
java
@Component // ← "Spring,管我!"
public class EmpMapper { ... }
@Service // ← "Spring,管我!"
public class EmpServiceImpl implements EmpService { ... }
@RestController // ← "Spring,管我!"(@RestController 内部包含了 @Component)
public class EmpController { ... }
Spring 启动时会扫描 所有类,找到带这些注解的,自动创建对象并放进容器。这些被 Spring 管理的对象叫 Bean(豆子)。
你可以把 Spring 容器想象成一个豆子罐:
Spring 容器(豆子罐):
┌──────────────────────────────────┐
│ empMapper ← 一个 Bean │
│ empServiceImpl ← 一个 Bean │
│ empController ← 一个 Bean │
│ helloController ← 一个 Bean │
│ ... │
└──────────────────────────────────┘
第 2 步:构造器注入 ------ Spring 自动"连线"
java
@Service
public class EmpServiceImpl implements EmpService {
private final EmpMapper empMapper;
public EmpServiceImpl(EmpMapper empMapper) { // 构造器告诉 Spring:"我需要一个 EmpMapper"
this.empMapper = empMapper;
}
}
Spring 创建 EmpServiceImpl 时看到构造器要 EmpMapper,内部逻辑是:
Spring 的内心独白:
"我要创建 EmpServiceImpl 了..."
"看它的构造器,需要一个 EmpMapper 类型的参数"
"我兜里有没有 EmpMapper?------有!之前已经创建好了"
"拿去,注入!"
"好了,EmpServiceImpl 创建完成,它的 empMapper 字段已经有值了"
第 3 步:Controller 也是同理------而且它用的是接口
java
@RestController
public class EmpController {
private final EmpService empService; // 字段类型是接口!
public EmpController(EmpService empService) { // 参数类型是接口!
this.empService = empService;
}
}
Spring 的内心独白:
"我要创建 EmpController..."
"看它的构造器,需要一个 EmpService 类型(接口)的参数"
"我兜里谁实现了 EmpService?------EmpServiceImpl!"
"就是它,注入!"
注意:EmpController 的代码里从来没出现过 EmpServiceImpl 这个类名。它只知道接口 EmpService。这就是面向接口编程。
第 4 步:整个注入链
Spring 启动时的创建顺序(从无依赖的开始):
第 1 步:new EmpMapper()
↑ 它不依赖任何人,最先创建
第 2 步:new EmpServiceImpl(empMapper)
↑ 它依赖 EmpMapper,从容器里取出第 1 步创建的注入
第 3 步:new EmpController(empService)
↑ 它依赖 EmpService,从容器里取出第 2 步创建的注入
第 4 步:所有 Bean 就绪,等待请求到来
依赖方向和创建顺序正好相反:
依赖方向:EmpController → EmpService → EmpMapper
创建顺序:EmpMapper → EmpService → EmpController
就像盖楼:先打地基(Mapper),再建主体(Service),最后装修(Controller)。
4.5 三种注入方式对比
| 方式 | 代码示例 | 优缺点 |
|---|---|---|
| 字段注入 | @Autowired private EmpService s; |
代码最短,但依赖被隐藏了,无法用 final,测试时需要反射注入,不推荐 |
| Setter 注入 | public void setService(EmpService s){} |
依赖可以被中途修改,可能被忘记调用,不推荐 |
| 构造器注入(推荐) | public Xxx(EmpService s){ this.s = s; } |
依赖声明在构造器参数里,一目了然;可以用 final 保证不可变;创建时一次性注入;测试时手动传入即可,强烈推荐 |
4.6 用外卖比喻串起所有概念
| 外卖场景 | 对应代码概念 | 说明 |
|---|---|---|
| 你是顾客 | 浏览器/请求 | 你只管下单和吃 |
| 外卖平台 App | Controller | 接你的单,传给后厨,把做好的菜端给你 |
| 餐厅后厨 | Service | 接到订单,按配方做菜,找仓库拿食材 |
| 食材仓库 | Mapper | 只管提供原料:青菜、肉、米 |
| 店老板统筹安排所有人 | IoC(控制反转) | 不是服务员自己跑去买菜、自己炒菜。店老板已经把所有人安排在各自的岗位上,物归其位,人归其位 |
| 老板给厨师配好供货渠道 | DI(依赖注入) | 厨师不用自己联系菜农,老板已经给他配好了固定的供货商(Mapper),直接就能拿货炒菜 |
| 签约入职 | @Component / @Service | 你入职签合同,成为店里的正式员工,店里给你配工位、配工具 |
| 入职第一天就明确供应商 | 构造器注入 | 入职合同写死了:"咖啡豆只从 A 供应商进货",明确、不可改 |
| 菜单上的菜名 | 接口(EmpService) | 你点"拿铁",不管今天是哪个咖啡师给你做,不管用的什么牌子牛奶。你只管点"拿铁" |
五、一条请求的完整生命周期 ------ 从浏览器到 XML,一步一步走
这是全文档最重要的部分。我们把一条 GET /emp/list 请求从头到尾、从文件到文件,完完整整走一遍。
浏览器 → Spring 分发器 → EmpController → EmpService → EmpServiceImpl → EmpMapper → emp.xml
───────────────────────────────────────────────────────────────────────────────────────────
第 1 站:浏览器
涉及文件: src/main/resources/static/emp.html + src/main/resources/static/js/emp.js
javascript
// emp.js 第 11 行:发起 Ajax 请求
axios.get('/emp/list')
.then(function (response) {
if (response.data.code === 1) {
self.tableData = response.data.data; // 拿到数据,渲染表格
}
})
发生了什么:
-
用户在浏览器打开
emp.html,页面加载完成 -
Vue 实例挂载(mounted),自动调用
fetchData() -
axios.get('/emp/list')向服务器发起一个 HTTP GET 请求 -
请求目标:
http://localhost:8080/emp/list -
浏览器此时处于等待状态,转圈等待服务器响应
┌──────────────────────┐
│ 浏览器(emp.html) │
│ 发起 GET /emp/list │
│ 正在加载中... │
└──────────┬───────────┘
│ HTTP 请求飞向服务器
↓
第 2 站:Spring Boot 的请求分发器(DispatcherServlet)
无具体文件------这是 Spring Boot 框架内置的组件,你看不到它的代码,但它一直在默默工作。
发生了什么:
-
请求到达
localhost:8080 -
Spring Boot 的
DispatcherServlet拦截到这个请求 -
它查看自己维护的"路径映射表"
-
在映射表里找到:
GET /emp/list → EmpController.list() -
决定把请求分发给
EmpControllerSpring 内部的路径映射表(启动时自动建立):
┌──────────────────────────────────┐
│ GET /hello → HelloController.hello() │
│ GET /register → RegController.registerPage() │
│ POST /register → RegController.register() │
│ GET /emp/list → EmpController.list() ← 命中!│
└──────────────────────────────────┘DispatcherServlet 内心独白:
"收到一个 GET 请求,路径是 /emp/list"
"查表... 找到了,交给 EmpController 的 list() 方法处理"
第 3 站:EmpController.list()
涉及文件: src/main/java/com/example/demo/controller/EmpController.java
java
@RestController
public class EmpController {
private final EmpService empService; // Spring 已经注入好了
public EmpController(EmpService empService) {
this.empService = empService;
}
@GetMapping("/emp/list")
public Result list() {
try {
return Result.success(empService.list()); // ← 关键调用
} catch (Exception e) {
return Result.error("获取员工列表失败: " + e.getMessage());
}
}
}
发生了什么:
-
Spring 调用
EmpController.list()方法 -
empService字段已经被 Spring 注入为EmpServiceImpl实例 -
Controller 调用
empService.list() -
Controller 不知道也不关心这个
list()到底怎么实现的------它只管调,然后包成Result返回EmpController 的内心独白:
"有人要 /emp/list?好的,我叫 Service 去办"
"empService.list() 返回了什么?哦,一个 List"
"用 Result.success() 包一下,返回给前端"
"如果出异常了?包成 Result.error() 返回,不能让用户看到 500"
第 4 站:EmpServiceImpl.list()
涉及文件: src/main/java/com/example/demo/service/impl/EmpServiceImpl.java
java
@Service
public class EmpServiceImpl implements EmpService {
private final EmpMapper empMapper; // Spring 已经注入好了
public EmpServiceImpl(EmpMapper empMapper) {
this.empMapper = empMapper;
}
@Override
public List<Emp> list() throws Exception {
return empMapper.list(); // ← 继续往下调
}
}
发生了什么:
-
empMapper字段已经被 Spring 注入为EmpMapper实例 -
调用
empMapper.list()获取原始数据 -
当前没有额外业务逻辑(这是将来加过滤、排序、缓存的地方)
-
直接把 Mapper 返回的数据原样返回给上层
EmpServiceImpl 的内心独白:
"Controller 问我要员工列表"
"我不关心数据存在哪,我找 Mapper 要"
"empMapper.list() 返回了什么?List,好的,返回给 Controller"
"(以后如果需要过滤、排序,就在这一层加)"
第 5 站:EmpMapper.list()
涉及文件: src/main/java/com/example/demo/mapper/EmpMapper.java
java
@Component
public class EmpMapper {
public List<Emp> list() throws Exception {
List<Emp> empList = new ArrayList<>();
InputStream inputStream = EmpMapper.class
.getClassLoader()
.getResourceAsStream("emp.xml"); // ← 打开 emp.xml 文件
DocumentBuilder builder = DocumentBuilderFactory
.newInstance().newDocumentBuilder();
Document document = builder.parse(inputStream); // ← 解析 XML
NodeList nodeList = document.getElementsByTagName("emp");
for (int i = 0; i < nodeList.getLength(); i++) {
Element element = (Element) nodeList.item(i);
Emp emp = new Emp();
emp.setName(getText(element, "name")); // 读取每个字段
emp.setAge(Integer.parseInt(getText(element, "age")));
emp.setImage(getText(element, "image"));
emp.setGender(Integer.parseInt(getText(element, "gender")));
emp.setJob(Integer.parseInt(getText(element, "job")));
empList.add(emp); // 添加到列表
}
return empList; // 返回完整的 List<Emp>
}
private String getText(Element element, String tagName) {
return element.getElementsByTagName(tagName)
.item(0).getTextContent();
}
}
发生了什么:
-
通过类加载器找到
emp.xml文件 -
用 Java 的 DOM 解析器读取 XML
-
遍历每一个
<emp>节点 -
对每个节点:读
name、age、image、gender、job,创建Emp对象 -
所有 Emp 对象放进 List 里返回
EmpMapper 的内心独白:
"Service 要数据?好的,我去 emp.xml 里取"
"打开 emp.xml..."
"找到 4 个标签,逐个解析..."
"构建好 4 个 Emp 对象,放进 List,返回"
第 6 站:emp.xml(数据源)
涉及文件: src/main/resources/emp.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<emps>
<emp>
<name>金毛狮王</name>
<age>55</age>
<image>https://web-framework.oss-cn-hangzhou.aliyuncs.com/web/1.jpg</image>
<gender>1</gender>
<job>1</job>
</emp>
<emp>
<name>白眉鹰王</name>
<age>65</age>
...
</emp>
<emp>
<name>青翼蝠王</name>
...
</emp>
<emp>
<name>紫杉龙王</name>
...
</emp>
</emps>
发生了什么:
- 被 EmpMapper 读取
- 4 个
<emp>节点 → 4 个EmpJava 对象 - 这就是整个请求链路的最底层------数据的源头
第 7 站:原路返回
数据拿到了,现在沿着原路一层一层往回传:
emp.xml
↓ 4 个 Emp 对象
EmpMapper.list() → 返回 List<Emp>
↓
EmpServiceImpl.list() → 原样返回(或加上业务处理)
↓
EmpController.list() → Result.success(empList) 包装
↓
Spring Boot → 把 Result 对象序列化为 JSON
↓
浏览器(emp.js) → response.data.code === 1 → 取 response.data.data
↓ → 赋值给 tableData
Vue + Element UI → 表格渲染 4 行数据
Spring Boot 把 Result 对象变成 JSON 字符串返回给浏览器:
json
{
"code": 1,
"msg": "success",
"data": [
{"name": "金毛狮王", "age": 55, "image": "https://.../1.jpg", "gender": 1, "job": 1},
{"name": "白眉鹰王", "age": 65, "image": "https://.../2.jpg", "gender": 1, "job": 1},
{"name": "青翼蝠王", "age": 45, "image": "https://.../3.jpg", "gender": 1, "job": 2},
{"name": "紫杉龙王", "age": 38, "image": "https://.../4.jpg", "gender": 2, "job": 3}
]
}
浏览器端 emp.js 收到 JSON 后的处理:
javascript
axios.get('/emp/list')
.then(function (response) {
if (response.data.code === 1) {
self.tableData = response.data.data; // 4 条数据赋值给表格
}
// code 不是 1 说明出错了,表格为空
})
.catch(function (error) {
self.$message.error('获取数据失败'); // 网络错误或其他异常
})
.finally(function () {
self.loading = false; // 不管成功失败,关掉加载动画
});
然后再由 emp.html 中的 Element UI 表格组件把数据渲染成用户看到的表格:
html
<el-table :data="tableData" stripe border>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="age" label="年龄"></el-table-column>
<el-table-column label="图像">
<img :src="scope.row.image"> <!-- 展示头像图片 -->
</el-table-column>
<el-table-column label="性别" :formatter="formatGender"> <!-- 1→男, 2→女 -->
</el-table-column>
<el-table-column label="职位" :formatter="formatJob"> <!-- 1→讲师, 2→班主任, 3→就业指导 -->
</el-table-column>
</el-table>
完整请求时间线(一张图看完)
时间 ────────────────────────────────────────────────────────────→
① 浏览器 ② Spring分发器 ③ Controller ④ Service ⑤ Mapper ⑥ emp.xml
│ │ │ │ │ │
│─GET /emp/list──→│ │ │ │ │
│ │─路由到list()──→ │ │ │ │
│ │ │─empService │ │ │
│ │ │ .list()──────→ │ │ │
│ │ │ │─empMapper │ │
│ │ │ │ .list()──────→ │ │
│ │ │ │ │─解析XML────────→│
│ │ │ │ │←──4个Emp对象───│
│ │ │ │←──List<Emp>──── │ │
│ │ │←──List<Emp>──── │ │ │
│ │←──Result JSON── │ Result.success(list) │
│←──JSON响应───── │ │ │ │ │
│ │ │ │ │ │
│ 渲染表格 │ │ │ │ │
│ 用户看到4行员工 │ │ │ │ │
六、总结
6.1 三个核心概念的对应关系
| 概念 | 一句话 |
|---|---|
| 单一职责(SRP) | 一个类只干一件活,只因为一个理由被修改 |
| 三层架构 | Controller 接待、Service 加工、Mapper 取货,只能从上往下调 |
| IoC(控制反转) | 创建对象的控制权从你的代码转移到 Spring 容器 |
| DI(依赖注入) | Spring 自动把你需要的对象通过构造器塞进你的类里 |
| Bean | 被 Spring 管理的对象,存在容器里的"豆子" |
| @Component / @Service / @RestController | 告诉 Spring:"把我管起来,我是一个 Bean" |
| 构造器注入 | 在构造方法参数里声明依赖,Spring 帮你传进来 |
| 接口(EmpService) | 合同:定义能干什么,不定义怎么干。Controller 只看合同,不看实现 |
6.2 重构前后对比
| 对比项 | 重构前 | 重构后 |
|---|---|---|
| Controller 依赖 | XmlParserUtils.parse()(静态调用) |
EmpService(构造器注入) |
| 异常处理 | 吞在 Utils 里 e.printStackTrace() |
向上抛,Controller 统一兜底 |
| 数据访问层 | utils/XmlParserUtils(名字模糊) |
mapper/EmpMapper(职责明确) |
| 业务逻辑层 | 无(Controller 直接调数据层) | service/EmpService + EmpServiceImpl |
| 扩展点 | 无------加过滤得改 Controller | Service 层------加过滤只改 ServiceImpl |
6.3 最终项目结构
src/main/java/com/example/demo/
├── controller/
│ └── EmpController.java ← 表现层:接请求、返响应
├── service/
│ ├── EmpService.java ← 业务接口(合同)
│ └── impl/
│ └── EmpServiceImpl.java ← 业务逻辑:调 Mapper、加规则
├── mapper/
│ └── EmpMapper.java ← 数据访问:解析 XML
├── pojo/
│ ├── Emp.java ← 员工实体
│ └── Result.java ← 统一响应格式
src/main/resources/
├── emp.xml ← 数据源
├── static/
│ ├── emp.html ← 前端页面
│ └── js/
│ └── emp.js ← 前端逻辑
这个结构就是 Spring Boot 项目中最经典、最常用的分层方式。任何一个稍微正式一点的项目都会看到类似的目录结构。
