知識庫 / Spring RSS 訂閱

Spring Bean 與 EJB – 功能對比

Jakarta EE,Spring
HongKong
4
01:02 PM · Dec 06 ,2025

1. 概述

在過去的一段時間裏,Java生態系統已經發展和壯大。在此期間,企業 JavaBean(Enterprise Java Beans,簡稱EJB)和Spring是兩種技術,它們不僅競爭,而且相互學習,形成了一種共生關係。

在本教程中,我們將探討它們的發展歷史和差異。當然,我們還將提供EJB及其在Spring世界中等效代碼示例。

2. 技術發展簡史

為了更好地理解這些技術,我們先快速回顧一下它們的發展歷程,以及它們在過去幾年中的穩步發展。

2.1 企業Java Bean

企業Java Bean規範是Java EE(或J2EE,現在稱為Jakarta EE)規範的一個子集。 其第一版本於1999年發佈,它是最早旨在簡化Java中服務器端企業應用程序開發的之一。

它承擔了Java開發人員的併發、安全、持久性、事務處理等負擔。 規範將這些以及其他常見的企業關注點傳遞給實現應用程序服務器容器,這些容器無縫地處理了它們。 然而,由於配置量較大,使用EJBs的方式有些繁瑣。 此外,它也成為了性能瓶頸。

但現在,隨着註解的出現以及Spring的激烈競爭,在最新版本的3.2中,EJBs的使用比其首發版本要簡單得多。 今天的企業Java Bean深受Spring的依賴注入和POJO的使用影響。

2.2. Spring

雖然 EJB(以及整個 Java EE)在未能滿足 Java 社區的需求方面遇到了困難,但 Spring Framework 像一股新鮮空氣般出現。它的第一個里程碑版本於 2004 年發佈,為 EJB 模式及其重量級的容器提供了一種替代方案。

得益於 Spring,Java 企業應用程序現在可以運行在更輕量級的 IOC(依賴注入)容器上。此外,它還提供了依賴反轉、AOP(面向切面編程)和 Hibernate 支持等眾多有用的功能。憑藉來自 Java 社區的巨大支持,Spring 現在已經呈指數級增長,可以被視為一個完整的 Java/JEE 應用框架。

在它的最新形態中,Spring 5.0 甚至支持響應式編程模型。另一個分支,Spring Boot,是一個徹底改變遊戲規則的方案,它具有嵌入式服務器和自動配置功能。

3. 功能比較的前置準備

在開始進行代碼示例的功能比較之前,我們先明確一些基本概念。

3.1. 兩者基本區別

首先,最根本也是顯而易見的差別在於,EJB 是一種規範,而 Spring 是一整套框架

EJB 規範被許多應用服務器,如 GlassFish、IBM WebSphere 和 JBoss/WildFly 實施。這意味着,我們選擇使用 EJB 模式進行應用程序的後端開發,遠遠不夠。我們還需要選擇使用哪個應用服務器。

理論上,企業 JavaBean可以在許多應用服務器上運行,但前提是我們不應使用任何供應商特定的擴展,以保持互操作性。

其次,Spring 作為一種技術,與 EJB 在其廣泛的特性組合方面更接近 Java EE。而 EJB 僅指定後端操作,而 Spring,如 Java EE,也支持 UI 開發、RESTful API 和響應式編程等。

3.2. 有用信息

在接下來的部分中,我們將比較這兩種技術,並提供一些實際示例。由於 EJB 的特性是 Spring 生態系統中的一個子集,我們將根據它們的類型進行比較,並找到相應的 Spring 等價物。

為了更好地理解示例,請先了解 Java EE session bean、message driven bean、Spring bean 和 Spring bean 註解。

我們將使用 OpenJB 作為嵌入式容器來運行 EJB 示例。對於大多數 Spring 示例的運行,其 IOC 容器就足夠了;對於 Spring JMS,我們需要一個嵌入式 ApacheMQ 代理服務器。

我們將使用 JUnit 測試所有示例。

4. 單例 EJB 等同於 Spring 組件

有時我們需要容器僅創建一個 Bean 的實例。例如,假設我們需要一個 Bean 來統計我們 Web 應用程序的訪問者數量。這個 Bean 只需要在應用程序啓動期間創建一次

讓我們看看如何使用單例會話 EJB 和 Spring 組件 來實現這一點。

