背景

本文是《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 及以前)因以下原因被称为“重型”:

  • 必须实现特定接口(SessionBeanEntityBean);
  • 必须配置复杂的 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. 关键理解

  1. EJB 是服务器端业务组件模型:容器管理生命周期、事务、安全、并发等底层问题,开发者专注业务逻辑;
  2. 会话 Bean 是核心:无状态(Stateless)最常用,有状态(Stateful)保持会话,单例(Singleton)全局共享;
  3. MDB 实现异步解耦:消息驱动,自动触发,发送方无需等待;
  4. CMT 是事务首选:声明式事务,代码最简洁,RuntimeException 自动回滚;
  5. 声明式安全优于编程式安全:注解控制权限,更清晰、更易维护;
  6. EJB 与 CDI 深度整合:可以互相注入,发挥各自优势;
  7. 现代 EJB 已经轻量化:注解驱动、零 XML、POJO 编程模型,不再“重型”。

3. EJB 的现代定位

虽然 Spring 框架在轻量级企业应用开发中占据主导地位,但 EJB 作为 Jakarta EE 的核心规范,在以下场景中仍有重要价值:

  • 使用 Jakarta EE 应用服务器(WildFly、Payara、GlassFish)的企业项目;
  • 需要标准化、避免供应商锁定的场景;
  • **与 JPA、JMS、CDI 等 Jakarta EE 规范深度集成的项目;
  • 遗留系统的维护和升级

更重要的是,理解 EJB 的设计思想(容器管理、声明式服务、组件化架构)对理解整个 Jakarta EE 体系、乃至理解 Spring 等框架的底层原理都有极大帮助。