Singleton Session Bean a Jakarta EE

1. Panoramica

Ogni volta che una singola istanza di un Session Bean è richiesta per un dato caso d'uso, possiamo usare un Singleton Session Bean.

In questo tutorial, esploreremo questo attraverso un esempio, con un'applicazione Jakarta EE.

2. Maven

Prima di tutto, dobbiamo definire le dipendenze Maven richieste nel pom.xml .

Definiamo le dipendenze per le API EJB e il contenitore EJB incorporato per la distribuzione di EJB:

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

Le versioni più recenti possono essere trovate su Maven Central in JavaEE API e tomEE.

3. Tipi di fagioli di sessione

Esistono tre tipi di Session Beans. Prima di esplorare Singleton Session Beans, vediamo qual è la differenza tra i cicli di vita dei tre tipi.

3.1. Stateful Session Beans

Un bean di sessione con stato mantiene lo stato di conversazione con il client che sta comunicando.

Ogni client crea una nuova istanza di Stateful Bean e non viene condivisa con altri client.

Quando la comunicazione tra il client e il bean termina, termina anche il bean di sessione.

3.2. Fagioli di sessione senza stato

Uno Stateless Session Bean non mantiene alcuno stato di conversazione con il client. Il bean contiene lo stato specifico del client solo fino alla durata dell'invocazione del metodo.

I richiami dei metodi consecutivi sono indipendenti a differenza del bean di sessione con stato.

Il contenitore mantiene un pool di Stateless Beans e queste istanze possono essere condivise tra più client.

3.3. Singleton Session Beans

Un Singleton Session Bean mantiene lo stato del bean per l'intero ciclo di vita dell'applicazione.

I singleton Session Bean sono simili agli Stateless Session Bean, ma solo un'istanza del Singleton Session Bean viene creata nell'intera applicazione e non termina finché l'applicazione non viene chiusa.

La singola istanza del bean è condivisa tra più client ed è possibile accedervi contemporaneamente.

4. Creazione di un Singleton Session Bean

Cominciamo creando un'interfaccia per questo.

Per questo esempio, utilizziamo l' annotazione javax.ejb.Local per definire l'interfaccia:

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

L'uso di @Local significa che si accede al bean all'interno della stessa applicazione. Abbiamo anche la possibilità di utilizzare l' annotazione javax.ejb.Remote che ci consente di chiamare l'EJB da remoto.

Ora definiremo la classe bean EJB di implementazione. Contrassegniamo la classe come Singleton Session Bean utilizzando l'annotazione javax .ejb.Singleton .

Inoltre, contrassegniamo anche il bean con l' annotazione javax .ejb.Startup per informare il contenitore EJB di inizializzare il bean all'avvio:

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

Questa è chiamata inizializzazione desiderosa. Se non usiamo @Startup , il contenitore EJB determina quando inizializzare il bean.

Possiamo anche definire più Session Beans per inizializzare i dati e caricare i bean nell'ordine specifico. Pertanto, useremo l' annotazione javax.ejb.DependsOn per definire la dipendenza del nostro bean da altri Session Beans.

Il valore per l' annotazione @DependsOn è un array dei nomi dei nomi delle classi Bean da cui dipende il nostro Bean:

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

Definiremo un metodo initialize () che inizializza il bean e lo rende un metodo di callback del ciclo di vita utilizzando l' annotazione javax.annotation.PostConstruct .

Con questa annotazione, verrà chiamata dal contenitore all'istanza 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. Concorrenza

Successivamente, progetteremo la gestione della concorrenza di Singleton Session Bean. EJB fornisce due metodi per implementare l'accesso simultaneo a Singleton Session Bean: concorrenza gestita dal contenitore e concorrenza gestita dal bean.

L'annotazione javax.ejb.ConcurrencyManagement definisce la politica di concorrenza per un metodo. Per impostazione predefinita, il contenitore EJB utilizza la concorrenza gestita dal contenitore.

L' annotazione @ConcurrencyManagement accetta un valore javax.ejb.ConcurrencyManagementType . Le opzioni sono:

  • ConcurrencyManagementType.CONTAINER per la concorrenza gestita dal contenitore.
  • ConcurrencyManagementType.BEAN per la concorrenza gestita dal bean.

5.1. Concorrenza gestita dal contenitore

In poche parole, nella concorrenza gestita dal contenitore, il contenitore controlla il modo in cui i client accedono ai metodi.

Usiamo l' annotazione @ConcurrencyManagement con valore 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.