對 EJB 3 和 Spring 對比分析的回應

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

昨晚,我參加了新英格蘭 Java 使用者組 (NEJUG) 的一個會議,Reza Rahman 在會上展示了 EJB 3 和 Spring 的“對比分析”(EJB 3 and Spring)。Reza 是《EJB 3 實戰》(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 註解進行註解驅動的事務管理(支援與帶有預設 REQUIRED 傳播行為的 @Stateless EJB 相同的語義)。我尤其失望的是,這次比較沒有在雙方都包含 JPA(見下面的第 3 點)。Spring 2.0 還引入了完整的基於註解的 AspectJ 支援 (@Aspect, @Before, @After, @Around) 和“stereotype”註解的概念。例如,@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 支援任何型別物件的依賴注入)和 EJB 3 的主要優勢(使用註解代替 XML)。Spring 2.5 還引入了基於 @Autowired 和(可擴充套件的)@Qualifier 註解的更細粒度的註解驅動依賴注入模型。Spring 2.5 還將“stereotype”註解擴充套件到包括 @Service 和 @Controller。每個 stereotype 註解都透過將其作為元註解應用來擴充套件泛型的 @Component 註解。透過應用相同的技術,@Component 註解為使用者定義的 stereotype 提供了一個擴充套件點。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 允許您支援多種部署環境,但需要更多配置。

這一點實際上被呈現為 Spring 的一個優點,但著重強調了配置開銷。事實是,任何認真對待測試和敏捷開發的專案都需要支援“多種部署環境”。換句話說,這個特定主題經常被扭曲,彷彿它只適用於多種生產環境。實際上,在每個開發和測試周期都需要部署到應用伺服器是敏捷性的一個主要障礙。通常,Spring 使用者會對其配置進行模組化,使得“基礎設施”配置(例如 DataSource、TransactionManager、JMS ConnectionFactory)是分開的,而動態屬性是外部化的。由於 Spring 支援根據外部化屬性替換 '${placeholders}',因此包含不同的屬性檔案通常會成為一個透明的問題。

3. EJB 使用 JPA,Spring 使用 Hibernate

我必須承認,這一點最讓我感到困擾。在對比幻燈片中,EJB 3 示例展示了透過 entityManager 進行資料訪問的 JPA,並且 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。同樣,如果您想在 ORM 過度的情況下新增一些直接 JDBC 進行資料訪問,Spring 也支援這一點 - 即使在與 Hibernate 或 JPA 資料訪問相同的事務中呼叫。

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

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


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

如果您確實想利用容器特定的功能,例如按事務隔離級別,那麼 Spring 也提供了幾個專用的實現:WebLogicJtaTransactionManagerWebSphereUowTransactionManagerOC4JJtaTransactionManager。在這些實現之間切換隻需更改這個單一的定義即可。

此外,Spring 的配置幻燈片 unnecessarily verbose。我擔心這可能也是為了強調 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 會話互動來管理 Spring 應用程式中的狀態相反。此外,Repository 配置是可插拔的,因此可以使用各種策略進行狀態的物理儲存(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' 元素上可用(如上所示),它在邏輯上只設置 1 次,但由每個內部建立的監聽器定義對應的容器例項共享。

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

好吧,這確實是當晚最奇怪的時刻。程式碼幻燈片展示了一個完全無狀態的 MessageListener 實現(本應如此!),然後配置幻燈片顯示 'maxConcurrentConsumers' 的值設定為 1。此時,有人表示將該值設定為除 1 以外的任何值都會導致執行緒安全問題。很抱歉地說,這是徹頭徹尾的誤導資訊。併發消費者設定決定了可用於接收訊息的執行緒數量,而 'maxConcurrentConsumers' 決定了消費者池在負載重的情況下可以增長到何種程度(隨著需求的減少,消費者數量會回落到設定為 'concurrentConsumers' 的值)。只要 MessageListener 本身是執行緒安全的,就可以增加此值以控制吞吐量。我個人永遠不會使用 MessageListener 來做委託給“服務”以外的事情,因此即使在極不可能的情況下,我希望有一個有狀態的物件最終處理訊息的內容,那麼該目標物件也會配置一個池化目標源。MessageListener 本身將始終是執行緒安全的,因此 '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、 standalone、Eclipse、IDEA 等等。我在這裡的建議是,下載 Spring 發行版的 PetClinic 示例應用程式,並嘗試構建 WAR 檔案並將其部署到多個應用伺服器中。然後,注意它可以同樣輕鬆地部署到 Tomcat 中,並且一旦您想在應用程式支援的不同資料訪問策略(JDBC、Hibernate 和 JPA)之間切換時,其可移植性程度實際上遠遠超過 EJB 3 應用程式。仔細檢視這些不同版本的配置檔案(位於 'samples/petclinic/war/WEB-INF' 目錄中)。特別是增加了 Spring 2.5 之後,配置極其簡潔。請注意,在這些不同版本之間切換所需進行的唯一更改是 web.xml 中的一行,其中 Spring 上下文被引導。如果您想使用容器管理的 DataSource 執行,請使用一行 'jee:jndi-lookup' 元素。否則,有一個 bean 定義用於使用 standalone 的 DataSource,並且實際的資料庫屬性已外部化到 jdbc.properties 中。

結論

看來我說得比我想象的要多 :)。我的初衷是從 Spring 的角度提供一些客觀的澄清,希望讀者能夠明白這一點。我知道這次展示在 JUG 和會議上非常受歡迎,而且我認為這是一個重要的討論。許多 Java 開發人員如今面臨著大量的選擇,獲得必要的所有事實來做出明智的決策非常重要。雖然我在這裡沒有強調(您可能也不想讓我繼續說下去),但這次展示以及《EJB 3 實戰》(EJB 3 in Action)這本書的一個觀點是,Spring 和 EJB 3 不必相互排斥。Spring 可以在 EJB 應用程式中使用,可以從 Spring 應用程式訪問 EJB,並且 Spring 現在支援大多數相同的註解:@Resource、@PersistenceContext、@PostConstruct、@PreDestroy、@EJB 和 @WebServiceRef。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

保持領先

VMware 提供培訓和認證,助您快速提升。

瞭解更多

獲取支援

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

瞭解更多

近期活動

檢視 Spring 社群的所有近期活動。

檢視全部