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 Central 和 TomEE 官方页面 查看。
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);
}
}
📌 特点:
- ✅ 完全自由控制同步逻辑,适合复杂场景。
- ❌ 容易出错,必须手动处理
synchronized
、volatile
或java.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,既能提升性能,又能避免重复创建对象,是企业级开发中不可忽视的重要组件。