1. 概述

Java 生态系统在过去几十年中经历了巨大演进。在这期间,Enterprise JavaBeans(EJB)Spring 框架 不仅长期“竞争”,更在相互借鉴中共同进化。

本文将深入探讨两者的发展历程与核心差异,并通过代码示例展示 EJB 的典型用法及其在 Spring 中的等效实现。

目标很明确:✅ 帮你理清技术选型思路,避免踩坑;✅ 掌握两种技术在实际开发中的映射关系。


2. 技术发展简史

我们先快速回顾一下这两项技术的演进路径,理解它们为何走到今天这一步。

2.1 Enterprise JavaBeans(EJB)

EJB 是 Java EE(现称 Jakarta EE)规范的一个子集,最早于 1999 年发布,是 Java 服务端企业应用开发的早期尝试之一。

它的初衷是把开发者从繁琐的底层事务中解放出来 —— 比如 ✅ 并发控制、✅ 安全管理、✅ 持久化、✅ 事务处理 等,统统交给应用服务器(如 WebLogic、WebSphere、WildFly)的容器来自动处理。

但早期 EJB(尤其是 2.x 版本)配置复杂、依赖笨重,开发体验极差,性能也成瓶颈。直到 EJB 3.0 引入注解(Annotation)后,才真正变得可用。如今的 EJB 3.2 已大量借鉴 Spring 的 POJO 编程模型和依赖注入思想,变得简洁许多。

2.2 Spring 框架

就在 EJB 和 Java EE 被诟病“过于重量级”的时候,Spring 框架于 2004 年横空出世,带来一股清新之风。

它提供了轻量级的 IoC 容器,让 Java 企业应用摆脱了对庞大应用服务器的强依赖。不仅如此,Spring 还带来了 ✅ 控制反转(IoC)、✅ 面向切面编程(AOP)、✅ 对 Hibernate 的原生支持 等一系列实用特性。

凭借强大的社区支持,Spring 迅速成长为一个完整的 Java/JEE 应用开发生态。到了 Spring 5.0,甚至原生支持响应式编程(Reactive)。而 Spring Boot 的出现更是彻底改变了 Java 开发模式 —— 内嵌容器、自动配置,简单粗暴地提升了开发效率。


3. 特性对比前的准备

在进入具体功能对比之前,先明确几个基本认知。

3.1 根本区别

首先,最核心的一点:

EJB 是一套规范(Specification),而 Spring 是一个完整的框架(Framework)

这意味着:

  • EJB 需要由具体的应用服务器实现(如 GlassFish、WebSphere、WildFly)。你选择 EJB,就必须同时选择一个应用服务器。
  • EJB 理论上具备跨服务器可移植性,但前提是不能使用厂商私有扩展,否则就失去了“规范”的意义。

其次,从技术广度来看:

Spring 的能力覆盖范围更接近 Java EE 整体,而非仅对标 EJB

EJB 主要聚焦于后端业务逻辑(Session Bean、MDB),而 Spring 不仅支持后端,还涵盖了 ✅ Web 接口开发、✅ RESTful API、✅ 响应式编程、✅ 安全控制 等全方位能力。

3.2 环境准备

本文示例将使用以下技术栈:

  • EJB 示例:使用 OpenEJB 作为嵌入式容器运行。
  • Spring 示例:使用 Spring IoC 容器即可;涉及 JMS 时使用嵌入式 ActiveMQ。
  • 测试:统一使用 JUnit。

建议提前了解以下概念,以便更好理解代码:

  • Java EE Session Bean
  • Message Driven Bean (MDB)
  • Spring Bean 与 注解
  • JNDI 查找机制

4. Singleton EJB vs. Spring Component

有时候我们希望某个 Bean 在整个应用中只存在一个实例,比如用于统计网站访问量的计数器。

这种场景下,EJB 提供了 @Singleton,而 Spring 的 @Component 默认就是单例。

4.1 Singleton EJB 示例

首先定义一个远程接口(用于 JNDI 查找):

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

实现类加上 @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 容器:

@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 查找获取 EJB 实例。
  • 第二次获取的 secondCounterfirstCounter 是同一个实例,计数延续,状态共享。

