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

目录导读
- MVC核心思想与Java生态落地
- 案例背景:图书管理系统的需求拆解
- 代码分层实践:Model/View/Controller的职责边界
- 关键解耦技巧:Servlet+JSP+JavaBean的协同策略
- 常见陷阱与性能优化(含问答环节)
- 架构演进:从经典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的分层思想有更立体的认识。