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 实例。
- 第二次获取的
secondCounter
与firstCounter
是同一个实例,计数延续,状态共享。
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)
技术选型没有绝对对错,关键是理解背后的设计思想。希望这篇文章能帮你少走弯路。