早期 JSP 開發常陷入「Model 1」誤區:單個 JSP 同時承擔請求接收、業務邏輯、資料庫操作、HTML 渲染,導致代碼耦合度極高、難以測試、前後端協作混亂。Sun 官方早於 JSP 0.92 規範即推薦 **Model 2 (MVC)** 架構:Servlet 作為 Controller、JavaBean/EJB 作為 Model、JSP 作為 View。本文系統闡述 MVC 在傳統 JSP 專案中的分層設計、包結構規範、數據流轉與工程化實踐。
| 層 | 角色 | 技術實現 | 核心職責 | 禁止事項 |
|---|---|---|---|---|
| View (V) | 展示邏輯 | JSP + JSTL/EL + 自定義標籤 + CSS/JS | 數據渲染、表單提交、頁面跳轉、國際化、權限性顯示 | 包含 JDBC 代碼、業務計算、事務控制、複雜邏輯判斷 |
| Controller (C) | 請求調度 | Servlet / Struts Action / Spring MVC Controller | 參數接收驗證、調用 Service、異常捕獲、選擇視圖、設置響應狀態 | 直接操作資料庫、包含業務規則、生成 HTML 片段 |
| Model (M) | 業務與數據 | Service (POJO/EJB) + DAO + Domain Object (VO/PO/DTO) | 業務規則執行、事務邊界、數據持久化、外部系統集成、緩存策略 | 依賴 Servlet API、持有請求/會話狀態、直接輸出響應 |
com.company.project
├── web # Web 層 (Controller + View)
│ ├── controller # Servlet/Action/Controller
│ │ ├── user
│ │ │ ├── UserListController.java
│ │ │ ├── UserEditController.java
│ │ │ └── UserDeleteController.java
│ │ └── ...
│ ├── taglib # 自定義標籤
│ └── filter # 過濾器 (編碼、登入檢查、日誌)
├── service # 業務邏輯層
│ ├── user
│ │ ├── UserService.java # 介面
│ │ └── impl
│ │ └── UserServiceImpl.java # 實現,@Transactional
│ └── ...
├── dao # 數據訪問層
│ ├── user
│ │ ├── UserDao.java
│ │ └── impl
│ │ └── UserDaoImpl.java
│ └── ...
├── domain # 領域模型 (跨層傳遞)
│ ├── vo # View Object (視圖專用)
│ │ └── UserVO.java
│ ├── dto # Data Transfer Object (服務間傳遞)
│ │ └── UserDTO.java
│ ├── po # Persistent Object (映射表)
│ │ └── UserPO.java
│ └── entity # 核心實體 (含業務行為)
│ └── User.java
├── util # 工具類
└── config # 配置類
// domain/po/GuestbookPO.java
public class GuestbookPO {
private Long id;
private String title;
private String body;
private Long authorId;
private Long targetId;
private Date createTime;
private Boolean isRead;
// Getter/Setter
}
// domain/vo/GuestbookVO.java (視圖專用,含格式化、衍生字段)
public class GuestbookVO {
private Long id;
private String title;
private String summary; // 正文摘要
private String authorName; // 關聯查詢獲得
private String timeAgo; // "3分鐘前"
// Getter/Setter
}
public interface GuestbookDao {
GuestbookPO findById(Long id);
List<GuestbookPO> findPage(String condition, String orderBy, int offset, int limit);
int count(String condition);
void insert(GuestbookPO po);
void update(GuestbookPO po);
void deleteById(Long id);
}
// 實現使用 JDBC Template / MyBatis / 手寫 JDBC,無任何業務判斷
public interface GuestbookService {
GuestbookVO getDetail(Long id);
PageResult<GuestbookVO> queryPage(GuestbookQuery query);
void publish(GuestbookDTO dto); // 發布留言
void markRead(Long id); // 標記已讀
void delete(Long id, Long operatorId); // 刪除(權限檢查)
}
public class GuestbookServiceImpl implements GuestbookService {
@Autowired GuestbookDao dao;
@Autowired UserDao userDao;
@Transactional
public void publish(GuestbookDTO dto) {
// 1. 業務校驗
if (StringUtils.isBlank(dto.getTitle())) throw new BizException("標題不能為空");
if (dto.getBody().length() > 2000) throw new BizException("內容過長");
// 2. 構建持久化對象
GuestbookPO po = new GuestbookPO();
po.setTitle(dto.getTitle());
po.setBody(dto.getBody());
po.setAuthorId(dto.getAuthorId());
po.setTargetId(dto.getTargetId());
po.setCreateTime(new Date());
po.setIsRead(false);
// 3. 持久化
dao.insert(po);
// 4. 衍生業務:通知目標用戶、積分獎勵等
notificationService.send(dto.getTargetId(), "新留言:" + dto.getTitle());
pointService.add(dto.getAuthorId(), 5, "發布留言");
}
public PageResult<GuestbookVO> queryPage(GuestbookQuery query) {
int total = dao.count(query.buildCondition());
List<GuestbookPO> pos = dao.findPage(query.buildCondition(), query.buildOrder(),
query.getOffset(), query.getPageSize());
// PO -> VO 轉換 (可用 MapStruct)
List<GuestbookVO> vos = pos.stream().map(this::convertToVO).collect(Collectors.toList());
return PageResult.of(vos, total, query.getPageNum(), query.getPageSize());
}
private GuestbookVO convertToVO(GuestbookPO po) {
GuestbookVO vo = new GuestbookVO();
vo.setId(po.getId());
vo.setTitle(po.getTitle());
vo.setSummary(StringUtils.abbreviate(po.getBody(), 50));
vo.setAuthorName(userDao.findNameById(po.getAuthorId()));
vo.setTimeAgo(DateUtils.formatAgo(po.getCreateTime()));
return vo;
}
}
@WebServlet("/guestbook/*")
public class GuestbookController extends HttpServlet {
@Autowired GuestbookService service; // 需配合 Spring WebApplicationContext
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String action = req.getPathInfo(); // /list, /detail, /delete
try {
switch (action) {
case "/list": list(req, resp); break;
case "/detail": detail(req, resp); break;
case "/delete": delete(req, resp); break;
default: resp.sendError(404);
}
} catch (BizException e) {
req.setAttribute("error", e.getMessage());
forward("/error.jsp", req, resp);
} catch (Exception e) {
log.error("系統異常", e);
forward("/500.jsp", req, resp);
}
}
private void list(HttpServletRequest req, HttpServletResponse resp) {
GuestbookQuery query = new GuestbookQuery();
query.setPageNum(ParseUtils.parseInt(req.getParameter("page"), 1));
query.setPageSize(20);
// 其他查詢條件綁定...
PageResult<GuestbookVO> result = service.queryPage(query);
req.setAttribute("pageResult", result);
forward("/guestbook/list.jsp", req, resp);
}
private void forward(String path, HttpServletRequest req, HttpServletResponse resp) {
req.getRequestDispatcher(path).forward(req, resp);
}
}
<%@ taglib prefix="c" uri="https://java.sun.com/jsp/jstl/core" %>
<c:forEach items="${pageResult.records}" var="msg">
<div class="message">
<h3>${fn:escapeXml(msg.title)}</h3>
<p>${fn:escapeXml(msg.summary)}</p>
<small>作者:${msg.authorName} | ${msg.timeAgo}</small>
</div>
</c:forEach>
<!-- 分頁導航 -->
<c:url value="/guestbook/list" var="baseUrl">
<c:param name="page" value="${pageResult.pageNum - 1}"/>
</c:url>
<a href="${baseUrl}">上一頁</a> ...
早期 Struts 1 的 Action、ActionForm、ActionForward 即 MVC 的具體實現。Spring MVC 以 DispatcherServlet 為前端控制器,@Controller + @RequestMapping 簡化配置,HandlerAdapter 適配多種處理器,ViewResolver 解耦視圖技術(JSP/Thymeleaf/FreeMarker/JSON),是現代化改造的首選路徑。
JSP 設計模式的核心即 **MVC 分層**。嚴格遵循「Controller 調度、Service 業務、DAO 持久、JSP 渲染」的職責邊界,配合領域模型分離(PO/VO/DTO)、事務邊界下沉、異常統一處理、依賴注入管理,能構建出結構清晰、可測試、可維護、易擴展的傳統 Java Web 應用。即使最終遷移至前後端分離架構,良好的後端分層依然是微服務拆分、API 設計的堅實基石。