4.1. 單例 EJB 示例

我們需要一個接口來指定我們的 EJB 具備遠程處理的能力。

@Remote
public interface CounterEJBRemote {    
    int count();
    String getName();
    void setName(String name);
}

下一步是定義一個帶有註解 javax.ejb.Singleton 的實現類,瞧,我們的單例就完成了:

@Singleton
public class CounterEJB implements CounterEJBRemote {
    private int count = 1;
    private String name;

    public int count() {
        return count++;
    }
    
    // getter and setter for name
}

在我們可以測試單例(或任何其他 EJB 代碼示例)之前,我們需要初始化 ejbContainer 並獲取 context

@BeforeClass
public void initializeContext() throws NamingException {
    ejbContainer = EJBContainer.createEJBContainer();
    context = ejbContainer.getContext();
    context.bind("inject", this);
}

現在讓我們來看一下測試:

@Test
public void givenSingletonBean_whenCounterInvoked_thenCountIsIncremented() throws NamingException {

    int count = 0;
    CounterEJBRemote firstCounter = (CounterEJBRemote) context.lookup("java:global/ejb-beans/CounterEJB");
    firstCounter.setName("first");
        
    for (int i = 0; i < 10; i++) {
        count = firstCounter.count();
    }
        
    assertEquals(10, count);
    assertEquals("first", firstCounter.getName());

    CounterEJBRemote secondCounter = (CounterEJBRemote) context.lookup("java:global/ejb-beans/CounterEJB");

    int count2 = 0;
    for (int i = 0; i < 10; i++) {
        count2 = secondCounter.count();
    }

    assertEquals(20, count2);
    assertEquals("first", secondCounter.getName());
}

請注意以上示例中的幾點:

  • 我們使用 JNDI 查找從容器中獲取 counterEJB
  • count2 從 singleton 離開的點開始累加,累加到 20
  • secondCounter 保留了我們為 firstCounter 設置的名字

最後兩點表明了 singleton 的重要性。由於每次查找時都使用相同的 bean 實例,因此總計數為 20,一個的值在另一箇中保持不變。

4.2. 使用 Spring 組件的單例 Bean 示例

可以使用 Spring 組件獲得相同的功能。

我們不需要在這裏實現任何接口。相反,我們將添加 @Component 註解:

@Component
public class CounterBean {
    // same content as in the EJB
}

事實上,在 Spring 中組件默認是單例的。

我們還需要配置 Spring 掃描組件:

@Configuration
@ComponentScan(basePackages = "com.baeldung.ejbspringcomparison.spring")
public class ApplicationConfig {}

類似於我們初始化 EJB 上下文的方式,現在我們將設置 Spring 上下文:

@BeforeClass
public static void init() {
    context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
}

現在讓我們來看看我們的 組件 在實際應用中的效果:

@Test
public void whenCounterInvoked_thenCountIsIncremented() throws NamingException {    
    CounterBean firstCounter = context.getBean(CounterBean.class);
    firstCounter.setName("first");
    int count = 0;
    for (int i = 0; i < 10; i++) {
        count = firstCounter.count();
    }

    assertEquals(10, count);
    assertEquals("first", firstCounter.getName());

    CounterBean secondCounter = context.getBean(CounterBean.class);
    int count2 = 0;
    for (int i = 0; i < 10; i++) {
        count2 = secondCounter.count();
    }

    assertEquals(20, count2);
    assertEquals("first", secondCounter.getName());
}

我們可以看到,與EJB相比,我們獲取Bean的方式是使用Spring容器上下文,而不是JNDI查找。

5. 狀態感知 EJB 等價於 Spring 組件,具有 原型 作用域

在某些情況下,例如構建購物車的場景中,我們需要我們的 Bean 在方法調用之間記住其狀態

在這種情況下,我們需要容器為每次調用生成一個單獨的 Bean 並保存狀態。 讓我們看看如何使用我們所涉及的技術來實現這一點。

5.1. 狀態型 EJB 示例

類似於我們的單例 EJB 示例,我們需要一個 <em >javax.ejb.Remote</em> 接口及其實現。 這一次,它將被註解為 <em >javax.ejb.Stateful</em>

@Stateful
public class ShoppingCartEJB implements ShoppingCartEJBRemote {
    private String name;
    private List<String> shoppingCart;

