延續前四篇 Session Bean 基礎教學,本文實作一個實用的 無狀態 Session Bean——Pricing,從資料庫讀取產品代碼與價格,展示企業級 Bean 開發完整流程:介面設計、實作類別、資源管理、部署配置、客戶端測試。
CREATE TABLE price (
product_code VARCHAR(10) NOT NULL PRIMARY KEY,
price DECIMAL(10,2) NOT NULL
);
-- 測試資料
INSERT INTO price (product_code, price) VALUES ('A1', 1.59);
package usingj2ee.pricing;
import java.rmi.*;
import javax.ejb.*;
public interface Pricing extends EJBObject {
String[] getProductCodes() throws RemoteException;
double getPrice(String productCode) throws RemoteException, InvalidProductCodeException;
}
package usingj2ee.pricing;
import java.rmi.*;
import javax.ejb.*;
public interface PricingHome extends EJBHome {
Pricing create() throws RemoteException, CreateException;
}
無狀態 Bean 的 Home 介面僅需無參 create(),因不維護客戶狀態。
package usingj2ee.pricing;
import java.rmi.*;
import java.util.*;
import javax.ejb.*;
import java.sql.*;
import javax.sql.*;
import javax.naming.*;
public class PricingImpl implements SessionBean {
private SessionContext context;
private Connection conn;
public PricingImpl() {}
// 1. 設定 Session Context
public void setSessionContext(SessionContext aContext) {
context = aContext;
}
// 2. 建立時取得資料庫連線
public void ejbCreate() throws CreateException {
try { conn = getConnection(); }
catch (Exception e) { throw new CreateException("DB連線失敗: " + e); }
}
// 3. Passivate 時關閉連線 (容器可能序列化 Bean)
public void ejbPassivate() throws EJBException {
try { if (conn != null) { conn.close(); conn = null; } }
catch (Exception e) { throw new EJBException("關閉連線失敗: " + e); }
}
// 4. Activate 時重新取得連線
public void ejbActivate() throws EJBException {
try { conn = getConnection(); }
catch (Exception e) { throw new EJBException("重新連線失敗: " + e); }
}
// 5. 移除時清理資源
public void ejbRemove() throws EJBException {
try { if (conn != null) { conn.close(); conn = null; } }
catch (Exception e) { throw new EJBException("關閉連線失敗: " + e); }
}
// 業務方法:取得所有產品代碼
public String[] getProductCodes() throws EJBException {
Statement s = null;
try {
s = conn.createStatement();
ResultSet rs = s.executeQuery("SELECT product_code FROM price");
Vector v = new Vector();
while (rs.next()) v.addElement(rs.getString("product_code"));
String[] codes = new String[v.size()];
v.copyInto(codes);
return codes;
} catch (Exception e) { throw new EJBException("查詢失敗: " + e); }
finally { try { if (s != null) s.close(); } catch (Exception ignore) {} }
}
// 業務方法:取得指定產品價格
public double getPrice(String productCode) throws EJBException, InvalidProductCodeException {
PreparedStatement ps = null;
try {
ps = conn.prepareStatement("SELECT price FROM price WHERE product_code = ?");
ps.setString(1, productCode);
ResultSet rs = ps.executeQuery();
if (rs.next()) return rs.getDouble("price");
else throw new InvalidProductCodeException(productCode);
} catch (SQLException e) { throw new EJBException("查詢價格失敗: " + e); }
finally { try { if (ps != null) ps.close(); } catch (Exception ignore) {} }
}
// 取得連線:透過 JNDI 查找 DataSource
protected Connection getConnection() throws SQLException, NamingException {
InitialContext ic = new InitialContext();
DataSource ds = (DataSource) ic.lookup("java:comp/env/jdbc/PriceDB");
return ds.getConnection();
}
}
| 方法 | 時機 | 動作 |
|---|---|---|
| ejbCreate | Bean 建立 | 取得連線 |
| ejbPassivate | 容器閒置 Bean | 關閉連線 (不可序列化) |
| ejbActivate | 容器重新啟用 Bean | 重新取得連線 |
| ejbRemove | Bean 銷毀 | 關閉連線 |
java:comp/env/jdbc/PriceDB 為邏輯名稱,部署時綁定至實體 DataSource (如 jdbc/Cloudscape、jdbc/Oracle),Bean 程式碼無需修改即可切換資料庫。
使用 ? 佔位符 + setString,自動處理引號跳脫,提升安全性與效能。
EJBException (RuntimeException),容器自動回滾交易InvalidProductCodeException,客戶端可捕捉處理PricingImpl.class、PricingHome.class、Pricing.classjdbc/PriceDB,類型 javax.sql.DataSource,認證 Containerjdbc/PriceDB 對應至伺服器實際 DataSource (如 jdbc/Cloudscape)
package usingj2ee.pricing;
import java.util.*;
import javax.naming.*;
import javax.rmi.*;
public class TestPricing {
public static void main(String[] args) {
try {
Context ctx = new InitialContext();
PricingHome home = (PricingHome) PortableRemoteObject.narrow(
ctx.lookup("Pricing"), PricingHome.class);
Pricing pricing = (Pricing) home.create();
String[] codes = pricing.getProductCodes();
for (String code : codes) {
System.out.println(code + ": " + pricing.getPrice(code));
}
// 測試例外處理
try { pricing.getPrice("f00b4r"); }
catch (InvalidProductCodeException e) {
System.out.println("預期例外: " + e);
}
pricing.remove();
} catch (Exception e) { e.printStackTrace(); }
}
}
| 問題 | 可能原因 | 解決方向 |
|---|---|---|
| EAR/JAR 未生成 | 實作類別違反 EJB 限制 (finalize、static 欄位等) | 啟用 Verifier 檢查相容性,查看 logs 目錄錯誤訊息 |
| 客戶端找不到 JNDI 服務 | EJB 伺服器未啟動、Context Factory 未設定、JNDI 名稱不符 | 確認伺服器運行、命令列設定 naming.factory.initial、大小寫完全一致 |
| DataSource 存取錯誤 | 未設定資源參考、邏輯名稱未綁定實體 DataSource | 檢查 deploytool 資源參考頁籤、JNDI 名稱頁籤綁定 |
Pricing Bean 展示了無狀態 Session Bean 封裝資料庫存取的標準模式:Remote/Home 介面定義契約、實作類別管理資源生命週期、JNDI 解耦資源配置、PreparedStatement 安全查詢、標準例外處理。此模式可直接套用至任何「查詢型」業務邏輯,是企業級 Java 開發的基石範本。