Singleton Session Bean à Jakarta EE

1. Vue d'ensemble

Chaque fois qu'une seule instance d'un Session Bean est requise pour un cas d'utilisation donné, nous pouvons utiliser un Singleton Session Bean.

Dans ce didacticiel, nous allons explorer cela à travers un exemple, avec une application Jakarta EE.

2. Maven

Tout d'abord, nous devons définir les dépendances Maven requises dans le fichier pom.xml .

Définissons les dépendances pour les API EJB et le conteneur EJB intégré pour le déploiement de l'EJB:

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

Les dernières versions peuvent être trouvées sur Maven Central à JavaEE API et tomEE.

3. Types de session Beans

Il existe trois types de Session Beans. Avant d'explorer les Singleton Session Beans, voyons quelle est la différence entre les cycles de vie des trois types.

3.1. Beans session avec état

Un bean session avec état maintient l'état conversationnel avec le client qu'il communique.

Chaque client crée une nouvelle instance de Stateful Bean et n'est pas partagée avec d'autres clients.

Lorsque la communication entre le client et le bean se termine, le bean de session se termine également.

3.2. Beans session sans état

Un bean de session sans état ne conserve aucun état conversationnel avec le client. Le bean contient l'état spécifique au client uniquement jusqu'à la durée de l'appel de la méthode.

Les invocations de méthode consécutives sont indépendantes contrairement au Stateful Session Bean.

Le conteneur gère un pool de Stateless Beans et ces instances peuvent être partagées entre plusieurs clients.

3.3. Haricots de session singleton

Un bean session singleton conserve l'état du bean pendant tout le cycle de vie de l'application.

Les beans session singleton sont similaires aux beans session sans état, mais une seule instance du bean session singleton est créée dans toute l'application et ne se termine pas tant que l'application n'est pas arrêtée.

L'instance unique du bean est partagée entre plusieurs clients et peut être accédée simultanément.

4. Création d'un bean de session singleton

Commençons par créer une interface pour cela.

Pour cet exemple, utilisons l' annotation javax.ejb.Local pour définir l'interface:

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

L'utilisation de @Local signifie que le bean est accessible dans la même application. Nous avons également la possibilité d'utiliser l' annotation javax.ejb.Remote qui nous permet d'appeler l'EJB à distance.

Nous allons maintenant définir la classe de bean EJB d'implémentation. Nous marquons la classe comme un bean de session singleton en utilisant l'annotation javax .ejb.Singleton .

De plus, marquons également le bean avec l' annotation javax .ejb.Startup pour informer le conteneur EJB d'initialiser le bean au démarrage:

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

C'est ce qu'on appelle une initialisation hâtive. Si nous n'utilisons pas @Startup , le conteneur EJB détermine quand initialiser le bean.

Nous pouvons également définir plusieurs Session Beans pour initialiser les données et charger les beans dans l'ordre spécifique. Par conséquent, nous utiliserons l' annotation javax.ejb.DependsOn pour définir la dépendance de notre bean à d'autres Session Beans.

La valeur de l' annotation @DependsOn est un tableau des noms des noms de classe Bean dont dépend notre Bean:

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

Nous allons définir une méthode initialize () qui initialise le bean, et en fait une méthode de rappel du cycle de vie en utilisant l' annotation javax.annotation.PostConstruct .

Avec cette annotation, il sera appelé par le conteneur lors de l'instanciation du 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. Concurrence

Ensuite, nous allons concevoir la gestion de la concurrence de Singleton Session Bean. EJB fournit deux méthodes pour implémenter l'accès simultané au bean de session Singleton: la concurrence gérée par conteneur et la concurrence gérée par le bean.

L'annotation javax.ejb.ConcurrencyManagement définit la stratégie de concurrence pour une méthode. Par défaut, le conteneur EJB utilise la concurrence gérée par le conteneur.

L' annotation @ConcurrencyManagement prend une valeur javax.ejb.ConcurrencyManagementType . Les options sont:

  • ConcurrencyManagementType.CONTAINER pour l'accès concurrentiel géré par le conteneur.
  • ConcurrencyManagementType.BEAN pour la concurrence gérée par bean.

5.1. Accès concurrentiel géré par conteneur

En termes simples, dans la concurrence gérée par conteneur, le conteneur contrôle l'accès des clients aux méthodes.

Utilisons l' annotation @ConcurrencyManagement avec la valeur 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.