【Day37】MVC 设计模式:原理与手动实现简易 MVC 框架

本文收录于「Java 学习日记」专栏,聚焦 Java Web 核心设计模式 ------MVC,从底层原理拆解到手动实现简易 MVC 框架,帮你理解 SpringMVC 的底层逻辑,夯实架构基础~

一、为什么要学 MVC 设计模式?

在前面的 Servlet+JSP 开发中,我们遇到了一个核心问题:职责混乱

  • 一个 Servlet 既处理请求参数、又写业务逻辑、还负责跳转页面,代码臃肿不堪;
  • JSP 中混合 Java 代码,展示和逻辑耦合严重,维护成本极高;
  • 新增功能时,需要修改大量代码,不符合 "开闭原则"。

MVC 设计模式(Model-View-Controller) 就是为解决这些问题而生:

  • 核心目标:将 "数据处理、页面展示、请求控制" 三者分离,降低代码耦合度,提高可维护性和扩展性;
  • 实际应用:SpringMVC、Struts2 等主流 Web 框架都是基于 MVC 模式设计的,理解 MVC 是掌握这些框架的关键;
  • 学习价值:手动实现简易 MVC 框架,能让你从 "使用框架" 升级到 "理解框架",建立架构思维。

今天这篇日记,我们先拆解 MVC 的核心原理,再手把手实现一个简易的 MVC 框架,彻底搞懂 MVC 的工作方式。

二、MVC 设计模式核心原理

1. MVC 三大组件的职责

MVC 将 Web 应用分为三个核心部分,各司其职、相互协作:

组件 中文名称 核心职责 对应技术实现(Java Web)
Model(模型) 数据模型 封装数据、处理业务逻辑(如数据库操作) JavaBean、Service、DAO
View(视图) 视图展示 展示数据、接收用户输入,仅负责页面渲染 JSP、HTML、Thymeleaf
Controller(控制器) 控制器 接收请求、分发请求、协调 Model 和 View Servlet

2. MVC 工作流程(核心)

MVC 的核心是 "控制器居中调度,模型处理数据,视图只做展示",完整流程:

3. MVC 模式的核心优势

  • 职责分离:数据处理、页面展示、请求控制分开,代码结构清晰;
  • 可维护性高:修改页面样式只需改 View,修改业务逻辑只需改 Model,互不影响;
  • 可复用性强:Model 层的业务逻辑可被多个 Controller/View 复用;
  • 便于扩展:新增功能只需新增对应的 Controller/Model/View,符合开闭原则。

三、传统开发的问题(反例)

先看一个传统 Servlet 开发的反例,感受 MVC 要解决的问题:

java

运行

java 复制代码
// 传统Servlet:职责混乱(接收请求+业务逻辑+页面跳转)
@WebServlet("/user/query")
public class UserServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 接收请求参数(控制器职责)
        String userId = request.getParameter("id");
        
        // 2. 处理业务逻辑(模型职责)
        User user = null;
        if ("1".equals(userId)) {
            user = new User("admin", 18);
        }
        
        // 3. 存储数据(控制器职责)
        request.setAttribute("user", user);
        
        // 4. 跳转页面(控制器职责)
        request.getRequestDispatcher("/user.jsp").forward(request, response);
    }
}

这个 Servlet 既做了控制器的工作,又做了模型的工作,代码耦合度极高 ------ 如果业务逻辑变复杂,这个类会变得臃肿不堪。

四、手动实现简易 MVC 框架(核心实战)

我们基于 MVC 思想,手动实现一个极简版的 MVC 框架,核心目标:

  1. 控制器(Controller)只负责请求分发,不写业务逻辑;
  2. 模型(Model)专注处理业务逻辑;
  3. 视图(View)只负责展示数据;
  4. 新增功能只需新增 Controller 和 Model,无需修改核心代码。

步骤 1:定义核心规范(接口)

1.1 模型层规范:UserService(处理用户业务)

java

运行

java 复制代码
// src/main/java/com/mvc/service/UserService.java
package com.mvc.service;

import com.mvc.model.User;

// Model层:业务逻辑接口
public interface UserService {
    // 根据ID查询用户
    User getUserById(String userId);
    // 用户登录
    boolean login(String username, String password);
}

1.2 模型层实现:UserServiceImpl

java

运行

java 复制代码
// src/main/java/com/mvc/service/impl/UserServiceImpl.java
package com.mvc.service.impl;

import com.mvc.model.User;
import com.mvc.service.UserService;

// Model层:业务逻辑实现(模拟数据库操作)
public class UserServiceImpl implements UserService {
    @Override
    public User getUserById(String userId) {
        // 模拟数据库查询
        if ("1".equals(userId)) {
            return new User("Java日记", 18, "admin@test.com");
        } else {
            return new User("游客", 0, "guest@test.com");
        }
    }

    @Override
    public boolean login(String username, String password) {
        // 模拟登录校验
        return "admin".equals(username) && "123456".equals(password);
    }
}

1.3 数据模型:User(封装数据)

java

运行

