回覆:EJB 3 與 Spring 的比較分析

工程 | Mark Fisher | 2007年11月09日 | ...

昨晚我參加了一個新英格蘭 Java 使用者組(NEJUG)的會議,Reza Rahman 在會上發表了關於 EJB 3 和 Spring 的“比較分析”。Reza 是《EJB 3 in Action》的合著者之一。我很欣賞 Reza 能夠就可能被視為有爭議的話題進行演講,也很高興能與他見面。我也讚賞他努力分析了 EJB 3 和 Spring 各自的優缺點。不過,我覺得有必要澄清他關於 Spring 的一些表述,這些表述並不完全準確,並且讓我們(以及其他與會者)認為這次演講可能帶有偏向 EJB 3 的動機。公平地說,與固定的規範版本不同,Spring 持續不斷地發展,我將指出的有些內容是新功能。另一方面,有些內容是 Spring 2.0 的功能,已經發布一年多了。我個人認為,“比較分析”必須包含被比較產品的最新穩定版本的最新功能集。我想不言而喻,我可能也有點偏見,但我在此的動機是提供一個完全客觀的回覆,以便該演講能夠被修改,以反映更“實事求是”的比較。我將對演講中的 10 個“主題”進行簡要回復。

1. EJB 使用註解來表示元資料。Spring 使用 XML。

會上提到 Spring 開始支援更多註解,但“還需要一段時間”。然而,Spring 2.0 版本提供了完整的 JPA 整合,使用 @PersistenceContext 註解來注入 EntityManager,並提供了註解驅動的事務管理,使用 Spring 的 @Transactional 註解(支援與 @Stateless EJB 相同的語義,預設傳播級別為 REQUIRED)。我特別感到失望的是,比較中沒有在雙方都包含 JPA(見下文第 3 點)。Spring 2.0 還引入了完整的基於註解的 AspectJ 支援(@Aspect、@Before、@After、@Around)以及“構造型”註解的概念。例如,@Repository 註解為直接使用 JPA 或 Hibernate API 的資料訪問程式碼(無需 Spring 的模板)提供了非侵入式的異常翻譯。Spring 早在 1.2 版本就提供了註解支援,例如 @ManagedResource,用於透明地將任何 Spring 管理的物件匯出為 JMX MBean。

現在,這個問題對我來說排在第一位的主要原因是“還需要一段時間”的說法。作為 Spring 2.5 註解驅動配置支援的主要開發人員之一,我必須說,Spring 的元資料模型非常靈活,因此我們能夠比預期更快地提供一個全面的基於註解的模型。事實上,Spring 2.5 支援 JSR-250 註解:@Resource、@PostConstruct 和 @PreDestroy,以及 @WebServiceRef 和 @EJB。其中 @Resource 尤其值得關注,因為它是 EJB 3 中用於依賴注入的主要註解。在 Spring 中,@Resource 註解不僅支援 JNDI 查詢(與 EJB 3 相同),還支援注入 *任何 Spring 管理的物件*。這有效地結合了本次演講中提到的 Spring 的主要優勢(Spring 支援任何型別的物件的 DI)和 EJB 3 的主要優勢(使用註解而不是 XML)。Spring 2.5 還引入了一個更細粒度的基於註解的依賴注入模型,基於 @Autowired 和(可擴充套件的)@Qualifier 註解。Spring 2.5 還將“構造型”註解擴充套件到包括 @Service 和 @Controller。每個構造型註解透過將其作為元註解來擴充套件通用的 @Component 註解。透過應用相同的技術,@Component 註解為使用者定義的構造型提供了擴充套件點。Spring 甚至可以自動檢測這些帶註解的元件,作為 XML 配置的替代方案。例如,以下摘錄來自 PetClinic 示例應用程式的 2.5 版本。


   <context:component-scan base-package="org.springframework.samples.petclinic.web" />

由於 Web 控制器使用了註解驅動的依賴注入和用於請求對映的註解,因此不需要額外的 XML。我之所以指出這一點,是因為演講特別強調了 Web 層的配置的冗長。


