May 25, 2010, 7:23 a.m.
posted by hashspark
The Stateful Session BeanEach stateful session bean is dedicated to one client for the life of the bean instance; it acts on behalf of that client as its agent. Stateful session beans are not swapped among EJB objects nor are they kept in an instance pool, like stateless session bean instances are. Once a stateful session bean is instantiated and assigned to an EJB object, it is dedicated to that EJB object for its entire life cycle.[*]
Stateful session beans maintain conversational state, which means that the instance variables of the bean class can maintain data specific to the client between method invocations. This makes it possible for methods to be interdependent so that changes made to the bean's state in one method call can affect the results of subsequent method invocations. Therefore, every method call from a client must be serviced by the same instance (at least conceptually), so the bean instance's state can be predicted from one method invocation to the next. In contrast, stateless session beans don't maintain client-specific data from one method call to the next, so any instance can be used to service any method call from any client. Although stateful session beans maintain conversational state, they are not themselves persistent, like entity beans are. Entity beans represent data in the database; their persistence fields are written directly to the database. Stateful session beans can access the database but do not represent data in the database. Stateful session beans are often considered extensions of the client. This makes sense if you think of a client as being made up of operations and state. Each task may rely on some information gathered or changed by a previous operation. A GUI client is a perfect example: when you fill in the fields on a GUI client you are creating conversational state. Pressing a button executes an operation that might fill in more fields, based on the information you entered previously. The information in the fields is conversational state. Stateful session beans allow you to encapsulate some of the business logic and conversational state of a client and move it to the server. Moving this logic to the server thins the client application and makes the system as a whole easier to manage. The stateful session bean acts as an agent for the client, managing processes or taskflow to accomplish a set of tasks; it manages the interactions of other beans in addition to direct data access over several operations to accomplish a complex set of tasks. By encapsulating and managing taskflow on behalf of the client, stateful beans present a simplified interface that hides the details of many interdependent operations on the database and other beans from the client. Getting Set Up for the TravelAgent EJBThe TravelAgent EJB will use the Cabin, Cruise, Reservation, and Customer entity beans developed in Chapters 6 and 7. It will coordinate the interaction of these entity beans to book a passenger on a cruise. We'll modify the Reservation entity that was used in Chapter 7 so that it can be created with all of its relationships identified right away. In other words, we will define another constructor in addition to a default constructor:
public class Reservation {
public Reservation( ) {}
public Reservation(Customer customer, Cruise cruise,
Cabin cabin, double price, Date dateBooked) {
setAmountPaid(price);
setDate(dateBooked);
setCruise(cruise);
Set cabins = new HashSet( );
cabins.add(cabin);
this.setCabins(cabins);
Set customers = new HashSet( );
customers.add(customer);
this.setCustomers(customers);
}
Creating this constructor will allow us to avoid calling all of those setter methods within our TravelAgent EJB code and will make it less cluttered. The TravelAgent EJBThe TravelAgent EJB, which we have already seen, is a stateful session bean that encapsulates the process of making a reservation on a cruise. We will develop this bean further to demonstrate how stateful session beans can be used as taskflow objects. We won't develop a local interface for the TravelAgent EJB, partly because it is designed to be used by remote clients (and therefore doesn't require local component interfaces), and partly because the rules for developing local interfaces for stateful session beans are the same as those for stateless session beans. The remote interface: TravelAgentAs a stateful session bean that models taskflow, the TravelAgent EJB manages the interactions among several other beans while maintaining conversational state. Here's the modified travelAgentRemote interface:
package com.titan.travelagent;
import com.titan.processpayment.CreditCardDO;
import javax.ejb.Remote;
import com.titan.domain.Customer;
@Remote
public interface TravelAgentRemote {
public Customer findOrCreateCustomer(String first, String last);
public void updateAddress(Address addr);
public void setCruiseID(int cruise);
public void setCabinID(int cabin);
public TicketDO
bookPassage(CreditCardDO
card, double price)
throws IncompleteConversationalState;
}
The purpose of the TravelAgent EJB is to make cruise reservations. To accomplish this task, the bean needs to know which cruise, cabin, and customer make up the reservation. Therefore, the client using the TravelAgent EJB needs to gather this kind of information before booking the reservation. The TRavelAgentRemote interface provides methods for setting the IDs of the cruise and cabin that the customer wants to book. We can assume that the cabin ID comes from a list and that the cruise ID comes from some other source. The client will pass in the customer's name to the findOrCreateCustomer( ) method. If the customer exists in the database, the TravelAgent EJB will use that customer; otherwise, one will be created. Once the customer, cruise, and cabin are chosen, the TravelAgent EJB is ready to process the reservation. This operation is performed by the bookPassage( ) method, which needs the customer's credit card information and the price of the cruise. bookPassage( ) is responsible for charging the customer's account, reserving the chosen cabin in the right ship on the right cruise, and generating a ticket for the customer. How this is accomplished is not important to us at this point; when we are developing the remote interface, we are concerned only with the business definition of the bean. We will discuss the implementation when we talk about the bean class. Note that the bookPassage( ) method throws an application-specific exception, IncompleteConversationalState. This exception is used to communicate business problems encountered while booking a customer on a cruise. The IncompleteConversationalState exception indicates that the TravelAgent EJB did not have enough information to process the booking. Here's the IncompleteConversationalState class:
package com.titan.travelagent;
public class IncompleteConversationalState extends java.lang.Exception {
public IncompleteConversationalState( ){super( );}
public IncompleteConversationalState(String msg){super(msg);}
}
Domain objects: the TicketDO classLike the CreditCardDO and CheckDO classes used in the ProcessPayment EJB, the TicketDO class is defined as a pass-by-value object. One could argue that a ticket should be the Reservation entity bean, since it is a plain Java object and could be serialized back to the client. However, determining how a business object is used can also dictate whether it should be a bean or simply a class. Because the Reservation entity bean references a lot of interrelated entities, the number of objects serialized back to the client could become quite large, and thus very inefficient. With the TicketDO object, you can pull together the exact information you want to send back to the client. The constructor for TicketDO uses the entities from which it pulls the data:
package com.titan.travelagent;
import com.titan.domain.Cruise;
import com.titan.domain.Cabin;
import com.titan.domain.Customer;
public class TicketDO implements java.io.Serializable {
public int customerID;
public int cruiseID;
public int cabinID;
public double price;
public String description;
public TicketDO(Customer customer, Cruise cruise,
Cabin cabin, double price) {
description = customer.getFirstName( )+
" " + customer.getLastName( ) +
" has been booked for the "
+ cruise.getName( ) +
" cruise on ship " +
cruise.getShip( ).getName( ) + ".\n" +
" Your accommodations include " +
cabin.getName( ) +
" a " + cabin.getBedCount( ) +
" bed cabin on deck level " + cabin.getDeckLevel( ) +
".\n Total charge = " + price;
customerID = customer.getId( );
cruiseID = cruise.getId( );
cabinID = cabin.getId( );
this.price = price;
}
public String toString( ) {
return description;
}
}
Taking a peek at the client viewBefore settling on definitions for your component interfaces, it is a good idea to figure out how clients will use the bean. Imagine that the TravelAgent EJB is used by a Java application with GUI fields. These fields capture the customer's preference for the type of cruise and cabin. We start by examining the code used at the beginning of the reservation process:
Context jndi = getInitialContext( );
Object ref = jndi.lookup("TravelAgentBean/remote");
TravelAgentRemote agent = (TravelAgentRemote)
PortableRemoteObject.narrow(ref, TravelAgentRemote.class);
This code simply looks up the TravelAgent EJB in JNDI. The act of doing this creates a session that is dedicated to the client and is represented by the agent variable.
Customer cust = agent.findOrCreateCustomer(textField_firstName.getText( ),
textField_lastName.getText( ));
This code locates an existing customer or creates a new customer based on information the travel agent gathered over the phone. The act of calling findOrCreateCustomer( ) will store the customer being referenced in the TravelAgent EJB's internal state. The real-life travel agent also gathers any address changes to the client and makes those changes on the server. Address updatedAddress = new Address(textField_street.getText( ), ...); agent.updateAddress(updatedAddress); Next, we gather the cruise and cabin choices from another part of the client application: Integer cruise_id = new Integer(textField_cruiseNumber.getText( )); Integer cabin_id = new Integer( textField_cabinNumber.getText( )); agent.setCruiseID(cruise_id); agent.setCabinID(cabin_id); The travel agent chooses the cruise and cabin the customer wishes to reserve. These IDs are set in the TravelAgent EJB, which maintains the conversational state for the whole process. At the end of the process, the travel agent completes the reservation by processing the booking and generating a ticket. Because the TravelAgent EJB has maintained the conversational state, caching the customer, cabin, and cruise information, only the credit card and price are needed to complete the transaction: String cardNumber = textField_cardNumber.getText( ); Date date = dateFormatter.parse(textField_cardExpiration.getText( )); String cardBrand = textField_cardBrand.getText( ); CreditCardDO card = new CreditCardDO(cardNumber,date,cardBrand); double price = double.valueOf(textField_cruisePrice.getText( )).doubleValue( ); TicketDO ticket = agent.bookPassage(card,price); PrintingService.print(ticket); This summary of how the client will use the TravelAgent EJB confirms that our remote interface is workable. We can now move ahead with development. The bean class: TravelAgentBeanWe can now implement all of the behavior expressed in the new remote interface for the TravelAgent EJB. Here is a partial definition of the new travelAgentBean class:
package com.titan.travelagent;
import com.titan.processpayment.*;
import com.titan.domain.*;
import javax.ejb.*;
import javax.persistence.*;
import javax.annotation.EJB;
import java.util.Date;
@Stateful
public class TravelAgentBean implements TravelAgentRemote {
@PersistenceContext(unitName="titan")
private EntityManager entityManager;
@EJB private ProcessPaymentLocal processPayment;
private Customer customer;
private Cruise cruise;
private Cabin cabin;
public Customer findOrCreateCustomer(String first, String last) {
try {
Query q = entityManager.createQuery("from Customer c where
c.firstName = :first and c.lastName = :last");
q.setParameter("first", first);
q.setParameter("last", last);
this.customer = (Customer)q.getSingleResult( );
} catch (NoResultException notFound) {
this.customer = new Customer( );
this.customer.setFirstName(first);
this.customer.setLastName(last);
entityManager.persist(this.customer);
}
return this.customer;
}
public void updateAddress(Address addr) {
this.customer.setAddress(addr);
this.customer = entityManager.merge(customer);
}
public void setCabinID(int cabinID) {
this.cabin = entityManager.find(Cabin.class, cabinID);
if (cabin == null) throw new NoResultException("Cabin not found");
}
public void setCruiseID(int cruiseID) {
this.cruise = entityManager.find(Cruise.class, cruiseID);
if (cruise == null) throw new NoResultException("Cruise not found");
}
@Remove
public TicketDO bookPassage(CreditCardDO card, double price)
throws IncompleteConversationalState {
if (customer == null || cruise == null || cabin == null)
{
throw new IncompleteConversationalState( );
}
try {
Reservation reservation = new Reservation(
customer, cruise, cabin, price, new Date( ));
entityManager.persist(reservation);
processPayment.byCredit(customer, card, price);
TicketDO ticket = new TicketDO(customer, cruise, cabin, price);
return ticket;
} catch(Exception e) {
throw new EJBException(e);
}
}
}
This is a lot of code to digest, so we will approach it in small pieces.
@Stateful
public class TravelAgentBean implements TravelAgentRemote {
@PersistenceContext(unitName="titan")
private EntityManager entityManager;
@EJB private ProcessPaymentLocal processPayment;
The TravelAgent EJB needs a reference to Titan's entity manager service so that it can find, update, and create the entity beans that are needed. The @javax.persistence.PersistenceContext annotation causes the EJB container to initialize the entityManager field for this purpose. The ProcessPayment EJB is also needed to be able to set up a credit card payment. The @javax.ejb.EJB annotation is used to pull in a reference to this stateless session bean in much the same way as the @Resource annotation was used to initialize its datasource field in the ProcessPayment EJB's bean class. Chapter 14 discusses the semantics of these annotations in detail. Next, let's examine the findOrCreateCustomer( ) method:
public Customer findOrCreateCustomer(String first, String last) {
try {
Query q = entityManager.createQuery("select c " +
+ "from Customer c "
+ "where c.firstName = :first and c.lastName = :last");
q.setParameter("first", first);
q.setParameter("last", last);
this.customer = (Customer)q.getSingleResult( );
} catch (NoResultException
notFound) {
this.customer = new Customer( );
this.customer.setFirstName(first);
this.customer.setLastName(last);
entityManager.persist(this.customer);
}
return this.customer;
}
The findOrCreateCustomer( ) method dynamically creates a query to search for an existing customer based on the first- and last-name parameters passed in. The Query.getSingleResult( ) method throws a javax.persistence.NoResultException . If thrown, this exception tells us that a brand-new Customer entity must be created.
public void updateAddress(Address addr) {
this.customer.setAddress(addr);
this.customer = entityManager.merge(customer);
}
The updateAddress( ) method simply synchronizes the address changes made on the client into the database. The customer field is no longer managed by the persistence context, as it was initialized separately in the findOrCreateCustomer( ) method and detached when that method finished. Since the Customer instance is detached, the EntityManager.merge( ) method is used to update changes made to the customer's address. The TravelAgent EJB has methods for setting the desired cruise and cabin. These methods take int IDs as arguments and retrieve references to the appropriate Cruise or Cabin entity from the injected EntityManager. These references are also part of the TravelAgent EJB's conversational state. Here's how setCabinID( ) and getCabinID( ) are defined:
public void setCabinID(int cabinID) {
this.cabin = entityManager.find(Cabin.class, cabinID);
if (cabin == null) throw new NoResultException("Cabin not found");
}
public void setCruiseID(int cruiseID) {
this.cruise = entityManager.find(Cruise.class, cruiseID);
if (cruise == null) throw new NoResultException("Cruise not found");
}
We look up the Cabin and Cruise entities via the EntityManager.find( ) method. If this method returns null, we throw a NoResultException back to the client so that it is notified that the Cabin or Cruise entity is no longer valid. It may seem strange that we set these values using int IDs, but we keep them in the conversational state as entity bean references. Using int IDs is simpler for the client, which does not work with their entity bean references. In the client code, we get the cabin and cruise IDs from text fields. Why make the client obtain a bean reference to the Cruise and Cabin entities when an ID is simpler? Also, we could have waited until the bookPassage( ) method was invoked before reconstructing the remote references, but this strategy keeps the bookPassage( ) method simple. The bookPassage( ) methodThe last point of interest in our bean definition is the bookPassage( ) method. This method uses the conversational state accumulated by the findOrCreateCustomer( ), setCabinID( ), and setCruiseID( ) methods to process a reservation for a customer. Here's how the bookPassage( ) method is defined: @Remove public TicketDO bookPassage(CreditCardDO card, double price) throws IncompleteConversationalState { if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState( ); } try { Reservation reservation = new Reservation( customer, cruise, cabin, price, new Date( )); entityManager.persist(reservation); process.byCredit(customer, card, price); TicketDO ticket = new TicketDO(customer, cruise, cabin, price); return ticket; } catch(Exception e) { throw new EJBException(e); } } This method demonstrates the taskflow concept. It uses several beans, including the ProcessPayment EJB and the Reservation, Customer, Cabin, and Cruise entities, to accomplish one task: booking a customer on a cruise. Deceptively simple, this method encapsulates several interactions that ordinarily might have been performed on the client. For the price of one bookPassage( ) call from the client, the TravelAgent EJB performs the following operations, in this order:
Notice that the bookPassage( ) method is annotated with the @javax.ejb.Remove annotation. The @Remove annotation tells the EJB container that when the method completes, the client no longer needs the session. After the bookPassage( ) method completes, the EJB container removes the session from the EJB container. The TravelAgent EJB is now complete. We've learned that from a design standpoint, encapsulating the taskflow in a stateful session bean means a less complex interface for the client and more flexibility for implementing changes. We could easily change bookPassage( ) to check for overlapped bookings (when a customer books passage on two cruises with overlapping dates). This type of enhancement does not change the remote interface, so the client application does not need modification. Encapsulating taskflow in stateful session beans allows the system to evolve without impacting clients. In addition, the type of clients used can change. One of the biggest problems with two-tier architecturesbesides scalability and transactional controlis that the business logic is intertwined with the client logic. As a result, it is difficult to reuse the business logic in a different kind of client. With stateful session beans, this is not a problem, because stateful session beans are an extension of the client but are not bound to the client's presentation. Let's say that our first implementation of the reservation system used a Java applet with GUI widgets. The TravelAgent EJB would manage conversational state and perform all the business logic while the applet focused on the GUI presentation. If, at a later date, we decide to go to a thin client (HTML generated by a Java servlet, for example), we would simply reuse the TravelAgent EJB in the servlet. Because all the business logic is in the stateful session bean, the presentation (Java applet or servlet or something else) can change easily. The TravelAgent EJB also provides transactional integrity for processing the customer's reservation. If any of the operations within the body of the bookPassage( ) method fails, all the operations are rolled back so that none of the changes is accepted. If the credit card cannot be charged by the ProcessPayment EJB, the newly created Reservation EJB and its associated record are not created. The transactional aspects of the TravelAgent EJB are explained in detail in Chapter 16. The remote and local EJB references can be used within the same taskflow. For example, the bookPassage( ) method uses a local reference when accessing the ProcessPayment EJB. This usage is totally appropriate. The EJB container ensures that the transaction is atomici.e., that failures in either the remote or the local EJB reference will affect the entire transaction. The XML Deployment DescriptorHere is a deployment descriptor that provides a complete annotation-alternative definition of the TravelAgent EJB:
<?xml version="1.0"?>
<ejb-jar
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"
version="3.0">
<enterprise-beans>
<session>
<ejb-name>TravelAgentBean</ejb-name>
<remote>com.titan.travelagent.TravelAgentRemote</remote>
<ejb-class>com.titan.travelagent.TravelAgentBean</ejb-class>
<session-type>Stateful
</session-type>
<ejb-local-ref>
<ejb-ref-name>ejb/PaymentProcessor</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<local>com.titan.processpayment.ProcessPaymentLocal</local>
<injection-target>
<injection-target-class>
com.titan.travelagent.TravelAgentBean
</injection-target-class>
<injection-target-name>processPayment
</injection-target-name>
</injection-target>
</ejb-local-ref>
<persistence-context-ref>
<persistence-context-ref-name>
persistence/titan
</persistence-context-ref-name>
<persistence-unit-name>titan</persistence-unit-name>
<injection-target>
<injection-target-class>
com.titan.travelagent.TravelAgentBean
</injection-target-class>
<injection-target-name>entityManager</injection-target-name>
</injection-target>
</persistence-context-ref>
</session>
</enterprise-beans>
</ejb-jar>
There is one minor difference between the stateful session bean syntax and that for stateless beans. The <session-type> element is set to Stateful rather than Stateless . You also see that we are using <ejb-local-ref> and <persistence-context-ref> to initialize the processPayment and entityManager fields of the bean class. These elements are explained in more detail in Chapter 14. |
- Comment
