«1. Обзор

Всякий раз, когда для данного варианта использования требуется один экземпляр Session Bean, мы можем использовать Singleton Session Bean.

В этом уроке мы рассмотрим это на примере приложения Jakarta EE.

2. Maven

Прежде всего, нам нужно определить необходимые зависимости Maven в файле pom.xml.

Давайте определим зависимости для EJB API и встроенного контейнера EJB для развертывания 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>

Последние версии можно найти на Maven Central в JavaEE API и tomEE.

3. Типы сеансовых компонентов

Существует три типа сеансовых компонентов. Прежде чем мы рассмотрим Singleton Session Bean, давайте посмотрим, в чем разница между жизненными циклами трех типов.

3.1. Stateful Session Bean

Stateful Session Bean поддерживает состояние диалога с клиентом, с которым он общается.

Каждый клиент создает новый экземпляр Stateful Bean и не используется совместно с другими клиентами.

Когда связь между клиентом и bean-компонентом завершается, Session Bean также завершается.

3.2. Сессионные компоненты без сохранения состояния

Сессионные компоненты без сохранения состояния не поддерживают состояние диалога с клиентом. Компонент содержит состояние, специфичное для клиента, только до момента вызова метода.

Последовательные вызовы методов независимы, в отличие от Stateful Session Bean.

Контейнер поддерживает пул Stateless Bean-компонентов, и эти экземпляры могут совместно использоваться несколькими клиентами.

3.3. Singleton Session Beans

Singleton Session Bean поддерживает состояние компонента на протяжении всего жизненного цикла приложения.

Одиночные сеансовые компоненты аналогичны сеансовым компонентам без сохранения состояния, но только один экземпляр одноэлементного сеансового компонента создается во всем приложении и не завершается до тех пор, пока приложение не будет закрыто.

Один экземпляр bean-компонента совместно используется несколькими клиентами и может быть доступен одновременно.

4. Создание Singleton Session Bean

Давайте начнем с создания для него интерфейса.

В этом примере давайте используем аннотацию javax.ejb.Local для определения интерфейса:

@Local
public interface CountryState {
   List<String> getStates(String country);
   void setStates(String country, List<String> states);
}

Использование @Local означает, что доступ к bean-компоненту осуществляется в том же приложении. У нас также есть возможность использовать аннотацию javax.ejb.Remote, которая позволяет нам удаленно вызывать EJB.

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

Кроме того, пометим bean-компонент аннотацией javax.ejb.Startup, чтобы указать EJB-контейнеру инициализировать bean-компонент при запуске:

@Singleton
@Startup
public class CountryStateContainerManagedBean implements CountryState {
    ...
}

Это называется энергичной инициализацией. Если мы не используем @Startup, контейнер EJB определяет, когда инициализировать компонент.

Мы также можем определить несколько сеансовых компонентов для инициализации данных и загрузки компонентов в определенном порядке. Поэтому мы будем использовать аннотацию javax.ejb.DependsOn для определения зависимости нашего компонента от других сеансовых компонентов.

Значение для аннотации @DependsOn представляет собой массив имен имен классов Bean, от которых зависит наш Bean:

@Singleton 
@Startup 
@DependsOn({"DependentBean1", "DependentBean2"}) 
public class CountryStateCacheBean implements CountryState { 
    ...
}

Мы определим метод initialize(), который инициализирует bean и делает его метод обратного вызова жизненного цикла с использованием аннотации javax.annotation.PostConstruct.

С этой аннотацией он будет вызываться контейнером при создании экземпляра компонента:

@PostConstruct
public void initialize() {

    List<String> states = new ArrayList<String>();
    states.add("Texas");
    states.add("Alabama");
    states.add("Alaska");
    states.add("Arizona");
    states.add("Arkansas");

    countryStatesMap.put("UnitedStates", states);
}

5. Параллелизм

Далее мы разработаем управление параллелизмом для Singleton Session Bean. EJB предоставляет два метода реализации параллельного доступа к Singleton Session Bean: параллелизм, управляемый контейнером, и параллелизм, управляемый компонентом.

Аннотация javax.ejb.ConcurrencyManagement определяет политику параллелизма для метода. По умолчанию контейнер EJB использует параллелизм, управляемый контейнером.

Аннотация @ConcurrencyManagement принимает значение javax.ejb.ConcurrencyManagementType. Возможные варианты:

    ConcurrencyManagementType.CONTAINER для параллелизма, управляемого контейнером. ConcurrencyManagementType.BEAN для параллелизма, управляемого компонентом.

5.1. Параллелизм, управляемый контейнером

«Проще говоря, в параллелизме, управляемом контейнером, контейнер контролирует доступ клиентов к методам.

Давайте воспользуемся аннотацией @ConcurrencyManagement со значением javax.ejb.ConcurrencyManagementType.CONTAINER:

