背景
本文是《JavaEE 后端从小白到大神》修仙系列第五篇,正式进入JavaEE后端世界。若想详细学习请点击首篇博文,我们开始吧。
第五篇:EJB(企业级组件)深入理解
- EJB 架构与容器;
- 会话 Bean(Session Bean):Stateless、Stateful、Singleton;
- 消息驱动 Bean(Message Driven Bean);
- 事务控制(CMT、BMT);
- 安全机制(Declarative Security);
- EJB 与 CDI 的关系;
- 现代 EJB 的轻量化演进。
一、EJB 是什么
1. 定义与定位
EJB(Enterprise JavaBeans,企业级 JavaBean)是 Jakarta EE(原 Java EE)规范定义的服务器端组件模型,用于构建可伸缩、多用户、事务安全的企业级分布式应用程序。
一句话总结:EJB = 运行在服务器上的业务逻辑组件,容器自动帮你管理事务、安全、并发、生命周期等底层复杂问题。
2. 为什么需要 EJB?
在 Web 应用开发中,除了处理 HTTP 请求/响应,还需要处理大量“底层基础设施”问题:
- 数据库事务如何管理?
- 多用户并发访问如何保证线程安全?
- 业务方法如何做权限控制?
- 如何实现异步消息处理?
- 如何让业务组件支持远程调用?
如果这些全部由开发者手动编写,代码会变得臃肿、重复、难以维护。EJB 的核心理念是:开发者只需专注业务逻辑,容器(Container)负责提供事务管理、安全性、生命周期管理等企业级服务。
3. EJB 容器
EJB 组件不能独立运行,必须部署在 EJB 容器中。容器是 EJB 的运行环境,负责:
- 生命周期管理:创建、初始化、销毁 EJB 实例;
- 事务管理:自动管理事务边界(CMT);
- 安全控制:基于角色的权限校验;
- 并发控制:管理多线程访问(尤其是单例 Bean);
- 资源注入:通过
@Resource、@PersistenceContext 等注入数据源、EntityManager 等;
- 远程访问:支持跨 JVM 的远程调用。
常见支持 EJB 的应用服务器:WildFly、Payara、GlassFish、WebLogic、WebSphere 等。Tomcat 是纯 Web 容器,不原生支持 EJB(可通过 OpenEJB 等扩展)。
4. EJB 的类型
EJB 规范定义了三种类型的 Enterprise Bean:
| Bean 类型 |
用途 |
核心特征 |
| 会话 Bean(Session Bean) |
封装业务逻辑 |
分为无状态、有状态、单例三种 |
| 消息驱动 Bean(Message-Driven Bean, MDB) |
异步处理 JMS 消息 |
实现 MessageListener 接口 |
实体 Bean(Entity Bean) |
数据持久化 |
已在 Java EE 6 中被 JPA 取代 |
重要提示:实体 Bean(Entity Bean)是 EJB 2.x 时代的产物,在 Java EE 6 中已被 JPA 完全取代。现代 EJB 开发中不再使用 Entity Bean,数据持久化统一使用 JPA。
二、会话 Bean(Session Bean)
会话 Bean 是 EJB 中最常用的组件类型,用于封装业务逻辑。它分为三种:无状态(Stateless)、有状态(Stateful) 和 单例(Singleton) 。
1. 无状态会话 Bean(Stateless Session Bean)
1.1 核心特征
- 不保存客户端状态:每次方法调用都是独立的,不保留任何会话信息;
- 实例池化:容器维护一个实例池,方法调用结束后实例归还池中,可被其他客户端复用;
- 无标识性:所有实例等价,客户端不关心具体由哪个实例处理;
- 最常用、性能最好。
1.2 类比理解
无状态会话 Bean 就像银行柜台:你去柜台办业务,办完就走,下一个客户可以用同一个柜台。柜台不记得你是谁,也不需要记得——每次业务都是独立的。
1.3 适用场景
- 无状态的业务操作(如查询、计算、工具方法);
- 高并发场景(实例池化,少量实例即可服务大量客户端);
- RESTful API 的后端服务;
- 不需要跨多次调用保持状态的操作。
1.4 代码示例
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
|
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
@Stateless
public class UserService {
@PersistenceContext
private EntityManager em;
// 查询用户(无状态,每次独立)
public User findUser(Long id) {
return em.find(User.class, id);
}
// 查询所有用户
public List<User> findAllUsers() {
return em.createQuery("SELECT u FROM User u", User.class)
.getResultList();
}
// 更新用户
public void updateUser(User user) {
em.merge(user);
}
}
|
关键点:
- 使用
@Stateless 注解标记;
- 无需手动管理实例,容器自动池化;
- 每次方法调用可能由不同实例处理。
2. 有状态会话 Bean(Stateful Session Bean)
2.1 核心特征
- 保持与客户端的会话状态:同一个客户端多次调用由同一个实例处理,状态在多次调用间保持;
- 与特定客户端绑定:一个实例只服务一个客户端;
- 生命周期较长:从客户端第一次调用创建,到会话超时或客户端主动销毁;
- 支持钝化/活化:长时间不使用时,容器可将实例序列化到磁盘(需实现
Serializable)。
2.2 类比理解
有状态会话 Bean 就像专属客服:你第一次打电话,客服接听后记住你的需求和情况;你第二次再打,还是同一个客服接(知道你是谁、之前聊了什么)。每个客户有自己的专属客服。
2.3 适用场景
- 需要跨多次请求保持状态的业务(如购物车、多步表单、工作流);
- 用户会话级别的数据缓存;
- 需要记住用户上下文的场景。
2.4 代码示例
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
|
import jakarta.ejb.Stateful;
import jakarta.ejb.Remove;
import java.util.ArrayList;
import java.util.List;
@Stateful
public class ShoppingCartService {
// 购物车商品列表(会话状态)
private List<String> items = new ArrayList<>();
// 添加商品
public void addItem(String item) {
items.add(item);
System.out.println("添加商品:" + item + ",当前购物车:" + items.size() + "件");
}
// 移除商品
public void removeItem(String item) {
items.remove(item);
}
// 获取购物车所有商品
public List<String> getItems() {
return new ArrayList<>(items);
}
// 结账(清理购物车)
@Remove
public void checkout() {
System.out.println("结账完成,商品:" + items);
items.clear();
// @Remove 标记的方法执行后,容器会销毁该 Stateful Bean 实例
}
}
|
关键点:
- 使用
@Stateful 注解标记;
- 实例与客户端会话绑定,一个客户端一个实例;
@Remove 标记的方法执行后,容器销毁实例,释放资源;
- 需注意:有状态 Bean 不支持 Web 服务。
3. 单例会话 Bean(Singleton Session Bean)
3.1 核心特征
- 整个应用只有一个实例:所有客户端共享同一个实例;
- 应用级生命周期:实例在应用启动时创建,应用关闭时销毁;
- 类似 ServletContext 的“应用范围”;
- 需要处理并发访问:EJB 3.2 提供了容器管理并发(CMC) 机制,简化线程安全实现。
3.2 类比理解
单例会话 Bean 就像公司公告栏:全公司只有一个,所有人都能看到和修改上面的内容。因为是共享资源,需要规则(锁)防止同时修改导致混乱。
3.3 适用场景
- 全局配置管理(如系统参数、开关);
- 全局计数器或统计(如在线人数、访问量);
- 缓存(如应用级数据缓存);
- 需要全局唯一实例的服务。
3.4 代码示例
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
41
42
43
44
45
46
|
import jakarta.ejb.Singleton;
import jakarta.ejb.Startup;
import jakarta.ejb.Lock;
import jakarta.ejb.LockType;
import jakarta.annotation.PostConstruct;
import java.util.concurrent.atomic.AtomicInteger;
@Singleton
@Startup // 应用启动时立即创建实例
public class SystemConfigService {
private AtomicInteger visitCount = new AtomicInteger(0);
private String systemName;
private boolean maintenanceMode;
@PostConstruct
public void init() {
// 应用启动时初始化
this.systemName = "My Enterprise System";
this.maintenanceMode = false;
System.out.println("系统配置服务初始化完成,系统名称:" + systemName);
}
// 读方法:允许多个线程并发读
@Lock(LockType.READ)
public String getSystemName() {
return systemName;
}
// 写方法:同一时间只允许一个线程写
@Lock(LockType.WRITE)
public void setMaintenanceMode(boolean mode) {
this.maintenanceMode = mode;
System.out.println("维护模式已切换为:" + mode);
}
@Lock(LockType.READ)
public boolean isMaintenanceMode() {
return maintenanceMode;
}
// 访问计数(原子类保证线程安全)
public int incrementAndGetVisitCount() {
return visitCount.incrementAndGet();
}
}
|
关键点:
- 使用
@Singleton 注解标记;
@Startup 让容器在应用启动时立即创建实例(而非首次访问时);
@Lock(LockType.READ):读锁,允许多个线程并发读取;
@Lock(LockType.WRITE):写锁,同一时间只允许一个线程写入;
- 容器管理并发(CMC)大大简化了单例 Bean 的线程安全编程。
4. 三种会话 Bean 对比
| 对比维度 |
无状态(Stateless) |
有状态(Stateful) |
单例(Singleton) |
| 实例数量 |
池化(多个实例) |
每个客户端一个实例 |
全局唯一一个实例 |
| 状态保持 |
不保持 |
保持客户端会话状态 |
保持应用级全局状态 |
| 生命周期 |
方法调用级 |
客户端会话级 |
应用级 |
| 并发处理 |
无需考虑(无状态) |
无需考虑(单客户端) |
需考虑(CMC) |
| 性能 |
最高(实例池复用) |
中等(状态管理开销) |
中等(并发控制开销) |
| 适用场景 |
无状态业务操作 |
购物车、多步表单 |
全局配置、缓存、计数器 |
| Web 服务支持 |
支持 |
不支持 |
支持 |
选择指南:
- 不确定选哪个?选无状态(Stateless) —— 90% 的场景都适用;
- 需要跨多次调用保持状态 → 有状态(Stateful);
- 需要全局唯一共享数据 → 单例(Singleton)。
三、消息驱动 Bean(Message-Driven Bean, MDB)
1. 什么是 MDB
消息驱动 Bean(MDB)是 EJB 中用于异步处理 JMS 消息的组件。它实现了 MessageListener 接口,当 JMS 目的地(Queue 或 Topic)有消息到达时,容器自动调用 MDB 的 onMessage() 方法处理消息。
一句话总结:MDB = 异步消息的消费者,消息来了自动处理,无需主动轮询。
2. 核心特征
- 异步处理:消息发送方和接收方解耦,发送方无需等待处理结果;
- 事件驱动:消息到达时自动触发,而非主动调用;
- 无状态:MDB 不保存客户端状态(类似无状态会话 Bean);
- 并发处理:容器可创建多个 MDB 实例并发处理消息(池化机制)。
3. 类比理解
MDB 就像自动取件柜:快递员(消息发送方)把包裹(消息)放进柜子(JMS 目的地)就走了,不需要等收件人。柜子系统(容器)会自动通知收件人(MDB)来取件处理。双方互不等待,完全解耦。
4. 适用场景
- 订单创建后的异步处理(如发送确认邮件、扣减库存);
- 日志收集和审计;
- 系统间解耦通信;
- 任务队列(如批处理任务分发);
- 事件驱动架构。
5. 代码示例
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
|
import jakarta.ejb.MessageDriven;
import jakarta.jms.Message;
import jakarta.jms.MessageListener;
import jakarta.jms.TextMessage;
import jakarta.jms.JMSException;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@MessageDriven(mappedName = "java:jboss/queue/orderQueue")
public class OrderProcessorMDB implements MessageListener {
@PersistenceContext
private EntityManager em;
@Override
public void onMessage(Message message) {
try {
// 1. 接收消息
if (message instanceof TextMessage) {
TextMessage textMsg = (TextMessage) message;
String orderJson = textMsg.getText();
System.out.println("收到订单消息:" + orderJson);
// 2. 处理业务逻辑(解析JSON、保存订单、扣减库存等)
// processOrder(orderJson);
// 3. 消息处理成功,容器自动确认(ACK)
System.out.println("订单处理完成");
}
} catch (JMSException e) {
// 异常发生时,容器可根据配置决定是否重试
System.err.println("处理消息失败:" + e.getMessage());
// 抛出 RuntimeException 会触发消息重发
throw new RuntimeException("消息处理失败", e);
}
}
}
|
关键点:
- 使用
@MessageDriven 注解标记,mappedName 指定监听的 JMS 目的地;
- 实现
MessageListener 接口,重写 onMessage() 方法;
- 消息处理成功 → 容器自动确认(ACK);
- 抛出
RuntimeException → 容器可配置重试机制;
- MDB 与 JPA 结合,可在
onMessage() 中操作数据库。
6. MDB 与普通会话 Bean 的区别
| 对比维度 |
会话 Bean(Session Bean) |
消息驱动 Bean(MDB) |
| 调用方式 |
客户端主动调用 |
消息到达时自动触发 |
| 同步/异步 |
同步(客户端等待结果) |
异步(发送方不等待) |
| 状态 |
可有状态(Stateless/Stateful) |
无状态 |
| 返回值 |
可返回结果 |
无返回值(void) |
| 适用场景 |
同步业务操作 |
异步任务、系统解耦 |
四、事务控制(CMT 与 BMT)
EJB 提供了两种事务管理方式:容器管理事务(CMT) 和 Bean 管理事务(BMT)。
1. 容器管理事务(CMT——推荐)
1.1 核心特征
- 声明式事务:通过注解或部署描述符声明事务边界;
- 容器自动管理:事务的开启、提交、回滚由容器自动完成;
- 代码最简洁:开发者无需编写任何事务相关代码;
- RuntimeException 自动回滚:方法抛出未捕获的
RuntimeException 时,容器自动回滚事务。
1.2 事务属性(Transaction Attribute)
通过 @TransactionAttribute 注解指定事务行为:
| 属性值 |
含义 |
REQUIRED |
默认。如果已有事务则加入,否则新建一个 |
REQUIRES_NEW |
始终新建事务,挂起已有事务 |
MANDATORY |
必须已有事务,否则抛出异常 |
SUPPORTS |
有事务则加入,没有则以无事务方式执行 |
NOT_SUPPORTED |
以无事务方式执行,如有事务则挂起 |
NEVER |
必须在无事务环境中执行,否则抛出异常 |
1.3 CMT 代码示例
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
41
42
|
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@Stateless
public class OrderService {
@PersistenceContext
private EntityManager em;
// 默认 REQUIRED:有事务则加入,无则新建
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void createOrder(Order order) {
em.persist(order);
// 方法正常结束 → 容器自动提交事务
}
// REQUIRES_NEW:总是新建独立事务
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void createAuditLog(String action) {
// 即使外层事务回滚,审计日志也会被独立提交
AuditLog log = new AuditLog(action);
em.persist(log);
}
// 抛出 RuntimeException → 容器自动回滚事务
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account from = em.find(Account.class, fromId);
Account to = em.find(Account.class, toId);
if (from.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足"); // 自动回滚事务
}
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
// 正常结束 → 自动提交
}
}
|
2. Bean 管理事务(BMT)
2.1 核心特征
- 编程式事务:开发者通过
UserTransaction API 手动控制事务边界;
- 更灵活:可精细控制事务的开启、提交、回滚时机;
- 代码更复杂:需要手动处理事务的每个环节;
- 适用场景有限:仅在 CMT 无法满足需求时使用(如需要细粒度事务控制)。
2.2 BMT 代码示例
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 jakarta.ejb.Stateless;
import jakarta.ejb.TransactionManagement;
import jakarta.ejb.TransactionManagementType;
import jakarta.annotation.Resource;
import jakarta.transaction.UserTransaction;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@Stateless
@TransactionManagement(TransactionManagementType.BEAN) // 声明使用 BMT
public class OrderServiceBMT {
@Resource
private UserTransaction utx; // 注入事务管理器
@PersistenceContext
private EntityManager em;
public void createOrderWithBMT(Order order) {
try {
// 1. 手动开启事务
utx.begin();
// 2. 执行业务逻辑
em.persist(order);
// 3. 手动提交事务
utx.commit();
} catch (Exception e) {
try {
// 4. 异常时手动回滚
utx.rollback();
} catch (Exception rollbackEx) {
rollbackEx.printStackTrace();
}
throw new RuntimeException("订单创建失败", e);
}
}
}
|
3. CMT vs BMT 对比
| 对比维度 |
CMT |
BMT |
| 事务控制方式 |
声明式(注解/XML) |
编程式(API) |
| 代码量 |
极少 |
较多 |
| 灵活性 |
中等(预定义属性) |
高(完全可控) |
| 出错风险 |
低(容器管理) |
高(需开发者谨慎处理) |
| 适用场景 |
绝大多数场景(推荐) |
需要细粒度事务控制的特殊场景 |
最佳实践:优先使用 CMT,仅在确实需要细粒度事务控制时才考虑 BMT。
五、安全机制(Declarative Security)
EJB 提供了声明式安全(Declarative Security) 和编程式安全(Programmatic Security) 两种方式。
1. 声明式安全(推荐)
通过注解在方法级别声明访问权限,容器自动执行权限校验。
1.1 核心注解
| 注解 |
含义 |
@RolesAllowed({"admin", "manager"}) |
仅允许指定角色访问 |
@PermitAll |
允许所有角色访问 |
@DenyAll |
禁止所有角色访问 |
1.2 代码示例
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
|
import jakarta.ejb.Stateless;
import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.DenyAll;
@Stateless
public class AdminService {
// 仅 admin 和 manager 角色可访问
@RolesAllowed({"admin", "manager"})
public void deleteUser(Long userId) {
// 删除用户逻辑
System.out.println("用户 " + userId + " 已被删除");
}
// 仅 admin 角色可访问
@RolesAllowed("admin")
public void resetSystem() {
// 系统重置逻辑
System.out.println("系统已重置");
}
// 所有角色都可访问
@PermitAll
public String getPublicInfo() {
return "公开信息";
}
// 禁止任何角色访问
@DenyAll
public void internalMethod() {
// 此方法不会被外部调用
}
}
|
2. 编程式安全
通过 EJBContext 在代码中手动检查权限。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import jakarta.ejb.Stateless;
import jakarta.ejb.EJBContext;
import jakarta.annotation.Resource;
@Stateless
public class DocumentService {
@Resource
private EJBContext context; // 注入 EJBContext
public void deleteDocument(Long docId) {
// 编程式权限检查
if (!context.isCallerInRole("admin")) {
throw new SecurityException("只有管理员可以删除文档");
}
// 删除逻辑...
}
}
|
3. 安全最佳实践
- 优先使用声明式安全(注解),代码更简洁、可维护性更高;
- 方法级别的权限控制要具体到业务方法,而非整个类;
- 敏感操作(删除、修改权限等)使用
@RolesAllowed 严格控制;
- 公开接口使用
@PermitAll 显式声明。
六、EJB 与 CDI 的关系
1. CDI 是什么
CDI(Contexts and Dependency Injection,上下文与依赖注入)是 Jakarta EE 的依赖注入和上下文管理规范。它提供了一种类型安全的依赖注入机制。
2. EJB 与 CDI 的整合
在 Jakarta EE 中,EJB 和 CDI 深度整合:
- EJB 可以使用 CDI 注入:EJB 中可以使用
@Inject 注入 CDI Bean;
- CDI 可以注入 EJB:CDI Bean 中可以使用
@EJB 或 @Inject 注入 EJB;
- EJB 可以利用 CDI 的生命周期管理:EJB 可以受益于 CDI 的上下文生命周期管理;
- EJB 可以使用 CDI 事件:EJB 可以发布和监听 CDI 事件。
3. 代码示例
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
|
// EJB 中使用 CDI 注入
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
@Stateless
public class OrderService {
@Inject
private PaymentProcessor paymentProcessor; // CDI Bean
@Inject
@ConfigValue("max.retry.count") // 自定义 Qualifier
private int maxRetryCount; // CDI 注入配置值
public void processOrder(Order order) {
// 使用 CDI 注入的依赖
paymentProcessor.process(order, maxRetryCount);
}
}
// CDI Bean 中使用 EJB
import jakarta.inject.Named;
import jakarta.ejb.EJB;
@Named
public class OrderController {
@EJB
private OrderService orderService; // 注入 EJB
public String createOrder(Order order) {
orderService.processOrder(order);
return "success";
}
}
|
4. @EJB vs @Inject
| 对比维度 |
@EJB |
@Inject |
| 适用对象 |
专门用于注入 EJB |
通用依赖注入(CDI Bean、EJB 等) |
| 类型安全 |
是 |
是(CDI 是类型安全的) |
| 是否需要额外配置 |
否(EJB 规范内置) |
需启用 CDI |
| 推荐用法 |
明确注入 EJB 时使用 |
通用场景使用 |
最佳实践:在 Jakarta EE 环境中,EJB 和 CDI 可以混合使用。一般来说:
- 注入 EJB 时使用
@EJB 更明确;
- 注入其他 CDI Bean 时使用
@Inject;
- 两者可以同时使用,互不冲突。
七、现代 EJB 的轻量化演进
1. EJB 的演进历程
| 版本 |
发布时间 |
核心变化 |
| EJB 1.0 |
1998 |
初始规范,极其复杂(需要 Home/Remote 接口) |
| EJB 2.0 |
2001 |
引入本地接口、CMP 实体 Bean |
| EJB 3.0 |
2006 |
重大简化:引入注解、移除强制接口、JPA 取代 Entity Bean |
| EJB 3.1 |
2009 |
引入单例 Bean、简化部署、异步调用 |
| EJB 3.2 |
2013 |
容器管理并发(CMC)、增强定时服务 |
| Jakarta EE 8 |
2019 |
包名从 javax.ejb 迁移到 jakarta.ejb |
| Jakarta Enterprise Beans 4.0 |
2020 |
移除 EJB 2.x 遗留接口、移除嵌入式容器支持 |
| Jakarta Enterprise Beans 4.1 |
2026 |
最新版本 |
2. EJB 的轻量化:从“重型”到“轻量”
早期 EJB(2.x 及以前)因以下原因被称为“重型”:
- 必须实现特定接口(
SessionBean、EntityBean);
- 必须配置复杂的
ejb-jar.xml 部署描述符;
- 必须提供 Home 和 Remote 接口;
- 实体 Bean 极其复杂且性能差。
EJB 3.0 开始的轻量化革命:
| 旧方式(EJB 2.x) |
新方式(EJB 3.x) |
必须实现 SessionBean 接口 |
普通 POJO + @Stateless 注解 |
必须配置 ejb-jar.xml |
注解即可(零 XML) |
| 必须提供 Home/Remote 接口 |
可选,支持无接口视图 |
| 复杂的企业级编程模型 |
简洁的注解驱动模型 |
3. EJB Lite(轻量级子集)
Jakarta EE 定义了 EJB Lite 子集,适用于 Web 容器(如 Tomcat + OpenEJB)等轻量环境。EJB Lite 包含:
- 无状态会话 Bean(Stateless);
- 有状态会话 Bean(Stateful);
- 单例会话 Bean(Singleton);
- 拦截器(Interceptor);
- 定时服务(Timer Service)。
EJB Lite 不包含:
- 消息驱动 Bean(MDB);
- 远程调用(Remote Interface);
- EJB 2.x 遗留接口。
4. EJB 与 Spring 的对比
| 对比维度 |
EJB |
Spring |
| 标准 |
Jakarta EE 标准规范 |
第三方框架 |
| 容器 |
需要 EJB 容器(应用服务器) |
轻量级 IoC 容器 |
| 事务管理 |
CMT(声明式) |
@Transactional |
| 依赖注入 |
@EJB、@Inject |
@Autowired |
| 远程调用 |
原生支持(RMI/IIOP) |
需额外配置(RMI/HTTP) |
| 学习曲线 |
中等(现代 EJB 已大幅简化) |
中等 |
| 部署 |
应用服务器(WildFly/Payara) |
任意 Web 容器(Tomcat) |
| 云原生 |
通过 MicroProfile 支持 |
Spring Boot/Cloud 生态更成熟 |
现代选择建议:
- 如果你使用 Jakarta EE 应用服务器(WildFly、Payara),EJB 是天然选择;
- 如果你使用 Spring Boot,Spring 的事务和依赖注入机制更合适;
- 两者都在向轻量化和云原生演进,没有绝对的优劣。
八、实战案例:企业任务管理系统
1. 业务需求
实现一个简单的任务管理系统,包含:
- 用户创建和分配任务;
- 任务状态变更;
- 任务完成后发送通知(异步消息);
- 系统配置管理(全局);
- 权限控制(普通用户只能操作自己的任务,管理员可操作所有任务)。
2. 技术选型
| 组件 |
技术 |
| 业务逻辑 |
无状态会话 Bean(Stateless) |
| 用户会话 |
有状态会话 Bean(Stateful) |
| 全局配置 |
单例会话 Bean(Singleton) |
| 异步通知 |
消息驱动 Bean(MDB) |
| 事务管理 |
CMT(容器管理事务) |
| 安全控制 |
声明式安全(@RolesAllowed) |
| 数据持久化 |
JPA(配合上一章内容) |
| 依赖注入 |
CDI + @EJB |
3. 代码实现
3.1 实体类(配合 JPA)
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
|
// Task.java
import jakarta.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "t_task")
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
private String description;
private String status; // PENDING, IN_PROGRESS, COMPLETED, CANCELLED
@Column(name = "assignee_id")
private Long assigneeId;
@Column(name = "create_time")
private LocalDateTime createTime;
@Column(name = "update_time")
private LocalDateTime updateTime;
// getter/setter 省略
}
|
3.2 无状态会话 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
import jakarta.ejb.Stateless;
import jakarta.ejb.TransactionAttribute;
import jakarta.ejb.TransactionAttributeType;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.annotation.security.RolesAllowed;
import jakarta.annotation.security.PermitAll;
import java.time.LocalDateTime;
import java.util.List;
@Stateless
public class TaskService {
@PersistenceContext
private EntityManager em;
// 创建任务(需要登录用户)
@RolesAllowed({"user", "admin"})
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Task createTask(String title, String description, Long assigneeId) {
Task task = new Task();
task.setTitle(title);
task.setDescription(description);
task.setAssigneeId(assigneeId);
task.setStatus("PENDING");
task.setCreateTime(LocalDateTime.now());
task.setUpdateTime(LocalDateTime.now());
em.persist(task);
return task;
}
// 查询用户的任务
@PermitAll
public List<Task> findTasksByUser(Long userId) {
return em.createQuery(
"SELECT t FROM Task t WHERE t.assigneeId = :userId ORDER BY t.createTime DESC",
Task.class)
.setParameter("userId", userId)
.getResultList();
}
// 更新任务状态(管理员可更新任何任务)
@RolesAllowed({"admin"})
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void updateTaskStatus(Long taskId, String status) {
Task task = em.find(Task.class, taskId);
if (task == null) {
throw new IllegalArgumentException("任务不存在");
}
task.setStatus(status);
task.setUpdateTime(LocalDateTime.now());
// 事务提交时自动 UPDATE
}
// 完成任务(普通用户只能完成自己的任务)
@RolesAllowed({"user", "admin"})
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void completeTask(Long taskId, Long currentUserId) {
Task task = em.find(Task.class, taskId);
if (task == null) {
throw new IllegalArgumentException("任务不存在");
}
// 权限校验:非管理员只能完成自己的任务
if (!isAdmin(currentUserId) && !task.getAssigneeId().equals(currentUserId)) {
throw new SecurityException("只能完成自己分配的任务");
}
task.setStatus("COMPLETED");
task.setUpdateTime(LocalDateTime.now());
}
private boolean isAdmin(Long userId) {
// 实际项目应从数据库或安全上下文获取角色
return false;
}
}
|
3.3 单例会话 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
import jakarta.ejb.Singleton;
import jakarta.ejb.Startup;
import jakarta.ejb.Lock;
import jakarta.ejb.LockType;
import jakarta.annotation.PostConstruct;
import java.util.concurrent.ConcurrentHashMap;
@Singleton
@Startup
public class SystemConfigService {
private ConcurrentHashMap<String, String> configMap;
private int maxTaskPerUser;
@PostConstruct
public void init() {
configMap = new ConcurrentHashMap<>();
configMap.put("system.name", "Task Management System");
configMap.put("system.version", "1.0.0");
maxTaskPerUser = 10; // 默认值
System.out.println("系统配置服务初始化完成");
}
@Lock(LockType.READ)
public String getConfig(String key) {
return configMap.get(key);
}
@Lock(LockType.WRITE)
public void setConfig(String key, String value) {
configMap.put(key, value);
}
@Lock(LockType.READ)
public int getMaxTaskPerUser() {
return maxTaskPerUser;
}
@Lock(LockType.WRITE)
public void setMaxTaskPerUser(int max) {
if (max < 1) {
throw new IllegalArgumentException("最大任务数不能小于1");
}
this.maxTaskPerUser = max;
}
}
|
3.4 有状态会话 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
import jakarta.ejb.Stateful;
import jakarta.ejb.Remove;
import java.util.ArrayList;
import java.util.List;
@Stateful
public class UserSessionService {
private Long userId;
private String username;
private List<String> recentOperations = new ArrayList<>();
public void login(Long userId, String username) {
this.userId = userId;
this.username = username;
recentOperations.clear();
recentOperations.add("登录系统");
System.out.println("用户 " + username + " 登录,会话已建立");
}
public Long getCurrentUserId() {
return userId;
}
public String getCurrentUsername() {
return username;
}
public void addOperation(String operation) {
recentOperations.add(operation);
if (recentOperations.size() > 20) {
recentOperations.remove(0); // 只保留最近20条
}
}
public List<String> getRecentOperations() {
return new ArrayList<>(recentOperations);
}
@Remove
public void logout() {
System.out.println("用户 " + username + " 登出,会话已销毁");
userId = null;
username = null;
recentOperations.clear();
}
}
|
3.5 消息驱动 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
32
33
34
35
36
37
38
|
import jakarta.ejb.MessageDriven;
import jakarta.jms.Message;
import jakarta.jms.MessageListener;
import jakarta.jms.TextMessage;
import jakarta.jms.JMSException;
import jakarta.ejb.ActivationConfigProperty;
@MessageDriven(
activationConfig = {
@ActivationConfigProperty(
propertyName = "destinationType",
propertyValue = "jakarta.jms.Queue"
),
@ActivationConfigProperty(
propertyName = "destination",
propertyValue = "queue/taskNotification"
)
}
)
public class TaskNotificationMDB implements MessageListener {
@Override
public void onMessage(Message message) {
try {
if (message instanceof TextMessage) {
TextMessage textMsg = (TextMessage) message;
String content = textMsg.getText();
System.out.println("【通知】" + content);
// 实际项目:发送邮件、推送通知等
// sendEmail(content);
// pushNotification(content);
}
} catch (JMSException e) {
System.err.println("处理通知消息失败:" + e.getMessage());
}
}
}
|
3.6 使用 EJB 的 Web 层(Servlet)
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
41
42
43
44
45
46
47
48
|
import jakarta.ejb.EJB;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.util.List;
@WebServlet("/task")
public class TaskServlet extends HttpServlet {
@EJB
private TaskService taskService;
@EJB
private SystemConfigService configService;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
HttpSession session = req.getSession();
Long userId = (Long) session.getAttribute("userId");
if (userId == null) {
resp.sendRedirect("/login");
return;
}
// 查询用户的任务列表
List<Task> tasks = taskService.findTasksByUser(userId);
// 获取系统配置
String systemName = configService.getConfig("system.name");
int maxTask = configService.getMaxTaskPerUser();
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().println("<html><body>");
resp.getWriter().println("<h1>" + systemName + "</h1>");
resp.getWriter().println("<p>最大任务数:" + maxTask + "</p>");
resp.getWriter().println("<h2>我的任务</h2>");
resp.getWriter().println("<ul>");
for (Task task : tasks) {
resp.getWriter().println("<li>" + task.getTitle() + " - " + task.getStatus() + "</li>");
}
resp.getWriter().println("</ul>");
resp.getWriter().println("</body></html>");
}
}
|
4. 架构图
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
|
┌─────────────────────────────────────────────────────────────────┐
│ 客户端(浏览器/APP) │
└────────────────────────────┬────────────────────────────────────┘
│ HTTP
▼
┌─────────────────────────────────────────────────────────────────┐
│ Web 层(Servlet/JSP) │
│ 注入 EJB(@EJB)调用业务逻辑 │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ EJB 层(业务逻辑) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ TaskService │ │UserSession │ │ SystemConfigService │ │
│ │ (Stateless) │ │(Stateful) │ │ (Singleton) │ │
│ │ 事务: CMT │ │ 会话状态 │ │ 全局配置 │ │
│ │ 安全:注解 │ │ @Remove │ │ @Startup │ │
│ └──────┬───────┘ └──────────────┘ └──────────────────────┘ │
│ │ │
│ │ 发送消息 │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ TaskNotificationMDB (Message-Driven Bean) │ │
│ │ 异步处理通知消息 │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│ JPA
▼
┌─────────────────────────────────────────────────────────────────┐
│ JPA(数据持久化) │
│ EntityManager │
└────────────────────────────┬────────────────────────────────────┘
│ JDBC
▼
┌─────────────────────────────────────────────────────────────────┐
│ 数据库(MySQL) │
└─────────────────────────────────────────────────────────────────┘
|
九、总结
1. 核心知识脉络
| 层级 |
技术 |
核心概念 |
| 组件类型 |
会话 Bean |
Stateless、Stateful、Singleton |
| 异步处理 |
消息驱动 Bean |
MDB、JMS、onMessage() |
| 事务管理 |
CMT / BMT |
@TransactionAttribute、UserTransaction |
| 安全控制 |
声明式安全 |
@RolesAllowed、@PermitAll、@DenyAll |
| 依赖注入 |
CDI 整合 |
@EJB、@Inject、类型安全注入 |
2. 关键理解
- EJB 是服务器端业务组件模型:容器管理生命周期、事务、安全、并发等底层问题,开发者专注业务逻辑;
- 会话 Bean 是核心:无状态(Stateless)最常用,有状态(Stateful)保持会话,单例(Singleton)全局共享;
- MDB 实现异步解耦:消息驱动,自动触发,发送方无需等待;
- CMT 是事务首选:声明式事务,代码最简洁,RuntimeException 自动回滚;
- 声明式安全优于编程式安全:注解控制权限,更清晰、更易维护;
- EJB 与 CDI 深度整合:可以互相注入,发挥各自优势;
- 现代 EJB 已经轻量化:注解驱动、零 XML、POJO 编程模型,不再“重型”。
3. EJB 的现代定位
虽然 Spring 框架在轻量级企业应用开发中占据主导地位,但 EJB 作为 Jakarta EE 的核心规范,在以下场景中仍有重要价值:
- 使用 Jakarta EE 应用服务器(WildFly、Payara、GlassFish)的企业项目;
- 需要标准化、避免供应商锁定的场景;
- **与 JPA、JMS、CDI 等 Jakarta EE 规范深度集成的项目;
- 遗留系统的维护和升级。
更重要的是,理解 EJB 的设计思想(容器管理、声明式服务、组件化架构)对理解整个 Jakarta EE 体系、乃至理解 Spring 等框架的底层原理都有极大帮助。