    public void addItem(String item) {
        shoppingCart.add(item);
    }
    // constructor, getters and setters
}

讓我們編寫一個簡單的測試,設置一個 name 並向 bathingCart 添加項目。 我們將檢查其大小並驗證名稱:

@Test
public void givenStatefulBean_whenBathingCartWithThreeItemsAdded_thenItemsSizeIsThree()
  throws NamingException {
    ShoppingCartEJBRemote bathingCart = (ShoppingCartEJBRemote) context.lookup(
      "java:global/ejb-beans/ShoppingCartEJB");

    bathingCart.setName("bathingCart");
    bathingCart.addItem("soap");
    bathingCart.addItem("shampoo");
    bathingCart.addItem("oil");

    assertEquals(3, bathingCart.getItems().size());
    assertEquals("bathingCart", bathingCart.getName());
}

現在,為了證明該 Bean 確實能在不同實例之間保持狀態,我們向此測試中添加另一個 shoppingCartEJB:

ShoppingCartEJBRemote fruitCart = 
  (ShoppingCartEJBRemote) context.lookup("java:global/ejb-beans/ShoppingCartEJB");

fruitCart.addItem("apples");
fruitCart.addItem("oranges");

assertEquals(2, fruitCart.getItems().size());
assertNull(fruitCart.getName());

此處未設置 name 屬性,因此其值為 null。 回顧一下單例測試,可知在某個實例中設置的 name 屬性在另一個實例中得以保留。 這表明我們從 Bean 池中獲得了不同的 ShoppingCartEJB 實例,這些實例具有不同的狀態。

5.2. 使用狀態的 Spring Bean 示例

為了在 Spring 中獲得相同效果,我們需要一個具有原型作用域的 Component

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ShoppingCartBean {
   // same contents as in the EJB
}

這就是全部,只有註釋有所不同——其餘代碼保持不變。

要測試我們的 Stateful bean,我們可以使用與 EJB 描述相同的測試。唯一的區別在於我們如何從容器中獲取 bean。

ShoppingCartBean bathingCart = context.getBean(ShoppingCartBean.class);

6. 無狀態 EJB != Spring 中的任何內容

例如,在搜索 API 中,我們既不關心 Bean 的實例狀態,也不關心它是否為單例。我們只需要搜索結果,無論結果來自哪個 Bean 實例,對我們來説都一樣。

6.1. 無狀態 EJB 示例

對於此類場景,EJB 具有無狀態變體。 容器維護一個 Bean 實例池,並將其中的任意一個返回給調用方法

定義方式與其它 EJB 類型相同,具有遠程接口,並使用 javax.ejb.Stateless 註解進行實現:

@Stateless
public class FinderEJB implements FinderEJBRemote {

    private Map<String, String> alphabet;

    public FinderEJB() {
        alphabet = new HashMap<String, String>();
        alphabet.put("A", "Apple");
        // add more values in map here
    }

    public String search(String keyword) {
        return alphabet.get(keyword);
    }
}

讓我們添加另一個簡單的測試,以驗證其效果:

@Test
public void givenStatelessBean_whenSearchForA_thenApple() throws NamingException {
    assertEquals("Apple", alphabetFinder.search("A"));        
}

在上述示例中,alphabetFinder 通過使用註解 javax.ejb.EJB 作為測試類中的一個字段進行注入:

@EJB
private FinderEJBRemote alphabetFinder;

無狀態 EJB 的核心思想是,通過擁有類似 Bean 的實例池來提高性能。

然而,Spring 不遵循這一理念,僅提供單例作為無狀態的實現。

7. 基於消息的 Bean == Spring JMS

所有之前討論的會話 Bean 都是會話 Bean。另一種類型是基於消息的 Bean。正如其名稱所示,它們通常用於兩個系統之間異步通信

7.1. MDB 示例

要創建消息驅動的企業 Java Bean,我們需要實現 javax.jms.MessageListener 接口,該接口定義了其 onMessage 方法,並將類註釋為 javax.ejb.MessageDriven

@MessageDriven(activationConfig = { 
  @ActivationConfigProperty(propertyName = "destination", propertyValue = "myQueue"), 
  @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue") 
})
public class RecieverMDB implements MessageListener {

    @Resource
    private ConnectionFactory connectionFactory;

