什么是 EJB?

EJB(Enterprise JavaBeans)是 Java EE(现更名为 Jakarta EE)平台的核心组件模型之一。它是一个服务端组件架构,用于开发分布式的、事务安全的、可扩展的企业级 Java 应用程序。

EJB 的设计初衷是将业务逻辑从客户端剥离,集中到服务器端执行,从而简化客户端的开发,并让服务器统一管理事务、安全、并发等企业级特性。

EJB架构概览

在 Java 生态中,EJB 曾经是企业开发的代名词。从 2000 年代初到 2010 年前后,几乎所有大型 Java 项目都在使用 EJB。虽然后来被 Spring 框架大幅取代,但理解 EJB 仍然非常重要——它是理解 Java 企业级开发演进的必经之路。


EJB 的实现原理:RMI 远程方法调用

EJB 是运行在独立服务器上的组件,客户端是通过网络对 EJB 对象进行调用的。在 Java 中,能够实现远程对象调用的技术是 RMI(Remote Method Invocation),而 EJB 的技术基础正是 RMI。

RMI 的核心原理

RMI 通过 Java 对象的序列化机制实现分布式计算。它的工作流程如下:

  1. 客户端调用远程对象的本地代理(Stub)
  2. Stub 将方法名、参数等进行序列化
  3. 序列化后的数据通过网络传输到服务器端
  4. 服务器端的 Skeleton 反序列化请求数据
  5. 调用实际的业务对象执行方法
  6. 将执行结果序列化后返回给客户端
1
2
3
4
5
6
7
8
9
10
11
┌─────────────┐         网络传输         ┌─────────────┐
│ 客户端 │ ── 序列化请求 ──> │ 服务端 │
│ │ │ │
│ Stub │ <── 反序列化结果 ── │ Skeleton │
│ (本地代理) │ │ (服务骨架) │
└─────────────┘ └─────────────┘

┌─────┴─────┐
│ 实际对象 │
│ (业务逻辑) │
└───────────┘

EJB 如何利用 RMI

EJB 将原来需要写在客户端的代码转移到了服务器端,并依靠 RMI 进行通信。客户端只需要通过 JNDI(Java Naming and Directory Interface)查找 EJB 的远程引用,就可以像调用本地方法一样调用远程业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import javax.naming.InitialContext;
import javax.naming.Context;

// 1. 初始化 JNDI 上下文
Context ctx = new InitialContext();

// 2. 通过 JNDI 查找远程 EJB
Object obj = ctx.lookup("java:global/MyApp/CalculatorBean");

// 3. 窄化为远程接口
CalculatorRemote calculator = (CalculatorRemote)
javax.rmi.PortableRemoteObject.narrow(obj, CalculatorRemote.class);

// 4. 像调用本地方法一样调用远程方法
int result = calculator.add(10, 20);
System.out.println("结果: " + result); // 输出: 结果: 30

RMI 的底层机制

RMI 的远程对象通信涉及两个关键概念:

  • 序列化(Serialization):将 Java 对象转换为字节流,以便通过网络传输或持久化存储
  • 反序列化(Deserialization):将字节流还原为 Java 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.*;

// 一个远程对象必须实现 Serializable 接口
public class RemoteData implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int value;

public RemoteData(String name, int value) {
this.name = name;
this.value = value;
}
}

EJB 的三种 Bean 类型

EJB 规范定义了三种核心 Bean 类型,分别对应不同的业务场景。

1. Session Bean(会话 Bean)

Session Bean 用于封装业务逻辑,是最常用的 EJB 类型。它分为两种:

1.1 Stateless Session Bean(无状态会话 Bean)

容器维护一个 Bean 实例池,客户端每次调用可能分配到不同的实例。适合无状态的业务逻辑(如计算服务、查询服务)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import javax.ejb.Stateless;

@Stateless(name = "CalculatorBean")
public class CalculatorBean implements CalculatorRemote {

@Override
public int add(int a, int b) {
return a + b;
}

@Override
public int multiply(int a, int b) {
return a * b;
}
}

// 远程接口定义
import javax.ejb.Remote;

@Remote
public interface CalculatorRemote {
int add(int a, int b);
int multiply(int a, int b);
}

1.2 Stateful Session Bean(有状态会话 Bean)

容器为每个客户端维护一个专属的 Bean 实例,可以在多次调用之间保持状态。适合购物车、向导式流程等场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import javax.ejb.Stateful;
import java.util.ArrayList;
import java.util.List;

