«1. Обзор

В этом руководстве мы более подробно рассмотрим два типа синглетонов, доступных в Jakarta EE. Мы объясним и продемонстрируем различия и рассмотрим варианты использования, подходящие для каждого из них.

Во-первых, давайте посмотрим, что такое синглтоны, прежде чем углубляться в детали.

2. Шаблон проектирования Singleton

Вспомните, что распространенный способ реализации шаблона Singleton — это статический экземпляр и закрытый конструктор:

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 {
    // ...
}

Наш класс имитирует автосервис. У нас много экземпляров разных Авто, но все они обслуживаются в одной мастерской. Таким образом, Singleton хорошо подходит.

Мы можем проверить, что это один и тот же экземпляр, с помощью простого теста JUnit, который дважды запрашивает контекст для класса. Обратите внимание, что здесь у нас есть вспомогательный метод getBean для удобочитаемости:

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

Из-за аннотации @Singleton контейнер будет возвращать одну и ту же ссылку оба раза. Однако если мы попробуем это сделать с простым управляемым компонентом, контейнер каждый раз будет предоставлять другой экземпляр.

И хотя это работает одинаково как для javax.inject.Singleton, так и для javax.ejb.Singleton, между ними есть ключевое различие.

4. EJB Singleton

Для создания EJB singleton мы используем аннотацию @Singleton из пакета javax.ejb. Таким образом мы создаем Singleton Session Bean.

Мы можем протестировать эту реализацию так же, как мы тестировали реализацию CDI в предыдущем примере, и результат будет таким же. Одиночные элементы EJB, как и ожидалось, предоставляют единственный экземпляр класса.

Однако синглтоны EJB также предоставляют дополнительную функциональность в виде управления параллелизмом, управляемого контейнером.

Когда мы используем этот тип реализации, EJB-контейнер гарантирует, что каждый публичный метод класса будет доступен одному потоку за раз. Если несколько потоков пытаются получить доступ к одному и тому же методу, только один поток может использовать его, в то время как другие ждут своей очереди.

Мы можем проверить это поведение с помощью простого теста. Мы представим симуляцию очереди обслуживания для наших одноэлементных классов:

private static int serviceQueue;

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

serviceQueue реализована как простое статическое целое число, которое увеличивается, когда автомобиль «въезжает» на обслуживание, и уменьшается, когда он «выходит». Если контейнер обеспечивает правильную блокировку, эта переменная должна быть равна нулю до и после службы и равна единице во время службы.

Мы можем проверить это поведение с помощью простого теста:

@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 параллельных потоков. Каждый поток создает экземпляр автомобиля и пытается его обслужить. После службы он утверждает, что значение serviceQueue возвращается к нулю.

Если мы, например, выполним аналогичный тест на синглтоне CDI, наш тест завершится ошибкой.

5. Заключение

В этой статье мы рассмотрели два типа одноэлементных реализаций, доступных в Jakarta EE. Мы увидели их преимущества и недостатки, а также продемонстрировали, как и когда использовать каждый из них.

И, как всегда, полный исходный код доступен на GitHub.