java 复制代码
// src/main/java/com/mvc/model/User.java
package com.mvc.model;

// Model层:数据模型(JavaBean)
public class User {
    private String username;
    private Integer age;
    private String email;

    // 无参构造
    public User() {}

    // 有参构造
    public User(String username, Integer age, String email) {
        this.username = username;
        this.age = age;
        this.email = email;
    }

    // getter/setter
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

步骤 2:控制器层(核心调度)

2.1 基础控制器:BaseController(封装通用逻辑)

java

运行

java 复制代码
// src/main/java/com/mvc/controller/BaseController.java
package com.mvc.controller;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

// 所有控制器的父类,封装通用方法
public class BaseController extends HttpServlet {
    // 转发到指定页面
    protected void forward(HttpServletRequest request, HttpServletResponse response, String path) throws ServletException, IOException {
        request.getRequestDispatcher(path).forward(request, response);
    }

    // 重定向到指定页面
    protected void redirect(HttpServletRequest request, HttpServletResponse response, String path) throws IOException {
        response.sendRedirect(request.getContextPath() + path);
    }

    // 解决POST请求中文乱码
    protected void setPostEncoding(HttpServletRequest request) {
        try {
            request.setCharacterEncoding("UTF-8");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.2 用户控制器:UserController(仅负责调度)

java

运行

java 复制代码
// src/main/java/com/mvc/controller/UserController.java
package com.mvc.controller;

import com.mvc.model.User;
import com.mvc.service.UserService;
import com.mvc.service.impl.UserServiceImpl;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

// Controller层:仅负责请求接收和调度
@WebServlet("/user/*")
public class UserController extends BaseController {
    // 注入Model层对象(实际框架中是IOC容器管理,这里手动new)
    private UserService userService = new UserServiceImpl();

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取请求路径,分发请求
        String uri = request.getRequestURI();
        String action = uri.substring(uri.lastIndexOf("/") + 1);

        // 根据不同action调用不同方法
        if ("query".equals(action)) {
            queryUser(request, response);
        } else if ("toLogin".equals(action)) {
            toLoginPage(request, response);
        }
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        setPostEncoding(request);
        String uri = request.getRequestURI();
        String action = uri.substring(uri.lastIndexOf("/") + 1);

        if ("login".equals(action)) {
            login(request, response);
        }
    }

    // 查询用户
    private void queryUser(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 接收参数(控制器职责)
        String userId = request.getParameter("id");

        // 2. 调用Model层处理业务(不写业务逻辑,只调度)
        User user = userService.getUserById(userId);

        // 3. 存储数据到域对象
        request.setAttribute("user", user);

        // 4. 转发到视图
        forward(request, response, "/userInfo.jsp");
    }

    // 跳转到登录页
    private void toLoginPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        forward(request, response, "/login.jsp");
    }

    // 用户登录
    private void login(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1. 接收参数
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        // 2. 调用Model层校验
        boolean isLogin = userService.login(username, password);

        if (isLogin) {
            // 登录成功,存储用户信息到session
            request.getSession().setAttribute("loginUser", username);
            // 重定向到首页
            redirect(request, response, "/index.jsp");
        } else {
            // 登录失败,存储错误信息,转发回登录页
            request.setAttribute("errorMsg", "用户名或密码错误!");
            forward(request, response, "/login.jsp");
        }
    }
}

步骤 3:视图层(仅展示数据)

3.1 登录页面:login.jsp

jsp

复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>登录页(View)</title>
</head>
<body>
    <h1>用户登录</h1>
    <c:if test="${not empty errorMsg}">
        <font color="red">${errorMsg}</font><br>
    </c:if>
    <form action="${pageContext.request.contextPath}/user/login" method="post">
        用户名:<input type="text" name="username"><br>
        密码:<input type="password" name="password"><br>
        <input type="submit" value="登录">
    </form>
</body>
</html>

3.2 用户信息页面:userInfo.jsp

jsp

复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>用户信息(View)</title>
</head>
<body>
    <h1>用户信息展示</h1>
    <table border="1" cellpadding="5" cellspacing="0">
        <tr>
            <th>用户名</th>
            <th>年龄</th>
            <th>邮箱</th>
        </tr>
        <tr>
            <td>${user.username}</td>
            <td>${user.age}</td>
            <td>${user.email}</td>
        </tr>
    </table>
</body>
</html>

3.3 首页:index.jsp

jsp

复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>首页(View)</title>
</head>
<body>
    <c:if test="${empty sessionScope.loginUser}">
        <c:redirect url="/user/toLogin"></c:redirect>
    </c:if>
    <h1>欢迎你,${sessionScope.loginUser}!</h1>
    <a href="${pageContext.request.contextPath}/user/query?id=1">查看用户信息</a>
</body>
</html>

步骤 4:项目结构(规范)

最终项目结构符合 MVC 分层规范,清晰易维护:

plaintext

复制代码
src/main/
├── java/
│   └── com/mvc/
│       ├── controller/  # 控制器层(Servlet)
│       │   ├── BaseController.java
│       │   └── UserController.java
│       ├── model/       # 数据模型层(JavaBean)
│       │   └── User.java
│       └── service/     # 业务逻辑层(Model核心)
│           ├── UserService.java
│           └── impl/
│               └── UserServiceImpl.java
└── webapp/              # 视图层(View)
    ├── login.jsp
    ├── userInfo.jsp
    └── index.jsp

步骤 5:测试运行

