EJB與Spring的整合
作為一個輕量級的容器,Spring通常被認為當作是EJB的替代方案。不可否認,在許多應用場合中,Spring以其卓越的效能與豐富的事務處理、ORM、JDBC存取等功能,確實可以取代EJB。不過,我們應當注意到,Spring本身並不排斥EJB,事實上,Spring還為在框架內部存取EJB、實現EJB的功能而提供了良好的支援。而且,如果利用Spring訪問EJB所提供的服務,那麼這些服務的具體實現可以在本地EJB、遠端EJB,或者POJO之間進行透明地切換,而無需改動任何一行客戶端的程式程式碼。
在本文中,我們著重討論一下Spring訪問、實現EJB的機制,特別是對於無狀態會話bean(SLSB)的支援。
1.如何在Spring中訪問EJB?
在Java EE中,為了呼叫一個本地或者遠端SLSB的方法,客戶端必須首先通過JNDI查詢獲取一個(本地或者遠端)EJB Home物件,而後通過這個 EJB Home物件的create()方法獲取一個實際的(本地或者遠端)EJB物件,從而呼叫此EJB物件的各種方法。
為了避免底層程式碼的重複,許多EJB應用程式採用了Service Locator和Business Delegate設計模式,這比在客戶端程式中使用很多JNDI查詢要好很多,但其缺陷也是十分明顯的,例如:
* 依賴Service Locator和Business Delegate singleton的程式程式碼通常是難以測試的。
* 若只使用Service Locator模式,而不使用Business Delegate,那麼應用程式依舊必須呼叫一個EJB Home的craete()方法,並處理各種異常。因此,應用程式將與EJB的API與複雜程式設計模型緊密耦合在一起。
* 使用Business Delegate模式通常將造成大量的重複程式碼,僅僅為了呼叫EJB的同一個方法,開發人員必須編寫許多程式。
而通過Spring,則可以建立和使用在Spring內部配置好的代理,這個代理將扮演業務delegate的角色,開發人員無需編寫另外一個Service Locator和JNDI查詢程式碼,也無需複製Business Delegate的方法呼叫,除非是在這些程式程式碼確實有其他有價值的內容。
假設有一個需要使用本地EJB的web controller,我們將採用EJB的業務方法介面模式來實現,從而EJB的本地介面可以擴充套件一個非EJB的業務方法介面。我們將這個業務方法暫且稱之為MyComponent,其定義類似於:
public interface MyComponent {
...
}
使用業務方法介面模式的一個主要原因在於保證本地介面中的方法定義和介面實現bean的方法定義自動同步,另一原因是如果需要將該方法以POJO實現,那麼這種轉化將比較容易。當然,我們還需要實現本地home介面,並提供一個實現SessionBean和MyComponet業務方法介面的類。現在,為了將示例中的web controller與EJB實現掛鉤,我們需要編寫的唯一一段程式碼就是在這個controller中對外公開一個MyComponet物件的setter,示例如下:
private MyComponent myComponent;
public void setMyComponent(MyComponent myComponent) {
this.myComponent = myComponent;
}
然後,便可以在controller中的任何一個業務方法中呼叫這個myComponet例項。假設需要在Spring容器之外獲取該controller物件,我們可以在同一環境中配置一個LocalStatelessSessionProxyFactoryBean 例項作為EJB代理。這個代理的配置以及controller中myComponent屬性的設定,均在Spring的配置檔案定義,示例如下:
<bean id="myComponent"
class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
<property name="jndiName" value="myComponent"/>
<property name="businessInterface" value="com.ejbtest.MyComponent"/>
</bean>
<bean id="myController" class="com.ejbtest.myController">
<property name="myComponent" ref="myComponent"/>
</bean>
其中,通過myComponent的定義建立了一個EJB代理,而這個EJB將實現特定的業務方法介面。由於EJB的本地home在應用程式啟動時就已經被快取,所以這裡只需要一個JNDI查詢。每當這個EJB被呼叫時,EJB代理將呼叫本地EJB的classname()方法,並呼叫EJB提供的相應的業務方法。
這種EJB存取機制極大地簡化了應用程式程式碼,web層程式碼(抑或其他EJB客戶端)將不再依賴EJB的使用。如果我們想將EJB替換為POJO或者其他測試stub,只要把配置檔案中的myComponent定義改動一下即可,Java程式依舊保持不變。而且,我們也不必再編寫任何JNDI查詢程式碼,或者其他EJB的硬編碼。
實踐表明,上述方法(包括對目標EJB的反射式呼叫)的執行開銷是最小的。記住,我們並不想做任何針對EJB的細粒度呼叫,畢竟這涉及到Java應用伺服器中EJB體系架構的成本開銷。
對於遠端EJB的訪問,本質上與訪問本地EJB是相同的,除了需要SimpleRemoteStatelessSessionProxyFactoryBean類。當然,無論是否使用Spring,遠端呼叫的基本語義是適用的。當訪問另外一臺電腦的虛擬機器中某個物件的方法時,這種呼叫的具體用法和失敗處理必須以不同於本地訪問的方式處理。
Spring在EJB客戶端支援方面,比非Spring方案具有更多優勢。通常,EJB客戶端在呼叫 本地或遠端EJB時,很難做自由的前後切換。 這是因為遠端介面方法必須丟擲RemoteException,並且客戶端程式必須處理此異常,但對於本地介面方法的呼叫則不需要這麼做。若將處理本地EJB的客戶端程式用來處理遠端EJB,那麼必須作出修改以增加對遠端異常情況的有效處理,同樣,若將處理遠端EJB的客戶端程式用來處理本地EJB,也需要做很多無謂的工作來處理遠端異常,或者將異常處理的程式程式碼刪除。不過,當使用Spring遠端EJB代理時,開發人員不必在業務邏輯方法介面和具體的EJB實現程式碼中宣告丟擲任何RemoteException,並且將有一個統一的遠端介面(這個介面丟擲RemoteException),並通過這個代理動態地分析客戶端呼叫的是遠端EJB還是本地EJB。 也即,客戶端程式不需要再處理RemoteException,在呼叫EJB過程中丟擲的任何RemoteException均將被重新以RemoteAccessException類的形式丟擲。於是,應用程式可以在本地EJB和遠端EJB(甚至是POJO)間任意切換呼叫,而客戶端程式碼無需關心這些後臺的操作。當然,這個解決方案是可選的,使用者可以根據自己的意願在業務介面中宣告丟擲RemoteExceptions。
2.如何利用Spring實現EJB
Spring還提供了若干易於使用的類,來幫助開發人員實現EJB。Spring鼓勵開發人員將業務邏輯以POJO的形式實現,而將EJB的處理重點放在事務分界與遠端呼叫。
無論是無狀態的會話bean,還是有狀態的會話bean,抑或訊息驅動的bean,其實現均需分別繼承AbstractStatelessSessionBean, AbstractStatefulSessionBean, 和
AbstractMessageDrivenBean/AbstractJmsMessageDrivenBean。
考慮一個無狀態的會話bean的例子。業務介面定義如下:
public interface MyComponent {
public void myMethod(...);
...
}
我們用一個POJO實現該介面:
public class MyComponentImpl implements MyComponent {
public String myMethod(...) {
...
}
...
}
最後,定義無狀態的會話bean:
public class MyComponentEJB extends AbstractStatelessSessionBean
implements MyComponent {
MyComponent myComp;
/**
* 從BeanFactory或者ApplicationContext獲取POJO物件
* @參見 org.springframework.ejb.support.AbstractStatelessSessionBean的onEjbCreate()方法
*/
protected void onEjbCreate() throws CreateException {
myComp = (MyComponent) getBeanFactory().getBean(
ServicesConstants.CONTEXT_MYCOMP_ID);
}
// 具體的業務方法,以POJO實現
public String myMethod(...) {
return myComp.myMethod(...);
}
...
}
AbstractStatelessSessionBean預設地建立並裝載一個Spring IoC容器,這個容器對EJB也是有用處的,例如獲取POJO服務物件。容器的裝載過程由 BeanFactoryLocator的一個子類完成,預設情況下使用ContextJndiBeanFactoryLocator來實現。 ContextJndiBeanFactoryLocator從一個資原始檔建立ApplicationContext,資原始檔的位置由JNDI環境變數指定,例如java:comp/env/ejb/BeanFactoryPath。如果需要更改BeanFactory/ApplicationContext裝載策略,那麼預設的BeanFactoryLocator實現可以通過呼叫setBeanFactoryLocator()方法來過載,這個方法可以在setSessionContext()或者EJB的建構函式中進行呼叫。
有狀態的會話bean在其生命週期中存在休眠與啟用的過程,若此有狀態的會話bean使用了一個非序列化的容器例項,由於該bean無法被EJB容器儲存,因此它必須從 ejbPassivate()和ejbActivate()分別手工地呼叫unloadBeanFactory()和loadBeanFactory()方法。為了使用EJB,ContextJndiBeanFactoryLocator類需要裝載一個ApplicationContext,這足以一般情況的需要。不過,若ApplicationContext裝載很多bean,或者這些bean的初始化可能佔用大量時間或記憶體資源的話,例如初始化Hibernate的一個SessionFactory物件,這個解決方案就存在較大的問題,因為每一個EJB都將會有自己的一個copy。在此情況下,開發人員可以考慮過載預設的ContextJndiBeanFactoryLocator,並使用BeanFactoryLocator的另一個變體,例如ContextSingletonBeanFactoryLocator類,這樣便可以在多個EJB或客戶端之間共享同一個容器。示例如下:
/**
* 過載預設的BeanFactoryLocator實現
* @參見javax.ejb.SessionBean#setSessionContext(javax.ejb.SessionContext)
*/
public void setSessionContext(SessionContext sessionContext) {
super.setSessionContext(sessionContext);
setBeanFactoryLocator(ContextSingletonBeanFactoryLocator.getInstance());
setBeanFactoryLocatorKey(ServicesConstants.PRIMARY_CONTEXT_ID);
}
而後,建立一個bean定義檔案,例如beanRefContext.xml,這個檔案將定義EJB可能會用到的所有bean工廠。一般情況下,這個檔案將只包含一個bean的定義,例如:
<beans>
<bean id="businessBeanFactory" class="org.springframework.context.support.ClassPathXmlApplicationContext">
<constructor-arg value="businessApplicationContext.xml" />
</bean>
</beans>
其中 businessApplicationContext.xml檔案包含了全部業務POJO物件的定義。ServicesConstants.PRIMARY_CONTEXT_ID則可以定義如下:
public static final String ServicesConstants.PRIMARY_CONTEXT_ID = "businessBeanFactory";
3.EJB與Spring究竟在何時進行整合?
儘管Spring和EJB的目標都是為鬆散耦合的POJO提供企業級服務,但現在很多人都認為Spring+Hibernate的方案,可以充分替代EJB,尤其EJB複雜的程式設計模型總是令人望而生畏,EJB的部署、執行效率亦總為人詬病。不過,EJB尤其是EJB 3.0在複雜資訊系統構建方面,依舊有著Spring目前不能超越的特性。使用EJB 3.0的時候,基於標準的方法、註釋的使用、以及與應用程式伺服器的緊密整合,實現了廠商無關性,並提高開發效率。尤其是,EJB 3.0在面向事務處理的、尤其是非同步通訊環境下的企業級應用方面,比Spring更具備優勢。在處理fail-over、load balancing、distributed caching和state duplication、clustering等方面,開發人員在EJB 3.0的環境中不必關心這些問題(當然,在部署時需要考慮)。而在Spring中,需要使用宣告式事務服務來管理Hibernate事務,開發人員必須在XML配置檔案中顯式地配置Spring TransactionManager和Hibernate SessionFactory物件。Spring應用程式開發者必須顯式地管理跨多個HTTP請求的事務。此外,要在Spring應用程式中使用群集服務也沒有簡單的途徑。
EJB與Spring本質上是相互競爭且相互學習的技術,很難說二者的整合是否能夠充分彰顯二者的優點,彌補彼此的缺陷。