CDI和EJB Singleton之间的区别

2025/04/08

1. 概述

在本教程中,我们将仔细研究Jakarta EE中提供的两种单例类型。我们将解释和演示它们之间的区别,并了解每种类型的适用用法。

首先,在了解细节之前,让我们先了解一下单例是什么。

2. 单例设计模式

回想一下,实现单例模式的常用方法是使用静态实例和私有构造函数:

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

    private Singleton() {}

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

但遗憾的是,这并不是真正面向对象的,而且它存在一些多线程问题

不过,CDI和EJB容器为我们提供了面向对象的替代方案

3. CDI单例

使用CDI(上下文和依赖注入),我们可以使用@Singleton注解轻松创建单例。此注解是javax.inject包的一部分,它指示容器实例化单例一次,并在注入期间将其引用传递给其他对象。

我们可以看到,使用CDI的单例实现非常简单:

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

我们的类模拟了一家汽车服务店,我们有很多不同Car的实例,但它们都使用同一家店进行维修。因此,单例很适合。

我们可以使用一个简单的JUnit测试来验证它是否是同一个实例,该测试两次询问该类的上下文。请注意,为了便于阅读,我们在这里提供了一个getBean辅助方法:

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

由于@Singleton注解,容器两次都会返回相同的引用。但是,如果我们使用普通托管Bean尝试此操作,容器每次都会提供不同的实例。

虽然这对于javax.inject.Singleton或javax.ejb.Singleton的作用相同,但两者之间存在一个关键区别。

4. EJB单例

要创建EJB单例,我们使用javax.ejb包中的@Singleton注解,这样我们就创建了一个单例会话Bean

我们可以按照上例中测试CDI实现的相同方式来测试此实现,结果将是相同的。正如预期的那样,EJB单例提供了类的单个实例。

但是,EJB Singleton还以容器管理并发控制的形式提供了附加功能

当我们使用这种类型的实现时,EJB容器会确保类的每个公共方法每次都只由一个线程访问。如果多个线程尝试访问同一个方法,则只有一个线程可以使用它,而其他线程则等待轮到它们。

我们可以通过一个简单的测试来验证这个行为,为我们的单例类引入一个服务队列模拟:

private static int serviceQueue;

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

serviceQueue实现为一个普通的静态整数,当汽车“进入”服务时,该整数增加,当汽车“离开”服务时,该整数减少。如果容器提供了适当的锁定,则该变量在服务前后应等于0,在服务期间应等于1。

我们可以通过一个简单的测试来检查这个行为:

@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;
}

此测试启动10个并行线程,每个线程实例化一个Car并尝试为其提供服务。服务结束后,它断言serviceQueue的值回到0。

此外,如果我们对CDI单例执行类似的测试,我们的测试将会失败。

5. 总结

在本文中,我们介绍了Jakarta EE中可用的两种单例实现;我们了解了它们的优点和缺点,并演示了如何以及何时使用每种方法。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章