@Stateful
public class ShoppingCartBean implements ShoppingCartRemote {

private List<String> items = new ArrayList<>();

@Override
public void addItem(String item) {
items.add(item);
}

@Override
public List<String> getItems() {
return new ArrayList<>(items);
}

@Override
public double getTotal() {
// 计算总价的逻辑
return items.size() * 10.0;
}

@Remove // 标记此方法调用后销毁 Bean 实例
@Override
public void checkout() {
System.out.println("结账,共 " + items.size() + " 件商品");
}
}

2. Entity Bean(实体 Bean)

Entity Bean 用于持久化数据到数据库,相当于数据库表在 Java 中的对象映射。在 EJB 2.x 时代使用 CMP(容器管理持久化),后来被 JPA(Java Persistence API)取代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// EJB 2.x 时代的 Entity Bean(已过时,仅作了解)
import javax.ejb.EntityBean;

public class UserBean implements EntityBean {
private EntityContext ctx;
private String id;
private String name;
private String email;

// CMP 方式:不需要写 JDBC 代码,容器自动处理
public abstract String getId();
public abstract void setId(String id);
public abstract String getName();
public abstract void setName(String name);
public abstract String getEmail();
public abstract void setEmail(String email);

// 生命周期回调
public void ejbCreate(String id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
}

3. Message-Driven Bean(消息驱动 Bean)

MDP(Message-Driven Bean)用于异步处理消息。它监听 JMS(Java Message Service)消息队列,收到消息后自动触发处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;

@MessageDriven(
activationConfig = {
@ActivationConfigProperty(
propertyName = "destinationType",
propertyValue = "javax.jms.Queue"
),
@ActivationConfigProperty(
propertyName = "destination",
propertyValue = "queue/OrderQueue"
)
}
)
public class OrderProcessorBean implements MessageListener {

@Override
public void onMessage(Message message) {
try {
if (message instanceof TextMessage) {
String orderData = ((TextMessage) message).getText();
System.out.println("收到订单消息: " + orderData);

// 处理订单逻辑
processOrder(orderData);
}
} catch (Exception e) {
System.err.println("处理订单失败: " + e.getMessage());
}
}

private void processOrder(String orderData) {
// 实际的订单处理逻辑
System.out.println("正在处理订单...");
}
}

EJB三种Bean类型


服务器集群与分布式架构

EJB 的另一个重要特性是服务器集群。通过 RMI 通信,可以将不同功能模块部署到不同的服务器上,形成一个完整的分布式系统。

集群架构示意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
                ┌──────────┐
│ 负载均衡 │
│ (F5/Nginx)│
└────┬─────┘

┌───────────────┼───────────────┐
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ Web层 │ │ Web层 │ │ Web层 │
│ (Tomcat)│ │ (Tomcat)│ │ (Tomcat)│
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└───────────────┼───────────────┘

┌──────────┴──────────┐
│ EJB 容器层 │
│ ┌───────────────┐ │
│ │ Session Bean │ │
│ │ Entity Bean │ │
│ │ Message Bean │ │
│ └───────────────┘ │
└──────────┬──────────┘

┌─────┴─────┐
│ 数据库层 │
│ (Oracle) │
└───────────┘

集群的关键概念

概念 说明
负载均衡 将请求均匀分配到多个服务器
会话复制 在多个服务器之间同步用户会话状态
故障转移 当某台服务器宕机时,自动将请求转移到其他服务器
水平扩展 通过增加服务器数量来提升系统处理能力

EJB 与 Spring 框架的对比

Spring 框架的崛起,很大程度上是因为它提供了一种比 EJB 更轻量的企业级开发方式。

对比分析

对比维度 EJB (2.x/3.x) Spring Framework
编程模型 必须实现接口,继承特定基类 普通 Java 对象(POJO),无需继承
配置方式 早期需要大量 XML,3.x 用注解 灵活的 XML + 注解 + Java Config
容器依赖 必须运行在 EJB 容器(JBoss/WebLogic) 可以在任何 Servlet 容器中运行
事务管理 容器管理事务(CMT),侵入性强 声明式事务(@Transactional),非侵入
学习曲线 陡峭,概念繁多 平缓,渐进式学习
测试难度 困难,需要启动 EJB 容器 简单,支持独立单元测试
性能 早期较差,3.x 改善明显 轻量,性能好

Spring 如何实现类似 EJB 的功能

Spring 通过依赖注入(DI)和面向切面编程(AOP),实现了 EJB 的大部分企业级特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Spring 中的 Service 层(替代 EJB Session Bean)
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class OrderService {

@Autowired
private OrderRepository orderRepository;

public Order createOrder(Order order) {
// 事务由 @Transactional 自动管理
return orderRepository.save(order);
}
}

// Spring 中的消息监听(替代 EJB Message-Driven Bean)
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

@Component
public class OrderMessageListener {

@JmsListener(destination = "OrderQueue")
public void processOrder(String orderData) {
System.out.println("收到订单: " + orderData);
}
}

// Spring Data JPA 中的 Repository(替代 EJB Entity Bean)
import org.springframework.data.jpa.repository.JpaRepository;

public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByUserId(Long userId);
}

