Java EE Stateful Session Bean (EJB) example
Introduction
Stateful Session Beans - as the name states (and opposite to Stateless Session Beans) - are able to keep state across multiple calls from a given client during an established session.
A Stateful EJB is usually coupled with a single client representing a conversation, where the EJB keeps the conversational state. Naturally many instances of a given Stateful EJB may exist simultaneously if we also have multiple simultaneous clients: Each EJB will be coupled to a single client.
The EJB container is responsible to manage the Statefull EJB lifecycle as we will see further in this tutorial. An usual real-life example of a Stateful EJB usage relies in the implementation of a web application's shopping cart and that's exactly what we will do in this example.
This tutorial considers the following environment:
- Ubuntu 12.04
- JDK 1.7.0.21
- Glassfish 4.0
A Product entity
If we are building a shopping cart we will need to have Products, so we will start by defining a simple class representing a product:
package com.byteslounge.model; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "PRODUCT") public class Product implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ID_PRODUCT", nullable = false) private int id; @Column(name = "DESCRIPTION", nullable = false) private String description; public int getId() { return id; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
If you are familiar with JPA you have already noticed that this Product class is defined as a JPA entity. This entity is mapped against the following table:
ID_PRODUCT INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
DESCRIPTION VARCHAR (32) NOT NULL
);
We are keeping the Product class very simple as this is not the focus of the tutorial. Next step is to create the Stateful Session Bean.
The Stateful Session Bean
Now it's time to define the main component of this tutorial: The Stateful EJB that represents the shopping cart.
package com.byteslounge.ejb; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import javax.annotation.PostConstruct; import javax.ejb.Stateful; import javax.ejb.StatefulTimeout; import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import com.byteslounge.model.Product; @Stateful @StatefulTimeout(unit = TimeUnit.MINUTES, value = 30) public class ShoppingCartBean implements ShoppingCart { @PersistenceContext private EntityManager em; private List<Product> products; @PostConstruct private void init(){ products = new ArrayList<>(); } @Override public void addProduct(Product product){ products.add(product); } @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public void completePurchase(){ for(Product product : products){ em.persist(product); } products.clear(); } }
We define an EJB as stateful by annotating the bean class with @Stateful. We also defined the Stateful EJB timeout with @StatefulTimeout annotation. If the EJB is to be used in a web application context the timeout should be synchronized with the HTTP session timeout.
Basically we have a product list that will be used to store the products we will be adding to the shopping cart (method addProduct is used to add a product to the cart). Method init is used to initialize the product list. Note that this method is annotated with @PostConstruct so it will be called by the EJB container during bean initialization.
Finally we are injecting the Entity Manager that will be used to persist the products list when the order is finished (method completePurchase).
The Stateless EJB is implementing a simple Local interface:
package com.byteslounge.ejb; import javax.ejb.Local; import com.byteslounge.model.Product; @Local public interface ShoppingCart { void addProduct(Product product); void completePurchase(); }
A testing Servlet
We will now use a Servlet to handle simulated HTTP requests made to a web application that represent the action of adding products to the shopping cart (and eventually complete the purchase). The Servlet may be defined as the following:
package com.byteslounge.servlet; import java.io.IOException; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.byteslounge.ejb.ShoppingCart; import com.byteslounge.model.Product; @WebServlet(name = "shoppingServlet", urlPatterns = {"/shopping"}) public class ShoppingServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final String SHOPPING_CART_BEAN_SESION_KEY = "shoppingCart"; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Obtain the EJB from the HTTP session ShoppingCart shoppingCartBean = (ShoppingCart) request.getSession() .getAttribute(SHOPPING_CART_BEAN_SESION_KEY); if(shoppingCartBean == null){ // EJB is not present in the HTTP session // so let's fetch a new one from the container try { InitialContext ic = new InitialContext(); shoppingCartBean = (ShoppingCart) ic.lookup("java:global/com-byteslounge/com-bytesloungeejb/ShoppingCartBean"); // put EJB in HTTP session for future servlet calls request.getSession().setAttribute( SHOPPING_CART_BEAN_SESION_KEY, shoppingCartBean); } catch (NamingException e) { throw new ServletException(e); } } String productName = request.getParameter("product"); if(productName != null && productName.length() > 0){ // Product is present in the HTTP request. // Let's add it to the shopping cart Product product = new Product(); product.setDescription(productName); shoppingCartBean.addProduct(product); } String persist = request.getParameter("complete"); if(persist != null && persist.equalsIgnoreCase("yes")){ // Request instructs to complete the purchase shoppingCartBean.completePurchase(); } } }
The comments present in the Servlet are almost self-explanatory. When a request is made against the Servlet we check if we already have a shopping cart EJB instance stored in the HTTP session. If it's not yet present we fetch a new EJB instance from the EJB container and store it in the user's HTTP session for future usage.
Now we may issue requests against the servlet using the parameter product=[PRODUCT_NAME] to add products to the shopping cart and the parameter complete=yes when we decide to complete the order.
The local EJB will be available under the Portable Global JNDI name of:
java:global/com-byteslounge/com-bytesloungeejb/ShoppingCartBean
You should be able to find the global JNDI name in your EJB container's startup log.
Testing
After deploying our application we may start issuing requests to add products to the shopping cart:
http://localhost:8080/byteslounge/shopping?product=dvd
http://localhost:8080/byteslounge/shopping?product=book
After adding a dvd and a book to the shopping cart we complete the purchase:
http://localhost:8080/byteslounge/shopping?complete=yes
Now let's list the PRODUCT table contents:
As we can see both of the products were successfully persisted into the database.
If we use more than a single web browser and keep adding products to the cart simultaneously - and eventually finalizing the orders - we will observe that each client will have it's own Stateful EJB instance and consequently it's own shopping cart view.
Downloadable sample
The downloadable sample available at the end of this page is a complete Maven project where everything is already configured and ready to be deployed in an Enterprise Container. We used Glassfish in our case.
If you are using the sample as-is don't forget to configure the datasource in your container and/or modifying the supplied persistence unit configuration (persistence.xml) if needed.