Bean Singleton Session en Yakarta EE.

1. Información general

Siempre que se requiera una sola instancia de un Session Bean para un caso de uso dado, podemos usar un Singleton Session Bean.

En este tutorial, vamos a explorar esto a través de un ejemplo, con una aplicación Jakarta EE.

2. Maven

En primer lugar, debemos definir las dependencias de Maven necesarias en pom.xml .

Definamos las dependencias para las API de EJB y el contenedor EJB integrado para la implementación de EJB:

 javax javaee-api 8.0 provided   org.apache.openejb tomee-embedded 1.7.5 

Las últimas versiones se pueden encontrar en Maven Central en JavaEE API y tomEE.

3. Tipos de beans de sesión

Hay tres tipos de Session Beans. Antes de explorar Singleton Session Beans, veamos cuál es la diferencia entre los ciclos de vida de los tres tipos.

3.1. Beans de sesión con estado

Un Bean de sesión con estado mantiene el estado conversacional con el cliente con el que se está comunicando.

Cada cliente crea una nueva instancia de Stateful Bean y no se comparte con otros clientes.

Cuando finaliza la comunicación entre el cliente y el bean, también termina el Session Bean.

3.2. Beans de sesión sin estado

Un Bean de sesión sin estado no mantiene ningún estado de conversación con el cliente. El bean contiene el estado específico del cliente solo hasta la duración de la invocación del método.

Las invocaciones de métodos consecutivos son independientes a diferencia del Bean de sesión con estado.

El contenedor mantiene un grupo de Beans sin estado y estas instancias se pueden compartir entre varios clientes.

3.3. Frijoles Singleton Session

Un Singleton Session Bean mantiene el estado del bean durante todo el ciclo de vida de la aplicación.

Los Singleton Session Beans son similares a los Stateless Session Beans, pero solo se crea una instancia del Singleton Session Bean en toda la aplicación y no finaliza hasta que se cierra la aplicación.

La única instancia del bean se comparte entre varios clientes y se puede acceder a ella simultáneamente.

4. Creación de un bean de sesión singleton

Comencemos creando una interfaz para ello.

Para este ejemplo, usemos la anotación javax.ejb.Local para definir la interfaz:

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

Usar @Local significa que se accede al bean dentro de la misma aplicación. También tenemos la opción de usar la anotación javax.ejb.Remote que nos permite llamar al EJB de forma remota.

Ahora, definiremos la clase de bean EJB de implementación. Marcamos la clase como un Singleton Session Bean usando la anotación javax .ejb.Singleton .

Además, también marquemos el bean con la anotación javax .ejb.Startup para informar al contenedor EJB que inicialice el bean al inicio:

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

Esto se llama inicialización ansiosa. Si no usamos @Startup , el contenedor EJB determina cuándo inicializar el bean.

También podemos definir múltiples Session Beans para inicializar los datos y cargar los beans en el orden específico. Por lo tanto, usaremos la anotación javax.ejb.DependsOn para definir la dependencia de nuestro bean en otros Session Beans.

El valor de la anotación @DependsOn es una matriz de los nombres de las clases de Bean de los que depende nuestro Bean:

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

Definiremos un método initialize () que inicializa el bean y lo convierte en un método de devolución de llamada de ciclo de vida utilizando la anotación javax.annotation.PostConstruct .

Con esta anotación, el contenedor lo llamará al crear una instancia del bean:

@PostConstruct public void initialize() { List states = new ArrayList(); states.add("Texas"); states.add("Alabama"); states.add("Alaska"); states.add("Arizona"); states.add("Arkansas"); countryStatesMap.put("UnitedStates", states); }

5. Simultaneidad

A continuación, diseñaremos la gestión de concurrencia de Singleton Session Bean. EJB proporciona dos métodos para implementar el acceso concurrente al Singleton Session Bean: concurrencia administrada por contenedor y concurrencia administrada por Bean.

La anotación javax.ejb.ConcurrencyManagement define la política de simultaneidad para un método. De forma predeterminada, el contenedor EJB utiliza la simultaneidad administrada por contenedor.

