前言
Struts 框架中 Model 層負責封裝業務邏輯與資料,核心組件包含 ActionForm (表單資料)、Action (控制流程) 與 JavaBean/EJB (業務實體)。本文系統性解析三類 Bean 的職責、作用域、驗證機制與最佳實踐。
Model 三大組件架構
Model Layer
├── ActionForm Beans (表單資料傳輸)
├── Action Beans (請求處理控制)
└── Business Beans (業務邏輯與資料)
├── JavaBeans (POJO 領域模型)
├── System State Beans (系統狀態如購物車)
└── EJBs (企業級分散式組件)
一、Bean 作用域與生命週期
| 作用域 | 生命週期 | 可見範圍 | Servlet API 對應 | 適用場景 |
| page | 單一 JSP 請求 | 當前頁面 | Service 方法區域變數 | 頁面暫時資料 |
| request | 單一 HTTP 請求 | 轉發鏈路內所有頁面 | Request 屬性 | 請求處理結果傳遞 |
| session | 用戶會話期間 | 同用戶所有請求 | Session 屬性 | 用戶登入狀態、購物車 |
| application | Web 應用存活期間 | 所有用戶、所有請求 | ServletContext 屬性 | 全域配置、快取資料 |
跨組件共享 Bean 範例
// Servlet 中存入
MyStudy study = new MyStudy(...);
request.setAttribute("study", study);
// JSP 中取用
<jsp:useBean id="study" scope="request" class="com.mycompany.MyApp.MyStudy"/>
二、ActionForm Bean 詳細設計
核心職責
- 封裝 HTTP 請求參數 (表單欄位)
- 提供基礎輸入驗證 (格式、必填)
- 保留使用者輸入值支援錯誤回顯
- 作為 View 與 Controller 間資料傳遞契約
Struts 自動處理流程
- 檢查配置作用域 (request/session) 是否存在同類實例
- 不存在則自動建立新實例並存入對應作用域
- 依請求參數名稱匹配 Bean 屬性,自動呼叫 Setter 注入值
- 若配置
validate="true",呼叫 validate() 方法
- 驗證通過則傳遞給 Action.execute(),失敗則轉向
input 頁面
- Action 執行完畢可選擇性重置或保留 Form Bean
ActionForm 實作範本
public class UserRegistrationForm extends ActionForm {
private String username;
private String email;
private String password;
private String confirmPassword;
private Date birthDate;
// Getter/Setter 省略
@Override
public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) {
ActionErrors errors = new ActionErrors();
if (username == null || username.trim().isEmpty()) {
errors.add("username", new ActionMessage("error.username.required"));
} else if (username.length() < 3 || username.length() > 20) {
errors.add("username", new ActionMessage("error.username.length"));
}
if (email == null || !email.matches("^[\\w.-]+@[\\w.-]+\\.\\w+$")) {
errors.add("email", new ActionMessage("error.email.format"));
}
if (password == null || password.length() < 6) {
errors.add("password", new ActionMessage("error.password.length"));
}
if (!password.equals(confirmPassword)) {
errors.add("confirmPassword", new ActionMessage("error.password.mismatch"));
}
return errors.isEmpty() ? null : errors;
}
@Override
public void reset(ActionMapping mapping, HttpServletRequest request) {
// 重置預設值 (如下拉選單預設選項)
this.username = "";
this.email = "";
}
}
設計原則
- 粒度:可細粒度 (每表單一 Bean) 或粗粒度 (多表單共用),視業務複雜度決定
- 無業務邏輯:僅做資料承載與格式驗證,業務規則委派給 Action 或 Service
- 屬性命名:遵循 JavaBean 慣例,表單欄位
userName 對應 setUserName()
- 不可變性:避免在 Form Bean 中持有可變集合物件,防止多請求間狀態汙染
三、Action Bean 詳細設計
核心職責
- 接收經驗證的 ActionForm
- 協調業務邏輯執行 (呼叫 Service/Manager/DAO)
- 處理例外與交易邊界
- 根據執行結果選擇轉向 (ActionForward)
執行緒安全要求
Action 為單例 (Singleton),多執行緒共用同一實例,嚴禁使用實例變數儲存請求相關狀態。
// ❌ 錯誤:實例變數會被多執行緒共享
private UserService userService; // 這個是安全的 (無狀態服務)
private String currentUsername; // 危險!請求狀態洩漏
// ✅ 正確:請求資料全部來自參數或 ThreadLocal
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) {
UserRegistrationForm regForm = (UserRegistrationForm) form;
String username = regForm.getUsername(); // 從 Form 取得
// ...
}
Action 實作範本
public class UserRegistrationAction extends Action {
private UserService userService; // 由 Spring/IOC 注入
public void setUserService(UserService userService) {
this.userService = userService;
}
@Override
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
// 1. 取消鍵檢查
if (isCancelled(request)) {
return mapping.findForward("cancel");
}
UserRegistrationForm regForm = (UserRegistrationForm) form;
try {
// 2. 呼叫業務服務 (交易邊界在 Service 層)
UserVO user = new UserVO();
user.setUsername(regForm.getUsername());
user.setEmail(regForm.getEmail());
user.setPassword(regForm.getPassword());
userService.registerUser(user);
// 3. 成功訊息
saveMessage(request, "message.registration.success");
// 4. 轉向成功頁
return mapping.findForward("success");
} catch (BusinessException e) {
// 5. 業務異常處理
saveErrors(request, e.getActionErrors());
return mapping.getInputForward(); // 回輸入頁
}
}
}
Action 實用方法
| 方法 | 用途 |
| getLocale(request) | 取得用戶 Locale (支援國際化) |
| getResources() | 取得 MessageResources (訊息國際化) |
| isCancelled(request) | 檢查用戶是否按取消鍵 |
| saveErrors(request, errors) | 儲存錯誤訊息供 JSP 顯示 |
| saveMessages(request, messages) | 儲存成功/資訊訊息 |
四、業務層 Bean 設計 (JavaBean / EJB)
系統狀態 Bean
// Session 作用域:購物車
public class ShoppingCart implements Serializable {
private Map items = new HashMap<>();
public void addItem(Product product, int quantity) { ... }
public void removeItem(Product product) { ... }
public BigDecimal getTotalPrice() { ... }
// 無業務規則,純資料容器
}
// Application 作用域:產品目錄快取
public class ProductCatalog {
private Map products = new ConcurrentHashMap<>();
public Product getProduct(String id) { ... }
public List search(String keyword) { ... }
}
業務邏輯 Bean (Service Layer)
// 無狀態、可重用、不依賴 Web 環境
public class UserService {
private UserDAO userDAO;
private EmailService emailService;
@Transactional
public void registerUser(UserVO user) throws BusinessException {
// 1. 業務規則驗證
if (userDAO.exists(user.getUsername())) {
throw new BusinessException("error.username.exists");
}
// 2. 執行核心邏輯
user.setCreatedDate(new Date());
user.setStatus(UserStatus.ACTIVE);
userDAO.insert(user);
// 3. 發送歡迎郵件 (非同步)
emailService.sendWelcomeEmailAsync(user.getEmail());
}
}
設計鐵律:業務層零 Web 依賴
- ❌
import javax.servlet.*、HttpServletRequest、HttpSession
- ✅ 純 Java 類別,參數為 VO/DTO/Primitive
- ✅ 可在單元測試、批次程式、Web Service 重用
- ✅ Action 負責從 Request 提取資料轉為 VO 傳給 Service
五、資料庫連線池整合
啟動 Servlet 初始化連線池
public class DataSourceInitServlet extends HttpServlet {
private ConnectionPool pool;
@Override
public void init() throws ServletException {
String url = getInitParameter("jdbc.url");
String user = getInitParameter("jdbc.user");
String pass = getInitParameter("jdbc.password");
int maxConn = Integer.parseInt(getInitParameter("jdbc.maxconn"));
pool = new ConnectionPool(url, user, pass, maxConn);
getServletContext().setAttribute("connectionPool", pool);
}
@Override
public void destroy() {
if (pool != null) pool.releaseAll();
}
}
web.xml 配置
<servlet>
<servlet-name>DataSourceInit</servlet-name>
<servlet-class>com.myapp.DataSourceInitServlet</servlet-class>
<init-param><param-name>jdbc.url</param-name><param-value>jdbc:mysql://localhost/mydb</param-value></init-param>
<init-param><param-name>jdbc.user</param-name><param-value>root</param-value></init-param>
<init-param><param-name>jdbc.password</param-name><param-value>secret</param-value></init-param>
<init-param><param-name>jdbc.maxconn</param-name><param-value>20</param-value></init-param>
<load-on-startup>1</load-on-startup>
</servlet>
Action 中使用
Connection conn = null;
try {
ConnectionPool pool = (ConnectionPool) getServletContext().getAttribute("connectionPool");
conn = pool.getConnection();
userDAO.setConnection(conn);
userService.registerUser(user);
} finally {
if (conn != null) pool.returnConnection(conn); // 歸還而非關閉
}
六、struts-config.xml 完整配置範例
<struts-config>
<form-beans>
<form-bean name="userRegistrationForm"
type="com.myapp.form.UserRegistrationForm"/>
</form-beans>
<action-mappings>
<action path="/register"
type="com.myapp.action.UserRegistrationAction"
name="userRegistrationForm"
scope="request"
validate="true"
input="/register.jsp">
<forward name="success" path="/welcome.jsp"/>
<forward name="cancel" path="/index.jsp"/>
</action>
</action-mappings>
<message-resources parameter="ApplicationResources"/>
</struts-config>
總結:Model 分層最佳實踐
| 層級 | 組件 | 職責 | 作用域 | 關鍵原則 |
| 表現層 | ActionForm | 資料承載、格式驗證、錯誤回顯 | request/session | 無業務邏輯、不可變集合 |
| 控制層 | Action | 流程協調、例外處理、轉向決定 | Singleton | 執行緒安全、無實例狀態、薄控制器 |
| 業務層 | Service/Manager | 核心業務規則、交易管理 | Singleton/Prototype | 零 Web 依賴、可獨立測試、POJO |
| 資料層 | DAO/Entity | 持久化操作、資料映射 | Prototype | 介面導向、連線池管理、異常轉換 |
| 狀態層 | State Beans | 會話/應用層級資料快取 | session/application | 序列化、同步控制、生命週期管理 |