4.2 Spring Singleton Bean 示例

Spring 实现更简洁,无需接口:

@Component
public class CounterBean {
    private int count = 1;
    private String name;

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

启用组件扫描:

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

初始化 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());
}

✅ 总结:
Spring 的 @Component 默认就是单例,无需额外配置,开发体验更友好。


5. Stateful EJB vs. Spring Prototype Bean

某些场景需要 Bean 保持状态,比如购物车。每次用户操作都应对应一个独立的实例。

5.1 Stateful EJB 示例

定义远程接口和实现类,使用 @Stateful

@Stateful
public class ShoppingCartEJB implements ShoppingCartEJBRemote {
    private String name;
    private List<String> shoppingCart = new ArrayList<>();

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

测试代码验证状态隔离:

@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());

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

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

    assertEquals(2, fruitCart.getItems().size());
    assertNull(fruitCart.getName()); // 未设置,状态独立
}

fruitCart 未设置 name,值为 null,说明两个实例状态完全隔离。

5.2 Spring Stateful Bean 示例

Spring 中通过 @Scope("prototype") 实现:

@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ShoppingCartBean {
   private String name;
   private List<String> shoppingCart = new ArrayList<>();

   public void addItem(String item) {
       shoppingCart.add(item);
   }
   // 其他方法同上
}

获取实例方式:

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

✅ 每次调用 getBean() 都会返回一个新实例,状态独立。


6. Stateless EJB:Spring 中没有直接对应

有些场景既不需要状态,也不关心是否单例,比如一个搜索服务。只要能返回结果,用哪个实例都无所谓。

6.1 Stateless EJB 示例

使用 @Stateless 注解,容器会维护一个实例池:

@Stateless
public class FinderEJB implements FinderEJBRemote {
    private Map<String, String> alphabet = new HashMap<>();

    public FinderEJB() {
        alphabet.put("A", "Apple");
        // 更多初始化...
    }

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

测试时可通过 @EJB 注入:

@EJB
private FinderEJBRemote alphabetFinder;

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

⚠️ 关键差异
Spring 并不提供“无状态会话 Bean”的概念。它的 @Component 虽然是单例,但通常用于无状态逻辑(如 Service 层),通过方法参数传递状态,本质上实现了类似效果。

但 Spring 没有实例池机制,所有 Bean 实例的生命周期由容器统一管理。


7. Message Driven Bean (MDB) vs. Spring JMS

MDB 用于异步消息处理,典型场景是系统间解耦通信。

7.1 MDB 示例

实现 MessageListener 接口,监听队列消息:

@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);
        }
    }
}

测试代码发送消息并验证响应:

@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());
}

7.2 Spring JMS 示例

Spring 配置 JMS 支持:

@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;
    }
}

生产者组件:

@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");
    }
}

消费者组件:

@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());
}

✅ Spring JMS 更灵活:支持同步发送/接收,也支持异步监听,编程模型更现代。


8. 总结

本文通过代码对比,系统梳理了 Spring Bean 与 EJB 在常见场景下的对应关系:

EJB 类型 Spring 等效实现 说明
@Singleton @Component(默认) 单例行为一致
@Stateful @Component + @Scope("prototype") 状态保持机制
@Stateless @Component(无状态使用) Spring 无实例池概念
@MessageDriven @JmsListener + JmsTemplate Spring JMS 更灵活

核心结论

  • EJB 是规范,Spring 是框架 —— 前者定义“做什么”,后者解决“怎么做”。
  • Spring 在开发体验、灵活性、生态整合上全面胜出,尤其是 Spring Boot 出现后,EJB 已逐渐边缘化。
  • 但 EJB 并未消亡,尤其在传统金融、电信等重型 Java EE 项目中仍有应用。
  • 两者并非互斥,Spring 支持集成 EJB,可在迁移项目中混合使用。

📌 源码已托管至 GitHub:https://github.com/baeldung/tutorials(模块:spring-ejb-modules)

技术选型没有绝对对错,关键是理解背后的设计思想。希望这篇文章能帮你少走弯路。


原始标题:Spring Bean vs. EJB - A Feature Comparison