MVC 设计模式
本章是 Spring MVC 教程的思想基础。在讲解 DispatcherServlet 如何调度请求、Controller 如何映射 URL 之前,必须先理解 MVC(Model-View-Controller) 这一经典设计模式。Spring MVC 的命名本身就表明它是对 MVC 模式的 Web 化实现——不理解 MVC 的三层分离思想,就无法理解为什么 Spring MVC 要设计成"DispatcherServlet + Controller + ViewResolver"这样的结构。
定义与作用
MVC 是一种架构模式,它将应用程序划分为三个核心组件,各自承担明确的职责,通过定义良好的接口协作完成用户请求的处理。
**Model(模型)**负责封装业务数据和业务规则。它是应用程序的"真相来源"——用户看到的信息来自 Model,用户提交的数据最终也存入 Model。Model 不关心数据如何展示,也不关心用户通过什么界面操作,它只保证数据的完整性和一致性。
**View(视图)**负责将 Model 中的数据以特定形式呈现给用户。它只读取 Model,不修改 Model。同一个 Model 可以被不同的 View 渲染:HTML 页面供浏览器查看、JSON 字符串供移动端接口调用、PDF 报表供财务部门打印。
**Controller(控制器)**负责接收用户输入,调用 Model 处理业务,并选择合适的 View 呈现结果。它是 Model 和 View 之间的"协调者"——用户操作先到达 Controller,Controller 决定接下来该做什么、该展示什么。
MVC 模式之所以被提出,是因为早期的 GUI 程序(如 1970 年代 Smalltalk 语言的应用)中,界面代码、业务逻辑和数据存储全部混在一起。修改界面可能破坏业务规则,更换数据库可能需要重写界面。MVC 用关注点分离(Separation of Concerns)解决了这一混乱。
生活类比:餐厅后厨分工
想象一家正规餐厅的后厨:
- Model 是食材与菜谱:冷库里存放着牛肉、蔬菜、调料(数据),菜谱上写着"牛排七分熟需要煎 4 分钟"(业务规则)。食材和菜谱不关心客人坐在大厅还是包间,也不关心服务员穿什么制服。
- View 是摆盘与上菜:同样的牛排,商务套餐配黑椒汁和西兰花(Web 页面),儿童套餐配薯条和番茄酱(移动端 H5)。摆盘师只从厨房取成品,不会自己去煎牛排。
- Controller 是传菜员:客人点单后,传菜员把订单送到厨房(调用 Model),煎好后端给摆盘师装饰(选择 View),最后送到客人桌上(返回响应)。传菜员不煎牛排,也不决定盘子花纹,他只负责"把对的东西送到对的地方"。
这个类比的关键在于:三层各司其职,通过标准接口协作,任何一层都可以独立替换而不影响其他层。
核心原理
MVC 三层职责对比
| 组件 | 职责 | 在 Web 开发中的典型实现 | 不能做的事 |
|---|---|---|---|
| Model | 封装数据与业务规则 | Entity 类、Service 层、Repository 层 | 直接操作 HTTP 请求/响应 |
| View | 数据可视化呈现 | JSP、Thymeleaf、FreeMarker、JSON 序列化 | 直接调用数据库 |
| Controller | 接收输入、调用 Model、选择 View | Servlet、Spring MVC 的 Controller | 包含复杂业务逻辑 |
MVC 在 Web 开发中的演变
MVC 模式诞生于桌面应用时代,进入 Web 领域后经历了三次重要演变:
演变解读:
JSP + JavaBean 时代(1998 年左右):JSP 页面中直接嵌入
<% %>脚本,既查数据库又拼 HTML。Model 和 View 完全混在一起,维护困难。Servlet + JSP + JavaBean 时代(2000 年左右):Sun 公司提出 Model 2 架构,用 Servlet 充当 Controller,JSP 只负责展示,JavaBean 负责业务。这是 MVC 在 Java Web 中的首次正规落地,但每个 URL 仍需一个独立 Servlet,配置繁琐。
Spring MVC 时代(2004 年至今):DispatcherServlet 作为唯一入口,用注解声明请求映射,自动完成参数绑定和视图解析。Controller 只需关注"收到什么请求、调用什么业务、返回什么视图",所有 Web 层基础设施由框架统一提供。
Spring MVC 与 MVC 模式的对应关系
对应关系解读:
- Controller 层:Spring MVC 做了更细的分工。DispatcherServlet 是"总调度员",Controller 方法是"具体办事员"。两者共同承担 MVC 中 Controller 的职责——接收输入、协调后续流程。
- Model 层:Spring MVC 本身不定义 Model 的具体形态。开发者用 Service 处理业务、用 Repository 访问数据,这些由 Spring 容器管理,Spring MVC 只负责在 Controller 中注入它们。
- View 层:Spring MVC 引入 ViewResolver 作为"视图查找器"。Controller 返回逻辑视图名
"employeeList",ViewResolver 根据配置将其解析为/WEB-INF/views/employeeList.jsp或templates/employeeList.html。这种解耦让更换视图技术无需修改 Controller。
完整示例
场景
飞翔科技要开发一个产品信息展示系统。CTO 大翔要求团队严格遵循 MVC 分层原则,禁止任何一层越权操作。架构师白歌负责设计分层架构,小崔负责 Controller 和 Service,黄俪负责前端页面,李眉负责部署和配置。
需求
用户访问 /products/1001 时,系统展示编号为 1001 的产品详情,包括名称、价格和库存。
分层实现
Model 层(小崔)
// Product.java - 数据模型
package com.feixiang.model;
public class Product {
private Long id;
private String name;
private Double price;
private Integer stock;
// 构造器、getter、setter 省略
}
// ProductService.java - 业务逻辑
package com.feixiang.service;
import com.feixiang.model.Product;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
public Product findById(Long id) {
// 模拟从数据库查询
Product product = new Product();
product.setId(id);
product.setName("Spring MVC 实战指南");
product.setPrice(89.00);
product.setStock(200);
return product;
}
}
白歌 review 意见:Service 层只操作 Product 对象,不涉及 HttpServletRequest 或 ResponseEntity。符合 Model 层职责。
View 层(黄俪)
<!-- productDetail.html (Thymeleaf) -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>产品详情</title>
</head>
<body>
<h1>产品详情</h1>
<p>名称:<span th:text="${product.name}">产品名称</span></p>
<p>价格:¥<span th:text="${product.price}">0.00</span></p>
<p>库存:<span th:text="${product.stock}">0</span> 件</p>
</body>
</html>
黄俪的设计原则:Thymeleaf 模板只读取 product 对象属性,不调用数据库,不写业务判断。如果以后换成 FreeMarker 或 Vue 前端渲染,只需替换模板文件,Controller 完全不用改。
Controller 层(小崔)
// ProductController.java
package com.feixiang.web;
import com.feixiang.model.Product;
import com.feixiang.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductService productService;
@GetMapping("/{id}")
public String detail(@PathVariable Long id, Model model) {
// 1. 调用 Model 层获取数据
Product product = productService.findById(id);
// 2. 将数据放入 Model 容器,供 View 读取
model.addAttribute("product", product);
// 3. 返回逻辑视图名,由 ViewResolver 解析
return "productDetail";
}
}
代码分析:
- Controller 不直接创建 Product 对象的数据,而是委托给 ProductService(Model 层)
- Controller 不拼接 HTML 字符串,而是返回逻辑视图名
"productDetail"(View 层解耦) org.springframework.ui.Model是 Spring MVC 提供的"数据搬运工",负责把数据从 Controller 送到 View
李眉的配置
// WebConfig.java
@Configuration
@EnableWebMvc
@ComponentScan("com.feixiang")
public class WebConfig implements WebMvcConfigurer {
@Bean
public ViewResolver viewResolver() {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine());
resolver.setCharacterEncoding("UTF-8");
return resolver;
}
// Thymeleaf 引擎配置省略...
}
李眉的部署笔记:ViewResolver 把 "productDetail" 解析为 templates/productDetail.html。如果客户要求换成 JSP,只需修改 ViewResolver 配置,Controller 代码零改动——这就是 MVC 分层带来的可替换性。
易错场景与面试考点
误区一:Controller 里写业务逻辑
错误代码:
@Controller
public class OrderController {
@Autowired
private OrderRepository orderRepository;
@PostMapping("/orders")
public String create(OrderForm form) {
// ❌ 错误:Controller 里直接写业务规则
if (form.getQuantity() > 100) {
throw new IllegalArgumentException("单次购买不能超过 100 件");
}
double finalPrice = form.getPrice() * form.getQuantity();
if (form.getVip()) {
finalPrice *= 0.9; // VIP 折扣逻辑也写在 Controller 里
}
Order order = new Order();
order.setPrice(finalPrice);
orderRepository.save(order); // 直接访问数据层
return "orderSuccess";
}
}
纠正:Controller 应该只负责"接收请求、调用 Service、返回视图"。业务规则(限购、折扣计算)属于 Model 层,应该封装在 Service 中:
@Service
public class OrderService {
public Order createOrder(OrderForm form) {
// 业务规则集中在这里
if (form.getQuantity() > 100) {
throw new IllegalArgumentException("单次购买不能超过 100 件");
}
double finalPrice = calculatePrice(form);
// ...
}
}
后果:如果 Controller 里写满业务逻辑,当需求变更(如折扣规则调整)时,需要修改所有相关的 Controller。而按 MVC 分层,只需修改 Service 层。
误区二:Model 层直接操作 HTTP 对象
错误代码:
@Service
public class ReportService {
public void exportReport(HttpServletResponse response) { // ❌ Service 依赖 Web 层 API
response.setContentType("application/pdf");
// ...
}
}
纠正:Model 层(Service)应该对 Web 层完全无感知。如果需要输出文件,Service 返回 byte[] 或 InputStream,由 Controller 写入 ResponseEntity:
@Service
public class ReportService {
public byte[] generateReport() { // ✅ 返回纯数据
// ...
}
}
面试高频:Spring MVC 中 MVC 各层分别对应什么?
标准回答:
| MVC 层 | Spring MVC 对应组件 | 职责 |
|---|---|---|
| Model | Service + Repository + Entity | 业务逻辑、数据访问、数据封装 |
| View | Thymeleaf / JSP / JSON 视图 | 数据可视化呈现 |
| Controller | DispatcherServlet + @Controller 方法 | 请求分发、参数绑定、调用 Model、选择 View |
加分点:强调 Spring MVC 对 Controller 层的细化——DispatcherServlet 负责"找对人",Controller 方法负责"办对事"。这种分工让框架处理基础设施,开发者专注业务映射。
小结
MVC 设计模式的核心价值是关注点分离:Model 管数据与规则,View 管展示,Controller 管协调。Spring MVC 并非发明了新的架构,而是将 MVC 模式在 Web 领域做了工程化落地——用 DispatcherServlet 统一入口、用注解声明映射、用 ViewResolver 解耦视图技术。
理解 MVC 分层后,你就能判断代码应该写在哪一层:业务规则进 Service,页面渲染进模板,请求映射进 Controller。任何一层的越权,都会破坏分层的可维护性和可测试性。
本章与全局的关系:本章建立了"三层分离"的思想框架。下一章"前端控制器模式"将深入讲解 Spring MVC 如何在 Controller 层内部做进一步分工——DispatcherServlet 作为唯一入口的设计原理。