@Singleton
@Startup
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
public class CountryStateContainerManagedBean implements CountryState {
    ...
}

Чтобы указать уровень доступа к каждому из бизнес-методов синглтона, мы будем использовать javax.ejb.Lock аннотация. javax.ejb.LockType содержит значения для аннотации @Lock. javax.ejb.LockType определяет два значения:

    LockType.WRITE — это значение обеспечивает эксклюзивную блокировку для вызывающего клиента и предотвращает доступ всех других клиентов ко всем методам компонента. Используйте это для методов, которые изменяют состояние одноэлементного компонента. LockType.READ — это значение обеспечивает одновременную блокировку нескольких клиентов для доступа к методу. Используйте это для методов, которые только считывают данные из bean-компонента.

Имея это в виду, мы определим метод setStates() с аннотацией @Lock(LockType.WRITE), чтобы предотвратить одновременное обновление состояния клиентами.

Чтобы клиенты могли читать данные одновременно, мы аннотируем getStates() с помощью @Lock(LockType.READ):

@Singleton 
@Startup 
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER) 
public class CountryStateContainerManagedBean implements CountryState { 

    private final Map<String, List<String> countryStatesMap = new HashMap<>();

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

Чтобы остановить выполнение методов в течение длительного времени и заблокировать других клиентов на неопределенный срок, мы будем использовать аннотацию javax.ejb.AccessTimeout для тайм-аута долго ожидающих вызовов.

Используйте аннотацию @AccessTimeout для определения времени ожидания метода в миллисекундах. По истечении времени ожидания контейнер генерирует исключение javax.ejb.ConcurrentAccessTimeoutException, и выполнение метода прекращается.

5.2. Параллелизм, управляемый компонентом

В параллелизме, управляемом компонентом, контейнер не контролирует одновременный доступ клиентов к Singleton Session Bean. Разработчик должен реализовать параллелизм самостоятельно.

Если параллелизм не реализован разработчиком, все методы доступны для всех клиентов одновременно. Java предоставляет примитивы синхронизации и volatile для реализации параллелизма.

Чтобы узнать больше о параллелизме, прочитайте о java.util.concurrent здесь и об атомарных переменных здесь.

Для параллелизма, управляемого bean-компонентом, давайте определим аннотацию @ConcurrencyManagement со значением javax.ejb.ConcurrencyManagementType.BEAN для класса Singleton Session Bean:

@Singleton 
@Startup 
@ConcurrencyManagement(ConcurrencyManagementType.BEAN) 
public class CountryStateBeanManagedBean implements CountryState { 
   ... 
}

Далее мы напишем setStates( ), который изменяет состояние компонента с помощью ключевого слова synchronized:

public synchronized void setStates(String country, List<String> states) {
    countryStatesMap.put(country, states);
}

Ключевое слово synchronized делает метод доступным только для одного потока в каждый момент времени.

Метод getStates() не изменяет состояние Bean-компонента, поэтому ему не нужно использовать ключевое слово synchronized.

6. Клиент

Теперь мы можем написать клиент для доступа к нашему Singleton Session Bean.

Мы можем развернуть Session Bean на серверах контейнеров приложений, таких как JBoss, Glassfish и т. д. Для простоты мы будем использовать класс javax.ejb.embedded.EJBContainer. EJBContainer работает на той же JVM, что и клиент, и предоставляет большинство сервисов корпоративного контейнера компонентов.

Сначала мы создадим экземпляр EJBContainer. Этот экземпляр контейнера будет искать и инициализировать все модули EJB, представленные в classpath:

public class CountryStateCacheBeanTest {

    private EJBContainer ejbContainer = null;

    private Context context = null;

    @Before
    public void init() {
        ejbContainer = EJBContainer.createEJBContainer();
        context = ejbContainer.getContext();
    }
}

Далее мы получим объект javax.naming.Context из инициализированного объекта контейнера. Используя экземпляр Context, мы можем получить ссылку на CountryStateContainerManagedBean и вызвать методы:

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

Точно так же мы можем использовать экземпляр Context, чтобы получить ссылку на Bean-Managed Singleton 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());
}

Завершите наши тесты, закрыв EJBContainer в методе close():

@After
public void close() {
    if (ejbContainer != null) {
        ejbContainer.close();
    }
}

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

Singleton Session Bean столь же гибок и мощен, как и любой стандартный Session Bean, но позволяет нам применять Singleton шаблон для обмена состоянием между клиентами нашего приложения.

Параллельное управление Singleton Bean можно легко реализовать с помощью Container-Managed Concurrency, где контейнер обеспечивает одновременный доступ нескольких клиентов, или вы также можете реализовать свое собственное управление параллелизмом с помощью Bean-Managed Concurrency.

Исходный код этого руководства можно найти на GitHub.