1. 概述

本文将深入探讨 Jakarta EE 中提供的两种 Singleton 实现:CDI SingletonEJB Singleton。我们将通过示例代码说明它们的差异,并指出在何种场景下更适合使用哪一种。

2. Singleton 设计模式回顾

我们都知道,传统的 Singleton 模式通常是通过私有构造器和静态实例实现的:

public final class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

这种方式虽然简单,但并不是面向对象的做法,而且在多线程环境下还可能存在并发问题。

幸运的是,CDI 和 EJB 容器为我们提供了更优雅、更符合现代 Java EE 风格的 Singleton 实现方式。

3. CDI Singleton

使用 CDI(Contexts and Dependency Injection) 创建 Singleton 非常简单,只需使用 @Singleton 注解,它属于 javax.inject 包。

@Singleton
public class CarServiceSingleton {
    // ...
}

我们模拟了一个汽车服务中心类。在这个场景中,有很多不同的 Car 实例,但它们都使用同一个服务中心。Singleton 模式非常适合这种场景。

我们可以通过一个简单的 JUnit 测试来验证是否返回的是同一个实例:

@Test
public void givenASingleton_whenGetBeanIsCalledTwice_thenTheSameInstanceIsReturned() {       
    CarServiceSingleton one = getBean(CarServiceSingleton.class);
    CarServiceSingleton two = getBean(CarServiceSingleton.class);
    assertTrue(one == two);
}

测试通过,说明容器确实返回了同一个引用。

⚠️ 注意: 如果不加 @Singleton 注解,容器每次都会创建一个新的实例。

虽然 javax.inject.Singletonjavax.ejb.Singleton 都能实现单例,但它们的行为和特性有显著差异。

4. EJB Singleton

使用 EJB 创建 Singleton 时,我们需要使用 @Singleton 注解,这个注解属于 javax.ejb 包。

@Singleton
public class CarServiceEjbSingleton {
    private static int serviceQueue;

    public int service(Car car) {
        serviceQueue++;
        Thread.sleep(100);
        car.setServiced(true); 
        serviceQueue--;
        return serviceQueue;
    }
}

我们可以通过与 CDI Singleton 类似的测试方式验证其行为,结果也会是同一个实例。

但 EJB Singleton 的优势在于它支持容器管理的并发控制。

当多个线程同时访问 EJB Singleton 的公共方法时,EJB 容器会自动确保每次只有一个线程可以执行该方法。其余线程将排队等待。

我们可以用下面的测试来验证这种行为:

@Test
public void whenEjb_thenLockingIsProvided() {
    for (int i = 0; i < 10; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                int serviceQueue = carServiceEjbSingleton.service(new Car("Speedster xyz"));
                assertEquals(0, serviceQueue);
            }
        }).start();
    }
    return;
}

测试通过,说明 EJB 容器确实提供了线程同步机制。

⚠️ 如果我们将相同的测试应用于 CDI Singleton,测试将失败。

5. 总结

本文我们介绍了两种在 Jakarta EE 中常用的 Singleton 实现方式:

类型 来源包 是否支持并发控制 适用场景
CDI Singleton javax.inject ❌ 不支持 简单的共享实例、无并发要求的场景
EJB Singleton javax.ejb ✅ 支持 需要并发控制、事务管理等高级功能的场景

选择建议:

  • 如果只是共享一个实例,不需要并发控制,用 CDI Singleton 简单粗暴。
  • 如果需要线程安全、事务支持等企业级功能,推荐使用 EJB Singleton。

完整示例代码可在 GitHub 上找到。


原始标题:The Difference Between CDI and EJB Singleton