现代微服务视角下的 EJB

站在今天(2024+)的视角来看 EJB,它有着深刻的历史意义,但也暴露出了明显的时代局限性。

EJB 的历史贡献

  1. 最早的企业级组件模型:定义了事务、安全、并发等企业级特性的标准处理方式
  2. 推动了 Java 企业化:让 Java 从桌面语言变成了企业开发的主力语言
  3. 催生了 Spring:EJB 的复杂性直接催生了 Spring 框架的诞生

现代替代方案

在今天,如果要构建分布式企业系统,通常会采用以下技术栈:

需求 EJB 时代 现代方案
远程调用 RMI / EJB Remote gRPC / REST API
事务管理 CMT Spring @Transactional / Seata
消息处理 JMS + MDB Kafka / RabbitMQ
依赖注入 EJB @Inject Spring @Autowired / CDI
持久化 Entity Bean (CMP) JPA / MyBatis
服务编排 EJB 容器 Spring Cloud / Kubernetes

微服务架构下的思考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│ 订单服务 │ │ 用户服务 │ │ 支付服务 │
│ (Spring Boot)│ │ (Spring Boot)│ │ (Spring Boot)│
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└──────────────────┼──────────────────┘

┌─────┴─────┐
│ API 网关 │
│ (Gateway) │
└─────┬─────┘

┌─────┴─────┐
│ 客户端 │
│ (浏览器) │
└───────────┘

在微服务架构中,每个服务都是独立的、可独立部署的单元。这与 EJB 时代的”单体 EJB 容器”有着本质区别。微服务强调的是服务的自治性去中心化治理,而不是将所有业务逻辑集中到一个厚重的容器中。

现代微服务架构


踩坑经验

经验一:EJB 2.x 时代的”地狱配置”

EJB 2.x 需要为每个 Bean 编写 Home 接口、Remote 接口、Bean 实现类,还要在 ejb-jar.xml 中配置大量 XML。一个最简单的 Bean 可能需要 4 个文件。EJB 3.0 引入注解后大幅简化,但历史包袱已经太重。

经验二:远程调用的性能陷阱

RMI 的每次调用都需要序列化和网络传输。如果把细粒度的方法(如 getUser().getName())做成远程调用,会产生大量的网络开销。正确的做法是粗粒度接口设计,一次性返回足够的数据。

1
2
3
4
5
6
7
8
9
10
11
// 错误:细粒度调用(每次都是远程调用)
UserRemote user = ejb.findUser(id);
String name = user.getName(); // 远程调用
String email = user.getEmail(); // 远程调用
String phone = user.getPhone(); // 远程调用

// 正确:粗粒度调用(一次获取全部需要的数据)
UserDTO userDTO = ejb.getUserDetail(id);
String name = userDTO.getName(); // 本地调用
String email = userDTO.getEmail(); // 本地调用
String phone = userDTO.getPhone(); // 本地调用

经验三:类加载器问题

在集群环境中,不同服务器的类加载路径可能不一致,导致 RMI 反序列化时抛出 ClassNotFoundException。解决方案是确保所有节点的 classpath 和依赖版本完全一致。


总结

EJB 作为 Java 企业级开发的先驱,它的 RMI 基础、三种 Bean 类型、事务管理和集群能力,奠定了现代企业级框架的基石。虽然今天大多数项目已经转向 Spring Boot + 微服务架构,但理解 EJB 的原理,能帮助我们更好地理解 Spring 的 @Transactional@Service@JmsListener 等注解背后的设计思想。

技术永远在演进,但底层原理相通。学习 EJB,不只是学习一个框架,更是理解”企业级应用应该如何设计”的经典教材。