Stateful Session Bean 能在多次方法呼叫間保持會話狀態,是實作購物車、精靈對話、多步驟表單的經典選擇。本文完整示範:遠端介面定義、Bean 實作、客戶端狀態保持驗證與 Web 環境下 Session 綁定最佳實踐。
package service;
import java.util.List;
import javax.ejb.Remote;
@Remote
public interface ShoppingCart {
void addCommodity(String value);
List<String> getCommodity();
}
package service;
import java.util.ArrayList;
import java.util.List;
import javax.ejb.Stateful;
@Stateful
public class ShoppingCartBean implements ShoppingCart {
private List<String> shoppingCart = new ArrayList<String>();
@Override
public void addCommodity(String value) {
shoppingCart.add(value);
}
@Override
public List<String> getCommodity() {
return shoppingCart;
}
}
| 特性 | @Stateless | @Stateful |
|---|---|---|
| 狀態保持 | 無 (每次呼叫可能不同實例) | 有 (同一客戶端專屬實例) |
| 實例池 | 有 (共用池) | 無 (專屬實例) |
| 鈍化/活化 | 無 | 有 (閒置超時序列化至硬碟) |
| 併發存取 | 多執行緒安全 (容器保證) | 單執行緒存取 (同一時間僅一客戶端) |
| 適用場景 | 無狀態服務、工具類、外觀 | 購物車、精靈對話、多步驟流程 |
package test;
import java.util.List;
import java.util.Properties;
import javax.naming.InitialContext;
import service.ShoppingCart;
public class Client {
public static void main(String[] args) throws Exception {
Properties prop = new Properties();
prop.setProperty("java.naming.factory.initial", "org.jnp.interfaces.NamingContextFactory");
prop.setProperty("java.naming.provider.url", "localhost:1099");
InitialContext ctx = new InitialContext(prop);
// 關鍵:使用同一個 proxy 物件呼叫多次
ShoppingCart shoppingCart = (ShoppingCart) ctx.lookup("ShoppingCartBean/remote");
shoppingCart.addCommodity("自行車");
shoppingCart.addCommodity("筆記本電腦");
shoppingCart.addCommodity("液晶電視");
List<String> commodities = shoppingCart.getCommodity();
for (String commodity : commodities) {
System.out.println(commodity);
}
}
}
自行車 筆記本電腦 液晶電視
三次 addCommodity 累積在同一個 Bean 實例的 shoppingCart 中,最終 getCommodity 返回全部三項。
置於客戶端專案 src 目錄:
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory java.naming.provider.url=localhost:1099
有此檔案時,客戶端程式碼可簡化為:
InitialContext ctx = new InitialContext(); // 自動讀取 classpath 下的 jndi.properties
ShoppingCart shoppingCart = (ShoppingCart) ctx.lookup("ShoppingCartBean/remote");
Servlet/JSP 中不可 將 Stateful Bean 存在 ServletContext 或靜態變數 (會導致所有用戶共用同一購物車)。正確做法:
@WebServlet("/cart")
public class CartServlet extends HttpServlet {
@EJB ShoppingCartRemote cartRemote; // 或 JNDI 查詢
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
HttpSession session = req.getSession(true);
ShoppingCart cart = (ShoppingCart) session.getAttribute("shoppingCart");
if (cart == null) {
// 首次建立:從 EJB 容器取得新實例
try {
InitialContext ctx = new InitialContext();
cart = (ShoppingCart) ctx.lookup("ShoppingCartBean/remote");
session.setAttribute("shoppingCart", cart);
} catch (NamingException e) { throw new ServletException(e); }
}
String item = req.getParameter("item");
if (item != null) cart.addCommodity(item);
// 導向顯示頁
req.getRequestDispatcher("/cart.jsp").forward(req, resp);
}
}
@Stateful
@StatefulTimeout(unit = TimeUnit.MINUTES, value = 30) // EJB 3.1+, 30 分鐘閒置後自動移除
@Remove(retainIfException = false) // 標記移除方法
public class ShoppingCartBean implements ShoppingCart {
// ...
@Remove // 呼叫此方法後容器銷毀 Bean
public void checkout() {
// 結帳邏輯
}
}
| 錯誤現象 | 原因 | 解決 |
|---|---|---|
| 狀態不保持 (每次只有一項) | 每次呼叫都 ctx.lookup() 取得新實例 | 同一客戶端會話重用同一 proxy 物件 |
NoSuchEJBException | Bean 已被容器移除 (逾時/呼叫 @Remove) | 捕獲例外重新 lookup、調整 @StatefulTimeout |
| 序列化失敗 | Bean 持有非序列化欄位 (Connection、Thread) | 欄位標記 transient、在 @PrePassivate 釋放 |
| 併發存取例外 | 多執行緒同時呼叫同一 Stateful Bean | 確保單執行緒存取、或改用 Stateless + 外部狀態存儲 |