1. Overview

Whenever a single instance of a Session Bean is required for a given use-case, we can use a Singleton Session Bean.

In this tutorial, we’re going to explore the this through an example, with a Jakarta EE application.

2. Maven

First of all, we need to define required Maven dependencies in the pom.xml.

Let’s define the dependencies for the EJB APIs and Embedded EJB container for deployment of the 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>

Latest versions can be found on Maven Central at JavaEE API and tomEE.

3. Types of Session Beans

There are three types of Session Beans. Before we explore Singleton Session Beans, let’s see what is the difference between the lifecycles of the three types.

3.1. Stateful Session Beans

A Stateful Session Bean maintains the conversational state with the client it is communicating.

Each client creates a new instance of Stateful Bean and is not shared with other clients.

When the communication between the client and bean ends, the Session Bean also terminates.

3.2. Stateless Session Beans

A Stateless Session Bean doesn’t maintain any conversational state with the client. The bean contains the state specific to the client only till the duration of method invocation.

Consecutive method invocations are independent unlike with the Stateful Session Bean.

The container maintains a pool of Stateless Beans and these instances can be shared between multiple clients.

3.3. Singleton Session Beans

A Singleton Session Bean maintains the state of the bean for the complete lifecycle of the application.

Singleton Session Beans are similar to Stateless Session Beans but only one instance of the Singleton Session Bean is created in the whole application and does not terminates until the application is shut down.

The single instance of the bean is shared between multiple clients and can be concurrently accessed.

4. Creating a Singleton Session Bean

Let’s start by creating an interface for it.

For this example, let’s use the javax.ejb.Local annotation to define the interface:

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

Using @Local means the bean is accessed within the same application. We also have the option to use javax.ejb.Remote annotation which allows us to call the EJB remotely.

Now, we’ll define the implementation EJB bean class. We mark the class as a Singleton Session Bean by using the annotation javax*.ejb.Singleton*.

In addition, let’s also mark the bean with the javax*.ejb.Startup* annotation to inform the EJB container to initialize the bean at the startup:

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

This is called eager initialization. If we don’t use @Startup, the EJB container determines when to initialize the bean.

We can also define multiple Session Beans to initialize the data and load the beans in the specific order. Therefore, we’ll use, javax.ejb.DependsOn annotation to define our bean’s dependency on other Session Beans.

The value for the @DependsOn annotation is an array of the names of Bean class names that our Bean depends on:

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

We’ll define an initialize() method which initializes the bean, and makes it a lifecycle callback method using javax.annotation.PostConstruct annotation.

With this annotation, it’ll be called by the container upon instantiation of the bean:

@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. Concurrency

Next, we’ll design the concurrency management of Singleton Session Bean. EJB provides two methods for implementing concurrent access to the Singleton Session Bean: Container-managed concurrency, and Bean-managed concurrency.

The annotation javax.ejb.ConcurrencyManagement defines the concurrency policy for a method. By default, the EJB container uses container-managed concurrency.

The @ConcurrencyManagement annotation takes a javax.ejb.ConcurrencyManagementType value. The options are:

  • ConcurrencyManagementType.CONTAINER for container-managed concurrency.
  • ConcurrencyManagementType.BEAN for bean-managed concurrency.

5.1. Container-Managed Concurrency

Simply put, in container-managed concurrency, the container controls how clients’ access to methods.

Let’s use the @ConcurrencyManagement annotation with value 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.READ This 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<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);
    }
}

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

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

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.


« 上一篇: Spring @Order 注解
» 下一篇: Java Collection 教程