    @Resource(name = "ackQueue")
    private Queue ackQueue;

    public void onMessage(Message message) {
        try {
            TextMessage textMessage = (TextMessage) message;
            String producerPing = textMessage.getText();

            if (producerPing.equals("marco")) {
                acknowledge("polo");
            }
        } catch (JMSException e) {
            throw new IllegalStateException(e);
        }
    }
}

請注意,我們還提供了 MDB 的一些配置方案:

      • destinationType 設置為 Queue
      • myQueue 設置為 destination 隊列名稱,該隊列監聽着我們的 Bean

在這個例子中,我們的 接收器還會產生一個確認,從某種意義上説,它本身就是一個發送者。它將消息發送到一個名為 ackQueue 的隊列中。

現在讓我們通過一個測試來觀察它的運行效果:

@Test
public void givenMDB_whenMessageSent_thenAcknowledgementReceived()
  throws InterruptedException, JMSException, NamingException {
    Connection connection = connectionFactory.createConnection();
    connection.start();
    Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
    MessageProducer producer = session.createProducer(myQueue);
    producer.send(session.createTextMessage("marco"));
    MessageConsumer response = session.createConsumer(ackQueue);

    assertEquals("polo", ((TextMessage) response.receive(1000)).getText());
}

我們向 myQueue 發送了一條消息,該消息被我們的 @MessageDriven 註解的 POJO 接收到。該 POJO 隨後發送了確認,而我們的測試接收到了響應,作為 MessageConsumer

7.2. Spring JMS 示例

現在,我們將使用 Spring 執行相同的操作。

首先,我們需要為此目的添加一些配置。我們需要用 ApplicationConfig 類(之前創建的)註解為 @EnableJms,並添加一些 Bean 以設置 JmsListenerContainerFactoryJmsTemplate

@EnableJms
public class ApplicationConfig {

    @Bean
    public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
        DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory());
        return factory;
    }

    @Bean
    public ConnectionFactory connectionFactory() {
        return new ActiveMQConnectionFactory("tcp://localhost:61616");
    }

    @Bean
    public JmsTemplate jmsTemplate() {
        JmsTemplate template = new JmsTemplate(connectionFactory());
        template.setConnectionFactory(connectionFactory());
        return template;
    }
}

接下來,我們需要一個 Producer – 一個簡單的 Spring Component – 它將向 myQueue 發送消息,並從 ackQueue 接收確認:

@Component
public class Producer {
    @Autowired
    private JmsTemplate jmsTemplate;

    public void sendMessageToDefaultDestination(final String message) {
        jmsTemplate.convertAndSend("myQueue", message);
    }

    public String receiveAck() {
        return (String) jmsTemplate.receiveAndConvert("ackQueue");
    }
}

然後,我們有一個 接收器組件,該組件的方法被標註為 @JmsListener,用於異步接收來自 myQueue 的消息:

@Component
public class Receiver {
    @Autowired
    private JmsTemplate jmsTemplate;

    @JmsListener(destination = "myQueue")
    public void receiveMessage(String msg) {
        sendAck();
    }

    private void sendAck() {
        jmsTemplate.convertAndSend("ackQueue", "polo");
    }
}

它還作為接收確認消息的發送者,在 中執行。

按照我們的慣例,我們來通過測試驗證一下:

@Test
public void givenJMSBean_whenMessageSent_thenAcknowledgementReceived() throws NamingException {
    Producer producer = context.getBean(Producer.class);
    producer.sendMessageToDefaultDestination("marco");

    assertEquals("polo", producer.receiveAck());
}

在本測試中,我們向 myQueue 發送了 marco,並收到 ackQueuepolo 作為確認,與我們使用 EJB 時所做的一致。

需要注意的是,Spring JMS 可以同步和異步地發送/接收消息

8. 結論

在本教程中,我們對 Spring 和 Enterprise Java Beans 進行了 一對一比較。我們瞭解了它們的歷史和基本差異。

隨後,我們通過簡單的示例演示了 Spring Bean 和 EJBs 的比較。 毫無疑問,這只是對這些技術所能達成的範圍的初步探索,還有很多值得進一步研究的地方

此外,這些技術可能存在競爭關係,但這並不意味着它們不能共存。 我們很容易將 EJBs 集成到 Spring 框架中。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.