web-第7次课后作业-1

重点是对于单一职责原则,三层架构的理解。采用非常通俗的方式讲清楚具体概念。

分层解耦 ------ 单一职责、三层架构、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 同时干了两件事:

  1. 接收 HTTP 请求、返回响应(Controller 本职)
  2. 调用数据获取逻辑(应该是 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)------ 跟数据源打交道

核心规则只有两条:

  1. 上层可以调下层,下层绝不能调上层(单向依赖)
  2. 同层之间尽量不互相调(水平隔离)

3.2 每一层看到的"世界"不同

这就是分层的精髓------每一层只看到自己需要的抽象,屏蔽无关细节。

Controller 看到的
复制代码
EmpService 接口(只知道这个)
Result(统一的返回格式)
HTTP 请求和响应

看不到的东西:
✗ EmpMapper(根本不知道它的存在)
✗ emp.xml(更不知道数据存在哪)
✗ XML 解析的细节
✗ 数据库(如果以后换了,Controller 不 care)

Controller 的代码里没有出现过 EmpMapperemp.xmlDocumentBuilder 这些字眼。它完全不知道底层是 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;  // 拿到数据,渲染表格
        }
    })

发生了什么:

  1. 用户在浏览器打开 emp.html,页面加载完成

  2. Vue 实例挂载(mounted),自动调用 fetchData()

  3. axios.get('/emp/list') 向服务器发起一个 HTTP GET 请求

  4. 请求目标:http://localhost:8080/emp/list

  5. 浏览器此时处于等待状态,转圈等待服务器响应

    ┌──────────────────────┐
    │ 浏览器(emp.html) │
    │ 发起 GET /emp/list │
    │ 正在加载中... │
    └──────────┬───────────┘
    │ HTTP 请求飞向服务器

第 2 站:Spring Boot 的请求分发器(DispatcherServlet)

无具体文件------这是 Spring Boot 框架内置的组件,你看不到它的代码,但它一直在默默工作。

发生了什么:

  1. 请求到达 localhost:8080

  2. Spring Boot 的 DispatcherServlet 拦截到这个请求

  3. 它查看自己维护的"路径映射表"

  4. 在映射表里找到:GET /emp/list → EmpController.list()

  5. 决定把请求分发给 EmpController

    Spring 内部的路径映射表(启动时自动建立):
    ┌──────────────────────────────────┐
    │ 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());
        }
    }
}

发生了什么:

  1. Spring 调用 EmpController.list() 方法

  2. empService 字段已经被 Spring 注入为 EmpServiceImpl 实例

  3. Controller 调用 empService.list()

  4. 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();                  // ← 继续往下调
    }
}

发生了什么:

  1. empMapper 字段已经被 Spring 注入为 EmpMapper 实例

  2. 调用 empMapper.list() 获取原始数据

  3. 当前没有额外业务逻辑(这是将来加过滤、排序、缓存的地方)

  4. 直接把 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();
    }
}

发生了什么:

  1. 通过类加载器找到 emp.xml 文件

  2. 用 Java 的 DOM 解析器读取 XML

  3. 遍历每一个 <emp> 节点

  4. 对每个节点:读 nameageimagegenderjob,创建 Emp 对象

  5. 所有 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>

发生了什么:

  1. 被 EmpMapper 读取
  2. 4 个 <emp> 节点 → 4 个 Emp Java 对象
  3. 这就是整个请求链路的最底层------数据的源头

第 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 项目中最经典、最常用的分层方式。任何一个稍微正式一点的项目都会看到类似的目录结构。