  1. 将项目部署到 Tomcat,启动服务器;
  2. 访问 http://localhost:8080/mvc/user/toLogin(跳转到登录页);
  3. 输入admin/123456登录,跳转到首页;
  4. 点击 "查看用户信息",展示用户数据;
  5. 输入错误密码,登录页显示错误信息。

五、简易 MVC 框架的核心优化(贴近 SpringMVC)

我们实现的简易 MVC 还比较基础,SpringMVC 在此基础上做了更多优化,核心点:

1. 前端控制器(DispatcherServlet)

  • 我们的UserController只能处理 /user/* 的请求,而 SpringMVC 的DispatcherServlet是全局控制器,接收所有请求,再分发到不同的 Controller;
  • 核心:通过配置(注解 / XML)映射请求路径和 Controller 方法,无需手动解析 URI。

2. 注解驱动

  • 我们用@WebServlet("/user/*")配置路径,SpringMVC 用@Controller@RequestMapping注解,更灵活;

  • 示例: java

    运行

    java 复制代码
    @Controller
    @RequestMapping("/user")
    public class UserController {
        @RequestMapping("/query")
        public String queryUser(String id, Model model) {
            User user = userService.getUserById(id);
            model.addAttribute("user", user);
            return "userInfo"; // 视图名,由视图解析器解析路径
        }
    }

3. 视图解析器

  • 我们手动写转发路径/userInfo.jsp,SpringMVC 通过视图解析器统一配置前缀 / 后缀,只需返回视图名即可;

  • 示例: xml

    XML 复制代码
    <!-- 视图解析器配置 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

4. IOC 容器

  • 我们手动new UserServiceImpl(),SpringMVC 通过 IOC 容器管理 Bean,自动注入依赖,降低耦合。

六、MVC 模式避坑指南

  1. 控制器职责越界:Controller 中不要写业务逻辑,只做请求分发,否则回到 "职责混乱" 的问题;
  2. 视图层写逻辑:JSP 中不要写 Java 脚本片段,仅用 EL+JSTL 展示数据,保持 View 的纯粹性;
  3. 模型层耦合视图:Model 层(Service/DAO)不要依赖 View 层(JSP),否则无法复用;
  4. 数据传递混乱:Controller 向 View 传递数据时,统一用 request/session 域,避免直接在 Model 中存储域数据。

七、今日实战小任务

  1. 基于我们实现的简易 MVC 框架,新增 "用户退出" 功能(Controller 新增 logout 方法,Model 无需修改,View 新增退出按钮);
  2. 新增 "修改用户信息" 功能,实现完整的 "查询 - 修改 - 展示" 流程,严格遵循 MVC 分层;
  3. 尝试给简易 MVC 添加 "视图解析器" 功能(封装前缀 / 后缀,Controller 只需返回视图名)。

总结

  1. MVC 设计模式将 Web 应用分为 Model(数据 / 业务)、View(展示)、Controller(控制)三层,核心是 "职责分离、控制器居中调度";
  2. 手动实现的简易 MVC 框架中,Controller 仅负责请求分发,Service(Model)处理业务逻辑,JSP(View)只做展示,彻底解决代码耦合问题;
  3. SpringMVC 是 MVC 模式的高级实现,核心优化包括前端控制器(DispatcherServlet)、注解驱动、视图解析器、IOC 容器等,理解简易 MVC 是掌握 SpringMVC 的关键。

下一篇【Day38】预告:SpringMVC 入门:核心组件、注解使用与实战案例,关注专栏从手写 MVC 过渡到主流框架~若本文对你有帮助,欢迎点赞 + 收藏 + 关注,你的支持是我更新的最大动力💖!

相关推荐
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 基于java的医院床位管理系统的设计与开发 为例,包含答辩的问题和答案
java·开发语言
曹轲恒2 小时前
SpringBoot的热部署
java·spring boot·后端
会员果汁2 小时前
18.设计模式-桥接模式
设计模式·桥接模式
Remember_9932 小时前
深入理解 Java String 类:从基础原理到高级应用
java·开发语言·spring·spring cloud·eclipse·tomcat
老蒋每日coding2 小时前
AI Agent 设计模式系列(九)——学习和适应模式
人工智能·学习·设计模式
程序员侠客行2 小时前
Mybatis插件原理及分页插件
java·后端·架构·mybatis
a努力。2 小时前
得物Java面试被问:Netty的ByteBuf引用计数和内存释放
java·开发语言·分布式·python·面试·职场和发展
Mcband2 小时前
Spring Boot 整合 ShedLock 处理定时任务重复执行的问题
java·spring boot·后端