La anotación @ConcurrencyManagement toma un valor javax.ejb.ConcurrencyManagementType . Las opciones son:

  • ConcurrencyManagementType.CONTAINER para simultaneidad administrada por contenedor.
  • ConcurrencyManagementType.BEAN para simultaneidad gestionada por beans.

5.1. Concurrencia gestionada por contenedor

En pocas palabras, en la concurrencia administrada por contenedores, el contenedor controla cómo los clientes acceden a los métodos.

Vamos a usar la @ConcurrencyManagement anotación con valor javax.ejb.ConcurrencyManagementType.CONTAINER :

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

To specify the access level to each of the singleton’s business methods, we'll use javax.ejb.Lock annotation. javax.ejb.LockType contains the values for the @Lock annotation. javax.ejb.LockType defines two values:

  • LockType.WRITE – This value provides an exclusive lock to the calling client and prevents all other clients from accessing all methods of the bean. Use this for methods that change the state of the singleton bean.
  • LockType.READThis value provides concurrent locks to multiple clients to access a method.

    Use this for methods which only read data from the bean.

With this in mind, we'll define the setStates() method with @Lock(LockType.WRITE) annotation, to prevent simultaneous update of the state by clients.

To allow clients to read the data concurrently, we'll annotate getStates() with @Lock(LockType.READ):

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

To stop the methods execute for a long time and blocking the other clients indefinitely, we'll use the javax.ejb.AccessTimeout annotation to timeout long-waiting calls.

Use the @AccessTimeout annotation to define the number of milliseconds method times-out. After the timeout, the container throws a javax.ejb.ConcurrentAccessTimeoutException and the method execution terminates.

5.2. Bean-Managed Concurrency

In Bean managed concurrency, the container doesn't control simultaneous access of Singleton Session Bean by clients. The developer is required to implement concurrency by themselves.

Unless concurrency is implemented by the developer, all methods are accessible to all clients simultaneously. Java provides the synchronization and volatile primitives for implementing concurrency.

To find out more about concurrency read about java.util.concurrent here and Atomic Variables here.

For bean-managed concurrency, let’s define the @ConcurrencyManagement annotation with the javax.ejb.ConcurrencyManagementType.BEAN value for the Singleton Session Bean class:

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

Next, we'll write the setStates() method which changes the state of the bean using synchronized keyword:

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

The synchronized keyword makes the method accessible by only one thread at a time.

The getStates() method doesn't change the state of the Bean and so it doesn't need to use the synchronized keyword.

6. Client

Now we can write the client to access our Singleton Session Bean.

We can deploy the Session Bean on application container servers like JBoss, Glassfish etc. To keep things simple, we will use the javax.ejb.embedded.EJBContainer class. EJBContainer runs in the same JVM as the client and provides most of the services of an enterprise bean container.

First, we'll create an instance of EJBContainer. This container instance will search and initialize all the EJB modules present in the classpath:

public class CountryStateCacheBeanTest { private EJBContainer ejbContainer = null; private Context context = null; @Before public void init() { ejbContainer = EJBContainer.createEJBContainer(); context = ejbContainer.getContext(); } }

Next, we'll get the javax.naming.Context object from the initialized container object. Using the Context instance, we can get the reference to CountryStateContainerManagedBean and call the methods:

@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 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 actualStates = countryStateBean.getStates("UnitedStates"); assertNotNull(actualStates); assertArrayEquals(expectedStates, actualStates.toArray()); }

Similarly, we can use the Context instance to get the reference for Bean-Managed Singleton Bean and call the respective methods:

@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 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 actualStates = countryStateBean.getStates("UnitedStates"); assertNotNull(actualStates); assertArrayEquals(expectedStates, actualStates.toArray()); }

End our tests by closing the EJBContainer in the close() method:

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

7. Conclusion

Singleton Session Beans are just as flexible and powerful as any standard Session Bean but allow us to apply a Singleton pattern to share state across our application's clients.

Concurrency management of the Singleton Bean could be easily implemented using Container-Managed Concurrency where the container takes care of concurrent access by multiple clients, or you could also implement your own custom concurrency management using Bean-Managed Concurrency.

The source code of this tutorial can be found over on GitHub.