EJB 2.0 引入本地介面與容器管理關係(CMR)前,實體 EJB 通常建模粗粒度領域物件,主因在於遠端通訊系統開銷阻礙客戶端細粒度存取企業層物件。粗粒度設計透過值物件(Value Object)改善效能——壓縮所有資料在客戶端與企業層間傳遞。但包含大量領域物件的複雜系統,仍導致系統內出現過多值物件,並在企業層與客戶端間形成緊密耦合。EJB 2.0 之前,Bean 提供者須明確提供維護領域物件間關聯的程式碼,領域物件間複雜關係下值物件設計變得極其複雜。
EJB 2.0 容器管理持久化(CMP)與關係欄位(CMR)、本地介面的出現,開啟嶄新途徑。本文示範如何結合 EJB 2.0 強大機制、Bean 組件與 JAXP,動態建立基於資料結構的 XML,在企業層與表現層間傳遞資訊。
企業層到客戶端傳遞資料使用 XML,可實現應用中多種鬆耦合;但新增領域物件到實體模型時,可能需新增建立新 DOM 結構的類別。本文開發一個框架,動態遍歷容器管理的關係與本地 EJB 領域,創建可在應用多層間傳遞的 XML 文件。此法優點:
EJB 2.0 本地介面促進存取 Bean 組件;結合本地介面可包含與其他容器管理 Bean 的關係。例如服務系統中,UserEJB 可有與 ServiceRequestEJB 一對多雙向關係、與 ProductEJB 一對一單向關聯、與 ServiceRequestHistoryEJB 一對多雙向關係,以及與 PhoneEJB 一對多雙向關係。透過本地介面與容器管理關係,可設計複雜物件圖。
容器管理持久化與關係欄位在 Bean 類別中用抽象存取方法定義。關係欄位存取方法視關係基數返回集合或定義關係的 Bean 本地介面,可透過 Bean 組件本地介面公開。EJB 2.0 詳細規範超出本文範圍,請參閱官方規範。
高效設計模式應透過外觀組件公開使用案例,禁止從客戶端直接存取實體組件。以服務系統為例,「取得指定用戶詳細資料」為一使用案例。外觀組件查找所需用戶實體組件,透過 CMP 與 CMR 欄位取得資料並返回表現層。
資料傳輸物件明顯選擇是簡單 Java Bean:UserBean 含代表持久化與關係欄位的屬性,關係欄位為 java.util.Collection 或其他 Bean 組件。UserBean 可能有 ServiceRequestBean 與 PhoneBean 集合;ServiceRequestBean 可能有 ProductBean 與多個 ServiceRequestHistory Bean。但此選擇主要缺點:使實體模型更複雜、資料傳輸物件 Bean 階層更複雜、在服務層與消費層間創建緊密耦合。
更好選擇:使用 XML DOM 物件作資料傳輸物件。組件產生 org.w3c.dom.Document 物件,表現組件用 JSP 自定義標籤與 XSLT 樣式表「消費」它們。
如何從 CMP 本地 Bean 參考建立 XML 文件?可用基於建立不同型 DOM 組件方法的工廠,但會創建過多「工廠」組件,擴展領域模型時可能需新增「工廠」組件。需要的是:通用工具類,接收實體本地物件、導航容器管理關係、動態建立 DOM 結構,處理雙向關係中的循環參考避免無限迴圈、深度導航關係元素。
package ws.business.service.util;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.ejb.EJBLocalObject;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Iterator;
import java.beans.Introspector;
import java.beans.BeanInfo;
import java.beans.PropertyDescriptor;
import java.beans.IntrospectionException;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
public class DOMGenerator {
private Document doc;
private int drillDownDepth;
private ArrayList _circularRef = new ArrayList();
private int _currentDepth;
private static String RESERVED = "EJBLocalHome^localHandle^class^primaryKey";
public DOMGenerator(String docElementName, int drillDownDepth) {
try {
DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = fact.newDocumentBuilder();
doc = builder.newDocument();
} catch (ParserConfigurationException ex) {
throw new RuntimeException(ex);
}
this.drillDownDepth = drillDownDepth;
doc.appendChild(doc.createElement(docElementName));
}
public Document getDOM(EJBLocalObject local) {
try {
populateElement(doc.getDocumentElement(), local);
return doc;
} catch (IntrospectionException | IllegalAccessException | InvocationTargetException ex) {
throw new RuntimeException(ex.getMessage());
}
}
private void populateElement(Element parent, EJBLocalObject local)
throws IntrospectionException, IllegalAccessException, InvocationTargetException {
_circularRef.add(local);
_currentDepth++;
BeanInfo info = Introspector.getBeanInfo(local.getClass());
PropertyDescriptor[] properties = info.getPropertyDescriptors();
for (int i = 0; i < properties.length; i++) {
Method propReadMethod = properties[i].getReadMethod();
String propName = properties[i].getName();
Object prop = propReadMethod.invoke(local, null);
if (RESERVED.indexOf(propName) >= 0) continue;
try {
EJBLocalObject locProp = (EJBLocalObject) prop;
if (isCircularRef(locProp) || _currentDepth >= drillDownDepth) continue;
Element child = doc.createElement(propName);
populateElement(child, locProp);
parent.appendChild(child);
} catch (ClassCastException ex1) {
try {
Collection colProp = (Collection) prop;
if (_currentDepth >= drillDownDepth) continue;
Element child = doc.createElement(propName);
Iterator it = colProp.iterator();
while (it.hasNext()) {
EJBLocalObject locProp = (EJBLocalObject) it.next();
if (isCircularRef(locProp)) continue;
Element grandChild = doc.createElement(propName + "-child");
child.appendChild(grandChild);
populateElement(grandChild, locProp);
}
parent.appendChild(child);
} catch (ClassCastException ex2) {
parent.setAttribute(propName, prop.toString());
}
}
}
_circularRef.remove(local);
_currentDepth--;
}
private boolean isCircularRef(EJBLocalObject local) {
Iterator it = _circularRef.iterator();
while (it.hasNext())
if (local.isIdentical((EJBLocalObject) it.next())) return true;
return false;
}
}
User local = userHome.findByPrimaryKey("1");
return new DOMGenerator("user", 4).getDOM(local);
產生的 DOM 視實體模型而定(範例):
<?xml version="1.0" encoding="UTF-8"?>
<user firstName="Wayne" id="1" lastName="Zheng">
<requests>
<requests-child description="Outlook not working" id="2" status="P">
<product description="Install Laptop" id="1" name="SVC01"/>
</requests-child>
<requests-child description="PC not booting" id="1" status="O">
<histories>
<histories-child description="Information requested" id="2" loggedAt="2002-4-7 12:12:12.0"/>
<histories-child description="Request logged" id="1" loggedAt="2000-4-7 00:00:00.0"/>
</histories>
<product description="Install Laptop" id="1" name="SVC01"/>
</requests-child>
</requests>
<phones>
<phones-child id="2" number="0771 8210586" type="M"/>
<phones-child id="1" number="01908 251575" type="W"/>
</phones>
</user>
表現層可用 XSLT 或 XML JSP 自定義標籤交付企業層產生的 XML 資料至客戶裝置。新增領域模型定義時,僅需定義細粒度本地實體 Bean 與容器管理關係,DOM 產生器提供通用方法產生動態 XML 文件。
但此法有缺點:
本文詳細研究在 J2EE 應用企業層與表現層間傳遞複雜資料的模型。透過動態遍歷 CMP/CMR 建立 XML,消除手工維護值物件的負擔,實現層間鬆耦合。雖有效能、型別安全、模型化標準等考量,但在需頻繁變更領域模型、追求快速開發與層間解耦的場景,此模式極具吸引力。