@Controller
public class ClinicController {

   private final Clinic clinic;

   @Autowired
   public ClinicController(Clinic clinic) {
      this.clinic = clinic;
   }
   ...

有關 Spring 註解支援的最新介紹,請參閱:The Server Side 上的 Spring 2.5 入門教程,或最新版本的 Spring 參考手冊 - 特別是基於註解的配置部分。此外,請繼續關注本部落格和 Spring Framework 主頁,瞭解即將釋出的關於 2.5 版的文章和部落格。

2. Spring 允許您支援多種部署環境,但需要更多配置。

這實際上被 presenters 作為一個 Spring 的優勢來介紹,但強調了配置的開銷。事實是,任何認真對待測試和敏捷開發的專案的都將需要支援“多種部署環境”。換句話說,這個特定主題經常被曲解,好像它只適用於多種*生產*環境。實際上,在每個開發和測試周期中都部署到應用伺服器會是敏捷性的一個重大障礙。通常,Spring 使用者會模組化其配置,以便“基礎設施”配置(例如 DataSource、TransactionManager、JMS ConnectionFactory)是獨立的,並且動態屬性被外部化。由於 Spring 支援根據外部化屬性替換 '${placeholders}',因此包含不同的屬性檔案通常會變成一個透明的問題。

3. EJB 使用 JPA,Spring 使用 Hibernate

我不得不承認,這一點最讓我感到困擾。在比較幻燈片中,EJB 3 的示例顯示了 JPA 透過 *entityManager* 進行資料訪問,並且 *entityManager* 例項是透過 @PersistenceContext 註解提供的。另一方面,Spring 的示例使用了 Hibernate,並顯示了 Hibernate SessionFactory 的 Setter 注入。在我看來,這違反了正宗“比較分析”的第一條規則:在比較的雙方使用最相似的功能。在這種特定情況下,Spring 確實支援直接使用 JPA API(即 JpaTemplate 完全是可選的;直接使用 'entityManager' 仍然參與 Spring 事務等),並且 Spring 也識別 @PersistenceContext 註解。自 Spring 2.0 起(最終釋出已一年多)就提供了此支援,因此我不明白為什麼比較中不也在 Spring 端使用 JPA。比較的其他部分顯然基於 Spring 2.0,因此這給人一種選擇性過時和帶有偏見的印象。如果這個特定示例被修改為“實事求是”的比較,它將削弱一個主要的總體主題:Spring 需要更多配置,而 EJB 3 依賴於標準註解。

現在,儘管我認為在 Spring 端使用 Hibernate 而不是 JPA 扭曲了比較,但它同時也揭示了 Spring 的一個優勢。如果您確實想直接使用 Hibernate API 而不是依賴 JPA API,Spring 支援這一點,並且它在 Spring 事務管理和異常翻譯方面以一致的方式實現。這就有機會使用超越 JPA 限制的 Hibernate 功能,例如 Hibernate 的“criteria”查詢 API。同樣,如果您想新增一些直接的 JDBC 進行資料訪問,而 ORM 是過度的話,Spring 也支援這一點——即使是在與 Hibernate 或 JPA 資料訪問相同的事務中呼叫。

4. Spring 不做任何假設,您必須提供配置。

一個具體的例子是事務管理器的定義。有人說您必須瞭解容器供應商級別的知識才能配置 Spring 整合。這是不正確的。例如,以下 bean 定義不包含任何特定於容器的資訊,但 Spring 將自動檢測所有 Java EE 應用伺服器中的事務管理器。


   <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>

如果您確實想利用特定於容器的功能,例如每個事務的隔離級別,那麼 Spring 也提供了一些專門的實現:*WebLogicJtaTransactionManager*、*WebSphereUowTransactionManager* 和 *OC4JJtaTransactionManager*。切換這些實現之間只需更改這一個定義。

除此之外,Spring 的配置幻燈片不必要地冗長。我擔心這也可能是出於強調 EJB 與 Spring 不同,依賴於智慧預設值的目的。例如,幻燈片顯示了


