MVC 三層架構中,控制器接收前端請求參數,需傳遞給模型層執行業務邏輯與資料庫操作;模型層處理完畢,需將結果(查詢數據、狀態碼、錯誤訊息)回傳控制器,再由控制器決定視圖渲染或轉向。若直接用 HttpServletRequest、Map 或零散參數傳遞,會導致:
本文介紹經典解法:ParamBean(命令/傳輸物件模式),封裝請求參數、業務狀態、查詢結果,實現層間解耦與類型安全。
package com.framework.core;
import java.io.Serializable;
import java.sql.ResultSet;
import java.util.Hashtable;
import javax.sql.rowset.CachedRowSet;
import com.sun.rowset.CachedRowSetImpl; // 具體實現,生產環境建議用標準 RowSetProvider
public class ParamBean implements Serializable {
// === 審計/日誌欄位 ===
private String oprCode = ""; // 操作員代碼
private String depCode = ""; // 部門代碼
private String menuCode = ""; // 選單代碼
private String opDesc = ""; // 操作描述
// === 統一回應狀態 ===
private int retCode = 0; // 0=成功, -1=失敗, 其他=業務錯誤碼
private String errMsg = ""; // 錯誤訊息
// === 動態參數載體 ===
private final Hashtable params = new Hashtable<>();
// === 斷開連線的結果集(可序列化、跨層傳遞) ===
private CachedRowSet crs;
// === 建構子 ===
public ParamBean() {}
// === 審計資訊一鍵設定 ===
public void setInfo(String oprCode, String depCode, String menuCode, String opDesc) {
this.oprCode = oprCode;
this.depCode = depCode;
this.menuCode = menuCode;
this.opDesc = opDesc;
}
// === 動態參數操作(核心) ===
public void addParam(String name, Object value) {
if (name == null || value == null) return;
name = name.trim();
if (name.isEmpty()) return;
params.put(name, value);
}
public Object getParam(String name) {
if (name == null) return null;
return params.get(name.trim());
}
public void removeParam(String name) {
if (name != null) params.remove(name.trim());
}
public int getParamCount() { return params.size(); }
public void clearParams() { params.clear(); }
public Hashtable getParams() { return params; }
// === CachedRowSet 封裝(關鍵:解決 ResultSet 不可序列化/連線綁定問題) ===
public boolean setCrs(ResultSet rs) {
try {
this.crs = new CachedRowSetImpl();
this.crs.populate(rs); // 將 ResultSet 數據完整複製到記憶體
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public CachedRowSet getCrs() { return crs; }
public void setCrs(CachedRowSet crs) { this.crs = crs; }
// === 狀態輔助方法 ===
public void setError(String msg) { this.retCode = -1; this.errMsg = msg; }
public void setSuccess() { this.retCode = 0; this.errMsg = ""; }
public boolean isSuccess() { return retCode == 0; }
// === Getters/Setters 省略 ===
// oprCode, depCode, menuCode, opDesc, retCode, errMsg 標準存取子
}
| 特性 | ResultSet | CachedRowSet |
|---|---|---|
| 序列化 | ❌ 不可序列化(遠程 EJB/RMI/Session 複製失效) | ✅ 實作 Serializable |
| 連線綁定 | ✅ 需保持 Connection 開啟 | ❌ 斷開連線,數據在記憶體 |
| 可捲動/可更新 | 視 JDBC 驅動支援度 | ✅ 保證支援 TYPE_SCROLL_INSENSITIVE, CONCUR_UPDATABLE |
| 跨層傳遞 | ❌ 危險(DAO 關閉連線後失效) | ✅ 安全,控制器可直接綁定 JSP/前端 |
結論:DAO 層查詢後立即 paramBean.setCrs(rs) 並關閉連線,控制器拿到的是純記憶體數據結構,零資源洩漏風險。
public class LoginController extends BaseController {
public void doPost(HttpServletRequest req, HttpServletResponse resp) {
ParamBean param = new ParamBean();
// 從 Session 取得登入上下文
String depCode = (String) req.getSession().getAttribute("LOGIN_USER_DEP");
param.addParam("DEP_CODE", depCode);
// 從請求參數綁定
param.addParam("USER_CODE", req.getParameter("userCode"));
param.addParam("PASSWORD", req.getParameter("password"));
// 設定審計資訊
param.setInfo(
(String) req.getSession().getAttribute("LOGIN_USER_ID"),
depCode,
"LOGIN_MENU",
"使用者登入驗證"
);
// 呼叫模型層業務邏輯
AuthService authService = new AuthService();
ParamBean result = authService.loginAuth(param);
// 統一結果判斷與視圖分發
if (result.isSuccess()) {
req.setAttribute("PARAM_CRS", result.getCrs()); // 綁定查詢結果給視圖
forward("loginSuccess.jsp", req, resp);
} else {
req.setAttribute("ERROR_MSG", result.getErrMsg());
forward("loginFail.jsp", req, resp);
}
}
}
public class AuthService {
public ParamBean loginAuth(ParamBean param) throws Exception {
String userCode = (String) param.getParam("USER_CODE");
String password = (String) param.getParam("PASSWORD");
// 清空輸入參數,準備裝載查詢結果
param.clearParams();
String sql = "SELECT user_id, user_name, role_id FROM sys_user " +
"WHERE user_code = ? AND password = ? AND status = '1'";
// 執行查詢(DAO 封裝 JDBC 樣板代碼)
sqlExecQuery(param, sql, userCode, password);
return param;
}
private void sqlExecQuery(ParamBean param, String sql, Object... args) {
DatabaseBean db = getDatabase(); // 連線池封裝
param.setError("查詢失敗"); // 預設失敗
try (Connection conn = db.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
// 綁定參數
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
try (ResultSet rs = ps.executeQuery()) {
param.setCrs(rs); // 關鍵:複製到 CachedRowSet
param.setSuccess();
}
} catch (Exception e) {
param.setError(e.getMessage());
e.printStackTrace();
} finally {
db.close(); // 歸還連線
}
}
}
<%@ taglib prefix="c" uri="https://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sql" uri="https://java.sun.com/jsp/jstl/sql" %>
用戶ID 用戶名 角色
| 方向 | 說明 |
|---|---|
| 泛型化參數 Map | 將 Hashtable 升級為 Map,支援型別安全取值 getParam(name, String.class)。 |
| 驗證框架整合 | 加入 Bean Validation (JSR-380) 註解於 ParamBean 子類,Controller 統一驗證。 |
| 分頁/排序封裝 | 內建 pageNo, pageSize, sortField, sortOrder 標準欄位,DAO 自動解析生成 LIMIT/OFFSET。 |
| 異常轉換器 | 全域異常處理器捕獲 Service 異常,自動填充 ParamBean 錯誤碼/訊息,Controller 無需 try-catch。 |
| 替換為 DTO/VO | 現代架構傾向定義具體 LoginRequest, LoginResponse 類別,替代通用 ParamBean,獲得編譯期型別安全與 IDE 提示。 |
ParamBean 模式以單一可序列化物件封裝「輸入參數、業務狀態、查詢結果、審計資訊」,實現:
CachedRowSet 斷開連線,消除連線洩漏與序列化障礙。retCode/errMsg/isSuccess() 標準化錯誤處理流程。雖然現代框架(Spring MVC、MyBatis、JPA)已提供更優雅的 @RequestBody/@ModelAttribute/ResultHandler 機制,但理解 ParamBean 設計思想,有助於在無框架環境、舊系統維護、或自定義框架時做出正確架構決策。