«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.