   <tx:annotation-driven transaction-manager="transactionManager"/>

實際上,如果 Spring 上下文中只定義了一個 'transactionManager',那麼 'annotation-driven' 元素上就不需要顯式提供該屬性。該屬性僅用於啟用在一個應用程式中*如果需要*使用多個事務管理器。這些“自動檢測”和“智慧預設”技術貫穿於 Spring,例如訊息偵聽器的 JMS 'connectionFactory'(在下面的示例 #6 中有體現)以及現有 MBean 伺服器或 RMI 登錄檔的自動定位。

從積極的方面來看,它實際上被提到是 Spring 允許“本地”事務管理的一個優勢。雖然 EJB 需要 JTA 進行事務管理,但許多應用程式不需要跨兩階段提交能力資源進行分散式事務。在這種情況下,Spring 允許使用更簡單、開銷更低的事務管理器:DataSourceTransactionManager(用於 JDBC)、HibernateTransactionManager 或 JpaTransactionManager。如果目標是準確描述優缺點,我本應聽到更多關於 Spring 這一特定優勢的細節。例如,這對於在容器外進行測試或在 Eclipse 或 IDEA 等輕量級 IDE 環境中開發是一個巨大的好處。

此外,如果您確實需要 JTA 進行分散式事務,但又想在 Tomcat 或 Jetty 等輕量級容器中執行,Spring 可以輕鬆支援 Atomikos 和 JOTM 等獨立的 JTA 提供商。當然,Spring 的事務管理器設定需要配置一個*單個* bean 定義,但這確實是一次性成本——但絕對值得。

5. Spring 沒有有狀態應用程式範例。

無狀態服務層的優點作為最佳實踐已經相當確立,Spring 也採納了這一點。然而,Spring 確實提供了單例以外的範圍。Spring 的“prototype”範圍為每次注入或查詢提供了一個獨立的例項,Spring 2.0 引入了 Web 範圍:“request”和“session”。範圍機制本身甚至可以擴充套件;可以定義自定義範圍並將其對映到對話的概念。Spring 還支援使用 CommonsPoolTargetSource 進行簡單的物件池,但物件池很少是狀態管理的最佳解決方案。

更重要的是,Spring 透過 Spring Web Flow 為 Web 應用程式提供了非常健壯、高度可配置的狀態管理。在那裡,對話狀態是透明管理的,這與本次演講中聲稱的開發人員必須直接與 HTTP Session 互動來管理 Spring 應用程式狀態的說法相反。此外,儲存庫配置是可插拔的,因此可以使用各種策略來物理儲存狀態(session、client、後端快取等)。最後,Spring Web Flow 的最新發展包括對擴充套件持久化上下文的支援以及對 JSF 的完全整合支援。

6. Spring 需要為每個 MessageListener 配置一個容器。

Spring 2.5 提供了一個新的 'jms' 名稱空間,以大大簡化訊息偵聽器的配置。請注意,沒有為每個偵聽器配置單獨的容器。多個偵聽器共享配置,並且廣泛使用了智慧預設值。


<jms:listener-container>
	<jms:listener destination="queue.confirm" ref="logger" method="log"/>
	<jms:listener destination="queue.order" ref="tradeService" method="placeOrder"/>
</jms:listener-container>

會上還提到執行緒管理始終是一個容器級問題。然而,事實並非如此。訊息偵聽器容器實際上使用 Spring 的 TaskExecutor 抽象,並且有多種實現可用。例如,如果在 Java 5+ 上執行,您可以配置一個執行緒池執行器,或者甚至可以配置一個 CommonJ WorkManager 執行器。如果需要,這些執行器可以輕鬆地在多個偵聽器容器之間共享。事實上,'task-executor' 屬性在 'listener-container' 元素(上面顯示)上可用,在那裡它將只設置一次,並且由每個內部為每個偵聽器定義建立的容器例項共享。

7. 併發消費者不能大於 1。

好吧,這確實是當晚最奇怪的時刻。程式碼幻燈片描繪了一個完全無狀態的訊息偵聽器實現(正如它應該的那樣!),然後配置幻燈片顯示 'maxConcurrentConsumers' 的值為 1。此時,有人說將該值設定為除一以外的任何值都會導致執行緒安全問題。抱歉,這是徹頭徹尾的錯誤資訊。併發消費者設定決定了用於接收訊息的執行緒數,而 'maxConcurrentConsumers' 決定了在負載過重時消費者池可以增長到什麼程度(當需求減少時,消費者數量會下降到設定為 'concurrentConsumers' 的值)。只要訊息偵聽器本身是執行緒安全的,就可以增加此值來控制吞吐量。我個人永遠不會將訊息偵聽器用於任何除了委託給“服務”之外的目的,因此即使在(非常不可能)的情況下,我也想讓一個有狀態物件最終處理訊息的內容,那麼該目標物件將配置一個池目標源。訊息偵聽器本身將始終是執行緒安全的,因此 'concurrentConsumers' 和 'maxConcurrentConsumers' 的值可用於管理吞吐量。

這個話題引出了另一個觀點。全面的比較將在此揭示 Spring 的另一個優點——即 Spring 的偵聽器介面卡。介面卡提供從 JMS 訊息到簡單 Java 載荷的自動轉換,然後委託給任何 Spring 管理的物件來處理該載荷。例如,在上面的配置中,“logger”和“tradeService”偵聽器甚至不需要實現 MessageListener 介面。如果它們沒有,那麼 Spring 會自動用一個介面卡包裝這些 POJO,該介面卡會轉換訊息並確定呼叫哪個方法。它甚至會將返回值(如果有)轉換為 JMS 回覆訊息,並自動回覆到傳入訊息的 'reply-to' 屬性指定的目的地。相同的行為從頭開始實現非常困難,因為 JMS MessageListener 處理方法有一個 'void' 返回型別。


public interface MessageListener {
   void onMessage(Message message);
}

8. Spring 對 AspectJ 表示式語言的使用功能強大但晦澀難懂。

EJB 3 僅限於 @AroundInvoke,並且示例透過一些簡單的審計(透過攔截)展示了這一點。Spring 的示例展示了 @Before 通知,因為審計只需要在方法執行*之前*發生(而不是環繞)。我讚賞示例強調了在 EJB 3 端呼叫 context.proceed() 的必要性,而 AspectJ @Before 通知則更簡單。然而,我感到失望的是,一些與會者似乎認為 AspectJ 模型僅限於 @Before,因此 EJB 3 的 @AroundInvoke 功能更強大。為了全面起見,我本應在 Spring 端包含一個 @Around 通知示例——以澄清它得到了支援,但它並不總是必需的。

EJB 3 攔截模型最大的限制是,應該被攔截的方法(或類)被直接註解,而 AOP 的基本目標之一是非侵入性——最終甚至支援對您無法控制的程式碼的通知。考慮到這個目標,AspectJ 表示式語言可以說是儘可能清晰和簡潔的,同時支援可能應用通知的所有構造。雖然它一開始可能看起來晦澀難懂,但它相當容易學習。例如,它在概念上類似於正則表示式,但在範圍上比正則表示式小得多(有關表示式語言的詳細資訊,請參閱 AspectJ 主頁)。

9. Spring 的工具支援一直比較稀疏。

關於這一點,我首先要指出,使用 Spring 有助於縮短開發/測試周期,因此開發人員的大部分時間都花在 IDE 中,*而不是*部署到應用伺服器,而 IDE 是很棒的工具。基於 Eclipse 的 Spring IDE 是 Spring 專案開發輔助的一個非常有價值的附加元件,IntelliJ 也在 IDEA 中提供了 Spring 支援。至於部署工具,基於 Spring 的應用程式當然可以部署到任何容器中,並且由於 Spring 可以利用底層資源(DataSource、TransactionManager、JMS ConnectionFactory 等),這些資源與任何部署在特定容器中的應用程式一樣進行管理。Spring 將任何物件暴露為 JMX MBean 的能力(包括對上述 @ManagedResource 的支援)及其對 JMX 通知/偵聽器的支援對於自定義監控和管理需求非常強大。

話雖如此,Spring 顯然可以受益於更多的工具支援。這就是為什麼最近成立了“Spring Tool Suite”來整合 Spring IDE、AJDT、AspectJ 和 Mylyn,並進一步發展。有關更多資訊,請參閱此處提供的文章和連結:此處

10. EJB 作為一種標準,您可以從一個供應商遷移到另一個供應商。使用 Spring,您仍然需要移植元資料。

眾所周知,可移植性“說起來容易做起來難”。雖然 EJB 3 在這方面可能比 EJB 2 更容易(配置的冗長性較低),但事實仍然是應用伺服器提供不同的功能,因此有不同的配置選項。顯然,如果您部署到應用伺服器,您*應該*利用某些特定功能。原始陳述的問題在於,它暗示了 Spring 在應用程式配置級別上具有固有的可移植性較低。恰恰相反,任何從一個應用伺服器遷移到另一個應用伺服器的 Spring 使用者都會同意,Spring 在這方面提供了大量的抽象。在上面的第 4 點中,我提到 Spring 的 JtaTransactionManager 在任何 Java EE 應用伺服器中都使用自動檢測,並且同樣適用於 MBean 伺服器和 RMI 登錄檔。沿著這些相同的思路,在使用 JPA 時,Spring 會檢測 persistence.xml 並相應地建立 EntityManagerFactory。在所有這些情況下,Spring 的元資料——無論是註解(@PersistenceContext、@Transactional、@Resource 等)還是 XML('jee:jndi-lookup' 等)——都與任何 EJB 3 應用程式一樣可移植。

即使超越了典型 EJB 3 應用程式的能力,最小的配置更改也能提供顯著的便利。在這方面,Spring 實際上*促進*了向更多樣化環境的可移植性:Tomcat、Jetty、獨立環境、Eclipse、IDEA 等。我的建議是,獲取 Spring 發行版的 PetClinic 示例應用程式,並嘗試將 WAR 檔案構建並部署到多個應用伺服器中。然後,您會注意到它可以輕鬆地部署到 Tomcat 中,並且一旦您想在應用程式支援的不同資料訪問策略(JDBC、Hibernate 和 JPA)之間切換,其可移植性程度實際上遠遠超過了 EJB 3 應用程式。仔細檢視這些不同版本(位於 'samples/petclinic/war/WEB-INF' 目錄中)的不同配置檔案。特別是隨著 Spring 2.5 的加入,配置非常簡潔。請注意,在這些不同版本之間切換所需的唯一更改是在引導 Spring 上下文的 web.xml 中的一行。如果您想使用容器管理的 DataSource,可以使用單行 'jee:jndi-lookup' 元素。否則,有一個 bean 定義用於使用獨立的 DataSource,實際的資料庫屬性被外部化到 jdbc.properties 中。

結論

嗯,看來我的話比我想的要多 :)。我的意圖是提供一些來自 Spring 視角的客觀澄清,我希望讀者能夠清楚地看到這一點。我知道這次演講在 JUG 和會議上非常受歡迎,我認為這是一個重要的討論。今天,許多 Java 開發人員被海量選項所壓倒,重要的是他們擁有做出明智決策所需的所有事實。雖然我在這裡沒有強調這一點(而且您可能也不希望我再說下去),但本次演講和書籍(《EJB 3 in Action》)的一個觀點是,Spring 和 EJB 3 無需互斥。Spring 可以在 EJB 應用程式中使用,EJBs 可以從 Spring 應用程式訪問,並且 Spring 現在支援大多數相同的註解:@Resource、@PersistenceContext、@PostConstruct、@PreDestroy、@EJB 和 @WebServiceRef。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有