Java MVC案例

wen java案例 2

Java MVC案例实战:从零搭建高复用三层架构(附完整代码解析)

Java MVC案例

目录导读

  1. MVC核心思想与Java生态落地
  2. 案例背景:图书管理系统的需求拆解
  3. 代码分层实践:Model/View/Controller的职责边界
  4. 关键解耦技巧:Servlet+JSP+JavaBean的协同策略
  5. 常见陷阱与性能优化(含问答环节)
  6. 架构演进:从经典MVC到Spring MVC的映射

MVC核心思想与Java生态落地

MVC(Model-View-Controller)并非Java独有,但它在Java Web开发中演化出了一套成熟的“约定优于配置”模式。Model负责数据与业务规则(如JDBC操作、服务层计算),View负责用户界面渲染(JSP、Thymeleaf等),Controller负责接收请求并调度(Servlet、Filter或前端控制器),在Java中,最基础的落地形式就是Servlet作为Controller + JSP作为View + JavaBean/POJO作为Model,这种组合至今仍是理解Spring MVC的必经之路。

真实场景中的MVC不是三个文件,而是三层逻辑边界,很多案例失败的核心原因,是Controller里塞满了SQL,View里嵌入了业务计算。


案例背景:图书管理系统的需求拆解

我们以一个经典的“图书列表+新增+删除”需求为例,业务规则:每本书有ISBN、书名、价格、库存,操作:用户通过浏览器访问/book/list查看所有图书;通过表单提交/book/add新增图书;通过链接/book/delete?id=1删除图书,该案例虽然简单,但能清晰展示MVC的完整请求流转路径。

目录导读对应模块:此章节对应“模型层的职责延伸”——我们需要创建一个Book类(封装属性)、一个BookDAO(数据访问)、一个BookService(业务判断,如价格必须大于0)。


代码分层实践:职责边界与依赖方向

1 Model层(JavaBean + DAO + Service)

// Book.java —— 纯POJO,无框架依赖
public class Book {
    private int id;
    private String isbn;
    private String name;
    private double price;
    // getters/setters...
}
// BookDAO.java —— 数据层,使用JDBC或MyBatis
public List<Book> findAll() {
    // 此处省略JDBC模板代码
}
// BookService.java —— 业务层,校验库存与价格
public boolean addBook(Book book) {
    if (book.getPrice() <= 0) { return false; }
    return bookDAO.insert(book);
}

重点原则:Service层不能出现HttpServletRequest,Model层绝不能调用JSP内置对象,依赖方向必须是:Controller → Service → DAO,禁止反向。

2 View层(JSP + EL + JSTL)

<%-- bookList.jsp --%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<table>
  <c:forEach items="${books}" var="book">
    <tr>
      <td>${book.name}</td>
      <td><a href="delete?id=${book.id}">删除</a></td>
    </tr>
  </c:forEach>
</table>

关键点:JSP中只允许使用EL表达式和JSTL,禁止写<% %>脚本片段,这能保证视图层不包含业务逻辑,便于后期更换为Thymeleaf。

3 Controller层(Servlet核心调度)

@WebServlet("/book/*")
public class BookServlet extends HttpServlet {
    private BookService service = new BookService();
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String path = req.getPathInfo();
        if ("/list".equals(path)) {
            req.setAttribute("books", service.findAll());
            req.getRequestDispatcher("/bookList.jsp").forward(req, resp);
        } else if ("/delete".equals(path)) {
            int id = Integer.parseInt(req.getParameter("id"));
            service.delete(id);
            resp.sendRedirect("list"); // 重定向避免表单重复提交
        }
    }
    @Override
    protected void doPost(req, resp) throws ServletException, IOException {
        String path = req.getPathInfo();
        if ("/add".equals(path)) {
            Book book = new Book();
            book.setName(req.getParameter("name"));
            // 设置其他字段...
            boolean success = service.addBook(book);
            if (success) { resp.sendRedirect("list"); }
            else { req.setAttribute("error", "价格非法"); req.getRequestDispatcher("/add.jsp").forward(req, resp); }
        }
    }
}

核心规则:Servlet只做三件事——解析参数、调用Service、转发或重定向,任何复杂的字符串拼接或逻辑判断都不应出现在这里。


关键解耦技巧:数据流转与前端控制器

技巧一:统一请求入口(前端控制器模式),上面代码中/book/*就是一个简化版的前端控制器,所有图书相关操作都由同一Servlet分发,这减少了Web.xml中的映射数量,也为未来改造成Spring DispatcherServlet打下基础。

技巧二:从Request到Model的自动封装,如果手动setProperty太啰嗦,可用BeanUtils.populate(book, req.getParameterMap()),但注意——这只适用于简单的Bean,如果涉及类型转换(如日期),必须自定义Converter。

技巧三:Gzip压缩与缓存控制,在Filter中设置resp.setHeader("Cache-Control", "max-age=3600"),对静态资源(图片、CSS)可减少80%的重复请求。


常见陷阱与性能优化(Q&A)

Q1:为什么我改了DAO后,页面永远显示旧数据? A:大概率是浏览器缓存,在Controller的doPost中,必须使用sendRedirect(302重定向),而不是forward,重定向会让浏览器重新请求/book/list,而forward只是服务端内部跳转,刷新时可能触发“重复提交”或旧缓存。

Q2:JSP中可以直接调用Service吗? A:绝对不行,这会导致视图与模型耦合,如果非要这么做,你等于抛弃了MVC的核心优势——可测试性与可维护性,正确方式是:Controller准备数据到Request域,JSP只负责读取。

Q3:目录导读中提到的“数据库连接泄露”怎么排查? A:核心是Connection必须放在finally或try-with-resources中,在Java 7+,推荐在DAO层使用:

try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql)) {
    // 执行
}

如果是手动管理事务,要确保close()commit/rollback之后一定会执行。

性能优化要点

  • 使用PreparedStatement防止SQL注入并预编译。
  • 禁用JSP的autoFlush,批量输出到response.getWriter()
  • 对图书列表增加Pagination(分页),避免一次加载万条数据。

架构演进:从经典MVC到Spring MVC的映射

如果你理解了上面的Servlet案例,那么Spring MVC对你来说只是一层“糖衣”:

  • DispatcherServlet = 前端控制器(就是你的BookServlet)。
  • @Controller + @RequestMapping = 你在Servlet里写的if/else if逻辑。
  • ModelAndView = req.setAttribute + forward
  • @ResponseBody = resp.getWriter().write(...)

但注意:Spring MVC的最大价值在于依赖注入面向切面(AOP),比如你的BookService不再自己new BookDAO(),而是通过注解自动注入——这让单元测试变得极其容易(Mockito可轻松替换依赖),Spring提供全局异常处理器(@ControllerAdvice),比在Servlet中每个方法都try-catch干净得多。

最后建议:学习阶段务必手写一次纯Servlet的MVC案例,然后对照Spring MVC源码阅读,你会发现——框架不是魔法,只是把重复的模板代码模块化而已,希望这篇案例解析能让你对Java Web的分层思想有更立体的认识。

抱歉,评论功能暂时关闭!