1. 概述

在 Jakarta EE 应用中,当你需要在整个应用生命周期内仅存在一个实例的会话 Bean 时,单例会话 Bean(Singleton Session Bean) 就是最佳选择。

本文通过一个具体示例,带你掌握它的用法和关键特性。内容涵盖生命周期、并发控制以及客户端调用方式,帮你避开常见踩坑点。


2. Maven 依赖

要使用 EJB 功能,首先得引入必要的依赖。以下是核心配置:

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>8.0</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>org.apache.openejb</groupId>
    <artifactId>tomee-embedded</artifactId>
    <version>1.7.5</version>
</dependency>

javaee-api 提供了 EJB 相关注解和接口,通常由应用服务器提供,所以 scope 设为 provided
tomee-embedded 是嵌入式 EJB 容器,适合本地测试,无需部署到完整应用服务器。

最新版可前往 Maven CentralTomEE 官方页面 查看。


3. 会话 Bean 的三种类型

在深入单例 Bean 前,先快速回顾下 EJB 中的三种会话 Bean 类型及其生命周期差异。

3.1. 有状态会话 Bean(Stateful Session Bean)

  • ✅ 每个客户端拥有独立的 Bean 实例。
  • ✅ 维护与客户端之间的“会话状态”,比如购物车内容。
  • ❌ 实例不共享,资源消耗较高。
  • ⚠️ 客户端断开后,Bean 实例通常会被销毁。

3.2. 无状态会话 Bean(Stateless Session Bean)

  • ✅ 所有客户端共享一个实例池(pool)。
  • ✅ 方法调用之间无状态关联,适合处理独立请求(如查询天气)。
  • ✅ 高并发下性能好,资源利用率高。
  • ⚠️ 不能保存跨方法调用的状态数据。

3.3. 单例会话 Bean(Singleton Session Bean)

  • ✅ 整个应用中仅存在一个实例。
  • ✅ 实例在应用启动时创建,直到应用关闭才销毁。
  • ✅ 多个客户端共享该实例,可用于全局缓存、配置管理等场景。
  • ✅ 支持并发访问,但需注意线程安全问题。

简单粗暴地说:如果你需要一个“全局变量”级别的组件,又想享受 EJB 容器管理的好处,那就用 Singleton Session Bean。


4. 创建单例会话 Bean

4.1 定义业务接口

先定义一个本地接口(Local Interface),表示该 Bean 只在当前应用内使用:

@Local
public interface CountryState {
   List<String> getStates(String country);
   void setStates(String country, List<String> states);
}

📌 @Local 表示本地调用,若需远程调用可用 @Remote,但实际项目中较少见。


4.2 实现单例 Bean

使用 @Singleton 注解标记为单例 Bean,并结合其他控制注解:

@Singleton
@Startup
public class CountryStateContainerManagedBean implements CountryState {
    private final Map<String, List<String>> countryStatesMap = new HashMap<>();

    @PostConstruct
    public void initialize() {
        List<String> states = new ArrayList<>();
        states.add("Texas");
        states.add("Alabama");
        states.add("Alaska");
        states.add("Arizona");
        states.add("Arkansas");

        countryStatesMap.put("UnitedStates", states);
    }

    @Lock(LockType.READ)
    public List<String> getStates(String country) {
        return countryStatesMap.get(country);
    }

    @Lock(LockType.WRITE)
    public void setStates(String country, List<String> states) {
        countryStatesMap.put(country, states);
    }
}

关键注解说明:

注解 作用
@Singleton 标记为单例 Bean
@Startup 应用启动时立即初始化(即“饿汉式”加载)
@PostConstruct Bean 初始化后自动执行的方法
@DependsOn({"BeanA", "BeanB"}) 控制初始化顺序,确保依赖的 Bean 先加载

示例中 initialize() 方法在容器创建 Bean 后自动执行,完成数据预加载。


5. 并发控制

单例 Bean 被多个线程同时访问是常态,因此并发管理至关重要。EJB 提供两种模式:

5.1 容器管理并发(Container-Managed Concurrency)

这是默认模式,由容器帮你处理锁机制。

使用 @ConcurrencyManagement(CONTAINER) 明确指定:

@Singleton
@Startup
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
public class CountryStateContainerManagedBean implements CountryState {
    // ...
}

然后通过 @Lock 注解控制方法级别的访问权限:

  • @Lock(LockType.WRITE):写锁,独占访问,其他线程必须等待。
  • @Lock(LockType.READ):读锁,允许多个线程并发读取。

示例代码:

@Lock(LockType.READ)
public List<String> getStates(String country) {
    return countryStatesMap.get(country);
}

@Lock(LockType.WRITE)
public void setStates(String country, List<String> states) {
    countryStatesMap.put(country, states);
}

防止无限等待:使用 @AccessTimeout

长时间持有写锁会导致“饿死”问题。可通过 @AccessTimeout 设置超时:

@AccessTimeout(value = 5000, unit = TimeUnit.MILLISECONDS)
@Lock(LockType.WRITE)
public void setStates(String country, List<String> states) {
    // 模拟耗时操作
    try {
        Thread.sleep(6000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    countryStatesMap.put(country, states);
}

⚠️ 若方法执行超过 5 秒,容器将抛出 ConcurrentAccessTimeoutException,避免阻塞太久。


5.2 Bean 管理并发(Bean-Managed Concurrency)

在这种模式下,容器不干预并发控制,完全由开发者自己负责线程安全

启用方式:

@Singleton
@Startup
@ConcurrencyManagement(ConcurrencyManagementType.BEAN)
public class CountryStateBeanManagedBean implements CountryState {
    private final Map<String, List<String>> countryStatesMap = new HashMap<>();

    public synchronized void setStates(String country, List<String> states) {
        countryStatesMap.put(country, states);
    }

    public List<String> getStates(String country) {
        return countryStatesMap.get(country);
    }
}

📌 特点:

  • ✅ 完全自由控制同步逻辑,适合复杂场景。
  • ❌ 容易出错,必须手动处理 synchronizedvolatilejava.util.concurrent 工具类。
  • ❌ 一旦疏忽,极易引发线程安全问题。

推荐优先使用 容器管理并发,简单、安全、不易出错。


6. 客户端调用测试

我们可以使用嵌入式 EJB 容器进行单元测试,无需部署到外部服务器。

6.1 初始化 EJB 容器

public class CountryStateCacheBeanTest {

    private EJBContainer ejbContainer = null;
    private Context context = null;

    @Before
    public void init() {
        ejbContainer = EJBContainer.createEJBContainer();
        context = ejbContainer.getContext();
    }

    @After
    public void close() {
        if (ejbContainer != null) {
            ejbContainer.close();
        }
    }
}

EJBContainer 会在 classpath 中自动扫描并初始化所有 EJB 组件。


6.2 测试容器管理并发的 Bean

@Test
public void whenCallGetStatesFromContainerManagedBean_ReturnsStatesForCountry() throws Exception {
    String[] expectedStates = {"Texas", "Alabama", "Alaska", "Arizona", "Arkansas"};

    CountryState countryStateBean = (CountryState) context
        .lookup("java:global/singleton-ejb-bean/CountryStateContainerManagedBean");
    List<String> actualStates = countryStateBean.getStates("UnitedStates");

    assertNotNull(actualStates);
    assertArrayEquals(expectedStates, actualStates.toArray());
}

@Test
public void whenCallSetStatesFromContainerManagedBean_SetsStatesForCountry() throws Exception {
    String[] expectedStates = { "California", "Florida", "Hawaii", "Pennsylvania", "Michigan" };

    CountryState countryStateBean = (CountryState) context
        .lookup("java:global/singleton-ejb-bean/CountryStateContainerManagedBean");
    countryStateBean.setStates("UnitedStates", Arrays.asList(expectedStates));

    List<String> actualStates = countryStateBean.getStates("UnitedStates");
    assertNotNull(actualStates);
    assertArrayEquals(expectedStates, actualStates.toArray());
}

6.3 测试 Bean 管理并发的 Bean

@Test
public void whenCallGetStatesFromBeanManagedBean_ReturnsStatesForCountry() throws Exception {
    String[] expectedStates = { "Texas", "Alabama", "Alaska", "Arizona", "Arkansas" };

    CountryState countryStateBean = (CountryState) context
        .lookup("java:global/singleton-ejb-bean/CountryStateBeanManagedBean");
    List<String> actualStates = countryStateBean.getStates("UnitedStates");

    assertNotNull(actualStates);
    assertArrayEquals(expectedStates, actualStates.toArray());
}

@Test
public void whenCallSetStatesFromBeanManagedBean_SetsStatesForCountry() throws Exception {
    String[] expectedStates = { "California", "Florida", "Hawaii", "Pennsylvania", "Michigan" };

    CountryState countryStateBean = (CountryState) context
        .lookup("java:global/singleton-ejb-bean/CountryStateBeanManagedBean");
    countryStateBean.setStates("UnitedStates", Arrays.asList(expectedStates));

    List<String> actualStates = countryStateBean.getStates("UnitedStates");
    assertNotNull(actualStates);
    assertArrayEquals(expectedStates, actualStates.toArray());
}

📌 JNDI 名称格式一般为:java:global/{模块名}/{Bean类名},具体取决于打包结构。


7. 总结

单例会话 Bean 是 Jakarta EE 中实现全局共享状态的利器,适用于:

  • ✅ 全局缓存(如国家-省份映射)
  • ✅ 配置中心
  • ✅ 定时任务调度器
  • ✅ 日志聚合器

推荐实践:

场景 建议
多数情况 使用 @ConcurrencyManagement(CONTAINER) + @Lock
复杂同步逻辑 考虑 BEAN 模式,但务必谨慎
初始化顺序依赖 使用 @DependsOn
需要预加载 加上 @Startup@PostConstruct

源码已托管至 GitHub:https://github.com/eugenp/tutorials/tree/master/spring-ejb-modules

合理使用单例 Bean,既能提升性能,又能避免重复创建对象,是企业级开发中不可忽视的重要组件。


原始标题:Singleton Session Bean in Jakarta EE