Stateful session beans offer an alternative that lies between entity beans and stateless session beans. Stateful session beans are dedicated to one client for the life of the bean instance; a stateful session bean acts on behalf of a client as its agent. They are not swapped among EJB objects or kept in an instance pool like entity and stateless bean instances. 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.[30]
Stateful session beans maintain conversational state, which means that the instance variables of the bean class can cache data relative to the client between method invocations. This makes it possible for methods to be interdependent, so that changes made by methods to the bean’s state can affect the result of subsequent method invocations. In contrast, the stateless session beans we have been talking about do not maintain conversational state. Although stateless beans may have instance variables, these fields are not specific to one client. A stateless instance is swapped among many EJB objects, so you can’t predict which instance will service a method call. With stateful session beans, every method call from a client is serviced by the same instance (at least conceptually), so the bean instance’s state can be predicted from one method invocation to the next.
Although stateful session beans maintain conversational state, they are not themselves persistent like entity beans. Entity beans represent data in the database; their persistent fields are written directly to the database. Stateful session beans, like stateless beans, can access the database but do not represent data in the database. In addition, session beans are not used concurrently like entity beans. If you have an entity EJB object that wraps an instance of the ship called Paradise, for example, all client requests for that ship will be coordinated through the same EJB object.[31] With session beans, the EJB object is dedicated to one client—session beans are not used concurrently.
Stateful session beans are often thought of as 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 the business logic and the 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 workflow 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 workflow 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.
The TravelAgent bean, 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 workflow objects.
In Chapter 4, we
developed an early version of the TravelAgent
interface that contained a single business method,
listCabins()
. We are going to remove the
listCabins()
method and redefine the TravelAgent
bean so that it behaves like a workflow object. Later in the chapter,
we will add a modified listing method for obtaining a more specific
list of cabins for the user.
As a stateful session bean that models
workflow, TravelAgent manages the interactions of several other beans
while maintaining conversational state. The following code contains
the modified TravelAgent
interface:
package com.titan.travelagent;
import java.rmi.RemoteException;
import javax.ejb.FinderException;
import com.titan.cruise.Cruise;
import com.titan.customer.Customer;
import com.titan.processpayment.CreditCard;
public interface TravelAgent
extends javax.ejb.EJBObject {
public void setCruiseID(int cruise)
throws RemoteException, FinderException;
public int getCruiseID() throws RemoteException,
IncompleteConversationalState;
public void setCabinID(int cabin)
throws RemoteException, FinderException;
public int getCabinID() throws RemoteException,
IncompleteConversationalState;
public int getCustomerID() throws RemoteException,
IncompleteConversationalState;
public Ticket bookPassage(CreditCard card, double price)
throws RemoteException,IncompleteConversationalState;
}
The purpose of the TravelAgent bean 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 bean needs to gather this kind of information before
making the booking. The TravelAgent
remote
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 came from a list and that the cruise ID came from some other
source. The customer is set in the create()
method
of the home interface—more about this later.
Once the customer, cruise, and cabin are chosen, the TravelAgent bean
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 only concerned with
the business definition of the bean. We will discuss the
implementation when we talk about the bean class.
Like the CreditCard
and Check
classes used in the ProcessPayment bean, the
Ticket
class that bookPassage()
returns is defined as a pass-by-value object. It can be argued that a
ticket should be an entity bean since it is not dependent and may be
accessed outside the context of the TravelAgent bean. However,
determining how a business object is used can also dictate whether it
should be a bean or simply a class. The Ticket
object, for example, could be digitally signed and emailed to the
client as proof of purchase. This wouldn’t be feasible if the
Ticket
object had been an entity bean. Enterprise
beans are only referenced through their remote interfaces and are not
passed by value, as are serializable objects such as
Ticket
, CreditCard
, and
Check
. As an exercise in pass-by-value, we define
the Ticket
as a simple serializable object instead
of a bean:
package com.titan.travelagent; import com.titan.cruise.Cruise; import com.titan.cabin.Cabin; import com.titan.customer.Customer; import java.rmi.RemoteException; public class Ticket implements java.io.Serializable { public int cruiseID; public int cabinID; public double price; public String description; public Ticket(Customer customer, Cruise cruise, Cabin cabin, double price) throws javax.ejb.FinderException, RemoteException, javax.naming.NamingException { description = customer.getFirstName()+" "+customer.getMiddleName()+ " " + customer.getLastName() + " has been booked for the " + cruise.getName().trim() + " cruise on ship " + cruise.getShipID() + ". " + " Your accommodations include " + cabin.getName().trim() + " a " + cabin.getBedCount() + " bed cabin on deck level " + cabin.getDeckLevel() + ". Total charge = " + price; } public String toString() { return description; } }
Note that the
bookPassage()
method throws an
application-specific exception, the
IncompleteConversationalState
. This exception is
used to communicate business problems encountered while booking a
customer on a cruise. The
IncompleteConversationalState
exception indicates
that the TravelAgent bean didn’t have enough information to
process the booking. The
IncompleteConversationalState
application exception class is
defined below:
package com.titan.travelagent; public class IncompleteConversationalState extends java.lang.Exception { public IncompleteConversationalState(){super();} public IncompleteConversationalState(String msg){super(msg);} }
Starting with the
TravelAgentHome
interface that we developed in
Chapter 4, we can modify the
create()
method to take a remote reference to the
customer who is making the reservation:
package com.titan.travelagent; import java.rmi.RemoteException; import javax.ejb.CreateException; import com.titan.customer.Customer; public interface TravelAgentHome extends javax.ejb.EJBHome { public TravelAgent create(Customer cust) throws RemoteException, CreateException; }
The
create()
method in this home interface requires
that a remote reference to a Customer bean be used to create the
TravelAgent bean. Because there are no other
create()
methods, you can’t create a
TravelAgent bean if you don’t know who the customer is. The
Customer bean reference provides the TravelAgent bean with some of
the conversational state it will need to process the
bookPassage()
method.
Before settling on definitions for your remote interface and home interface, it is a good idea to figure out how the bean will be used by clients. Imagine that the TravelAgent bean is used by a Java applet with GUI fields. The GUI 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 jndiContext = getInitialContext();
// EJB 1.0: Use native cast instead of narrow()
Object ref = jndiContext.lookup("CustomerHome");
CustomerHome customerHome =(CustomerHome)
PortableRemoteObject.narrow(ref, CustomerHome.class);
String ln = tfLastName.getText();
String fn = tfFirstName.getText();
String mn = tfMiddleName.getText();
Customer customer = customerHome.create(nextID, ln, fn, mn);
// EJB 1.0: Use native cast instead of narrow()
ref = jndiContext.lookup("TravelAgentHome");
TravelAgentHome home = (TravelAgentHome)
PortableRemoteObject.narrow(ref, TravelAgentHome.class);
TravelAgent agent = home.create(customer);
This snippet of code creates a new Customer bean based on information
the travel agent gathered over the phone. The
Customer
reference is then used to create a
TravelAgent bean. Next, we gather the cruise and cabin choices from
another part of the applet:
int cruise_id = Integer.parseInt(textField_cruiseNumber.getText()); int cabin_id = Integer.parseInt(textField_cabinNumber.getText()); agent.setCruiseID(cruise_id); agent.setCabinID(cabin_id);
The user chooses the cruise and cabin that the customer wishes to reserve. These IDs are set in the TravelAgent bean, 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 bean has maintained the conversational state, caching the
Customer
, Cabin
, and
Cruise
information, only the credit card and price
are needed to complete the transaction:
long cardNumber = Long.parseLong(textField_cardNumber.getText()); Date date = dateFormatter.format(textField_cardExpiration.getText()); String cardBrand = textField_cardBrand.getText(); CreditCardcard
= new CreditCard(cardNumber,date,cardBrand); doubleprice
= double.valueOf(textField_cruisePrice.getText()).doubleValue(); Ticket ticket = agent.bookPassage(card,price); PrintingService.print(ticket);
We can now move ahead with development; this summary of how the client will use the TravelAgent bean confirms that our remote interface and home interface definitions are workable.
We now implement all the behavior
expressed in the new remote interface and home interface for the
TravelAgent bean. Here is a partial definition of the new
TravelAgentBean
; the getHome()
method and, for EJB 1.0, the getJNDIContext()
method will be added as we go along:[32]
package com.titan.travelagent; import com.titan.cabin.*; import com.titan.cruise.*; import com.titan.customer.*; import com.titan.processpayment.*; import com.titan.reservation.*; import java.sql.*; import javax.sql.DataSource; import java.util.Vector; import java.rmi.RemoteException; import javax.naming.NamingException; import javax.ejb.EJBException; public class TravelAgentBean implements javax.ejb.SessionBean { public Customer customer; public Cruise cruise; public Cabin cabin; public javax.ejb.SessionContext ejbContext; // EJB 1.0: jndiContext should be declared transient public javax.naming.Context jndiContext; public void ejbCreate(Customer cust) { customer = cust; } public int getCustomerID() throws IncompleteConversationalState { // EJB 1.0: also throws RemoteException try { // EJB 1.0: remove try/catch if (customer == null) throw new IncompleteConversationalState(); return ((CustomerPK)customer.getPrimaryKey()).id; } catch(RemoteException re) { throw new EJBException(re); } } public int getCruiseID() throws IncompleteConversationalState { // EJB 1.0: also throws RemoteException try { // EJB 1.0: remove try/catch if (cruise == null) throw new IncompleteConversationalState(); return ((CruisePK)cruise.getPrimaryKey()).id; } catch(RemoteException re) { throw new EJBException(re); } } public int getCabinID() throws IncompleteConversationalState { // EJB 1.0: also throws RemoteException try { // EJB 1.0: remove try/catch if (cabin==null) throw new IncompleteConversationalState(); return ((CabinPK)cabin.getPrimaryKey()).id; } catch(RemoteException re) { throw new EJBException(re); } } public void setCabinID(int cabinID) throws javax.ejb.FinderException { // EJB 1.0: also throws RemoteException try { // EJB 1.0: remove try/catch CabinHome home = (CabinHome)getHome("CabinHome",CabinHome.class); CabinPK pk = new CabinPK(); pk.id=cabinID; cabin = home.findByPrimaryKey(pk); } catch(RemoteException re) { throw new EJBException(re); } } public void setCruiseID(int cruiseID) throws javax.ejb.FinderException { // EJB 1.0: also throws RemoteException try { // EJB 1.0: remove try/catch CruiseHome home = (CruiseHome)getHome("CruiseHome", CruiseHome.class); cruise = home.findByPrimaryKey(new CruisePK(cruiseID)); } catch(RemoteException re) { throw new EJBException(re); } } public Ticket bookPassage(CreditCard card, double price) throws IncompleteConversationalState { // EJB 1.0: also throws RemoteException if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState(); } try { ReservationHome resHome = (ReservationHome) getHome("ReservationHome",ReservationHome.class); Reservation reservation = resHome.create(customer, cruise, cabin, price); ProcessPaymentHome ppHome = (ProcessPaymentHome) getHome("ProcessPaymentHome",ProcessPaymentHome.class); ProcessPayment process = ppHome.create(); process.byCredit(customer, card, price); Ticket ticket = new Ticket(customer,cruise,cabin,price); return ticket; } catch(Exception e) { // EJB 1.0: throw new RemoteException("",e); throw new EJBException(e); } } public void ejbRemove() {} public void ejbActivate() {} public void ejbPassivate() { /* EJB 1.0: Close the JNDI Context and set to null. try { jndiContext.close(); } catch(NamingException ne) {} jndiContext = null; */ } public void setSessionContext(javax.ejb.SessionContext cntx) { // EJB 1.0: throws RemoteException ejbContext = cntx; try { jndiContext = new javax.naming.InitialContext(); } catch(NamingException ne) { // EJB 1.0: throw new RemoteException("",ne); throw new EJBException(ne); } } protected Object getHome(String name, Class type) {// EJB 1.0: throws RemoteException // EJB 1.1 and EJB 1.0 specific implementations } }
There is a lot of code to digest, so we will approach it in small pieces. First, let’s examine the
ejbCreate()
method:
public class TravelAgentBean implements javax.ejb.SessionBean { public Customer customer; public Cruise cruise; public Cabin cabin; public javax.ejb.SessionContext ejbContext; // EJB 1.0: jndiContext should be declared transient public javax.naming.Context jndiContext; public void ejbCreate(Customer cust) { customer = cust; }
When the bean is created, the remote reference to the Customer bean
is passed to the bean instance and maintained in the
customer
field. The customer
field is part of the bean’s conversational state. We could have
obtained the customer’s identity as an integer ID and
constructed the remote reference to the Customer bean in the
ejbCreate()
method. However, we passed the
reference directly to demonstrate that remote references to beans can
be passed from a client application to a bean. They can also be
returned from the bean to the client and passed between beans on the
same EJB server or between EJB servers.
References to the SessionContext
and JNDI context
are held in fields called ejbContext
and
jndiContext
. Prefixing the names of these context
types helps avoid confusion.
When a bean is passivated in EJB 1.1, the
JNDI ENC must be
maintained as part of the bean’s conversational state. This
means that the JNDI context should not be
transient
, as was the case in EJB 1.0. In EJB 1.1,
once a field is set to reference the JNDI ENC, the reference remains
valid for the life of the bean. In the
TravelAgentBean
, we set the field
jndiContext
to reference the JNDI ENC when the
SessionContext
is set a the beginning of the
bean’s life cycle:
public void setSessionContext(javax.ejb.SessionContext cntx) { // EJB 1.0: throws RemoteException ejbContext = cntx; try { jndiContext = new InitialContext(); } catch(NamingException ne) { // EJB 1.0: throw new RemoteException("",ne); throw new EJBException(ne); } }
EJB 1.1 makes special accommodations for references
to SessionContext
, the JNDI ENC, references to
other beans (remote and home interface types) and the JTA
UserTransaction
type, which is discussed in detail
in Chapter 8. The container must maintain any
instance fields that reference objects of these types as part of the
conversational state, even if they are not serializable. (In EJB 1.0,
references to most of these special types had to be closed or set to
null
in the ejbPassivate()
method.) All other fields must be serializable or
null
when the bean is passivated.
Open resources such as sockets or JDBC connections must be closed whenever the bean is passivated. In stateful session beans, open resources will not be maintained for the life of the bean instance. When a stateful session bean is passivated, any open resource can cause problems with the activation mechanism.
In EJB 1.0, references to a JNDI context are not considered part of
the bean’s conversational state; the JNDI context is
an open resource that must be referenced by a
transient
field and should be closed when the bean
is passivated. In the TravelAgentBean
, the
jndiContext
is obtained through the method
getJndiContext()
:
protected javax.naming.Context getJndiContext()
throws javax.naming.NamingException {
if (jndiContext != null)
return jndiContext;
// ... Specify the JNDI properties specific to the vendor.
jndiContext = new InitialContext(p);
return jndiContext; }
The
InitialContext
, obtained by the getJndiContext()
method, is saved in the instance
variable jndiContext
. This reduces the number of
times the InitialContext
needs to be
created.[33] However, because the JNDI
context is an open resource, it must be closed whenever the bean is
passivated. When a stateful session
bean is passivated, any open resource can cause problems with the
activation mechanism. TravelAgentBean
uses the
ejbPassivate()
notification method to close the
JNDI context and set the jndiContext
variable to
null
:
public void ejbPassivate() { try { jndiContext.close(); } catch(NamingException ne) {} jndiContext = null; }
The TravelAgent bean has methods for setting the desired cruise and cabin. These methods take integer IDs as arguments and retrieve references to the appropriate Cruise or Cabin bean from the appropriate home interface. These references are also a part of the TravelAgent bean’s conversational state:
public void setCabinID(int cabinID) throws javax.ejb.FinderException { // EJB 1.0: also throws RemoteException try { // EJB 1.0: remove try/catch CabinHome home = (CabinHome)getHome("CabinHome",CabinHome.class); CabinPK pk = new CabinPK(); pk.id=cabinID; cabin = home.findByPrimaryKey(pk); } catch(RemoteException re) { throw new EJBException(re); } } public void setCruiseID(int cruiseID) throws javax.ejb.FinderException { // EJB 1.0: also throws RemoteException try { // EJB 1.0: remove try/catch CruiseHome home = (CruiseHome)getHome("CruiseHome", CruiseHome.class); cruise = home.findByPrimaryKey(new CruisePK(cruiseID)); } catch(RemoteException re) { throw new EJBException(re); } }
It may seem strange that we set these values using the integer IDs,
but we keep them in the conversational state as remote references.
Using the integer IDs for these objects is simpler for the client,
which doesn’t work with their remote references. In the client
code, we got cabin and cruise IDs from text fields. Why make the
client obtain a remote reference to the Cruise and Cabin beans when
an ID is simpler? In addition, using the ID is cheaper than passing a
network reference in terms of network traffic. We need the EJB object
references to these bean types in the
bookPassage()
method, so we use their IDs to
obtain actual remote references. We could have waited until the
bookPassage()
method was invoked before
reconstructing the remote references, but this way we keep the
bookPassage()
method simple.
The method getHome()
, used in both set methods, is
a convenience method defined in the
TravelAgentBean
. It hides the details of obtaining
a remote reference to an EJB home object.
In EJB 1.1, the JNDI ENC can be used to obtain a reference to the home interface of other beans. Using the ENC lets you avoid hardcoding vendor-specific JNDI properties into the bean—a common problem in EJB 1.0. In other words, the JNDI ENC allows EJB references to be network and vendor independent.
In the TravelAgentBean
, the
getHome()
method uses the jndiContext
reference to obtain references to the
Cabin, Ship, ProcessPayment, and Cruise home objects:
protected Object getHome(String name,Class type) { try { Object ref = jndiContext.lookup("java:comp/env/ejb/"+name); return PortableRemoteObject.narrow(ref, type); } catch(NamingException ne) { throw new EJBException(ne); } }
EJB 1.1 recommends that all EJB references be bound to the
"java:comp/env/ejb"
context, which is the
convention followed here. In the TravelAgent bean, we pass in the
name of the home object we want and append it to the
"java:comp/env/ejb"
context to do the lookup.
The
deployment descriptor provides a
special set of tags for declaring EJB references. Here’s how
the <ejb-ref>
tag and its subelements are
used:
<ejb-ref> <ejb-ref-name>ejb/CabinHome</ejb-ref-name> <ejb-ref-type>Entity</ejb-ref-type> <home>com.titan.cabin.CabinHome</home> <remote>com.titan.cabin.Cabin</remote> </ejb-ref>
The <ejb-ref>
tag and its subelements should be
self explanatory: they define a name for the bean within the ENC,
declare the bean’s type, and give the names of its remote and
home interfaces. When a bean is deployed, the deployer maps the
<ejb-ref>
elements to actual beans in a way
specific to the vendor. The <ejb-ref>
elements can also be linked by the application assembler to beans in
the same deployment (a subject covered in detail in Chapter 10, which is about the XML deployment
descriptors).
In EJB 1.0, beans access other beans in exactly the same way that
application clients access beans: you use JNDI to look up the
bean’s home interface. To accomplish this, the JNDI
InitialContext
must be created and initialized
with properties specific to the EJB vendor.
The TravelAgent bean uses the getJndiContext()
method and the DeploymentDescriptor
environment
properties to obtain references to other beans. The
getHome()
method should be implemented as follows:
protected Object getHome(String name, Class type) throws RemoteException { try { String jndiName = ejbContext.getEnvironment().getProperty(name); return getJndiContext().lookup(jndiName); } catch(NamingException ne) { throw new RemoteException("Could not lookup ("+name+")",ne); } }
The last point of interest in our bean definition is the
bookPassage()
method. This method leverages the
conversational state accumulated by the
ejbCreate()
method and the methods
(setCabinID()
,setCruiseID()
)
to process a reservation for a customer on a cruise:
public Ticket bookPassage(CreditCard card, double price) throws IncompleteConversationalState{// EJB 1.0: also throws RemoteException if (customer == null || cruise == null || cabin == null) { throw new IncompleteConversationalState(); } try { ReservationHome resHome = (ReservationHome) getHome("ReservationHome",ReservationHome.class); Reservation reservation = resHome.create(customer, cruise, cabin, price); ProcessPaymentHome ppHome = (ProcessPaymentHome) getHome("ProcessPaymentHome",ProcessPaymentHome.class); ProcessPayment process = ppHome.create(); process.byCredit(customer, card, price); Ticket ticket = new Ticket(customer, cruise, cabin, price); return ticket; } catch(Exception e) { // EJB 1.0: throw new RemoteException("",e); throw new EJBException(e); } }
This
method exemplifies the workflow concept. It uses several beans,
including the Reservation bean, the ProcessPayment bean, the Customer
bean, the Cabin bean, and the Cruise bean to accomplish one task:
book 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 bean performs many operations:
Look up and obtain a remote reference to the Reservation bean’s EJB home.
Create a new Reservation bean resulting in a database insert.
Look up and obtain a remote reference to the ProcessPayment bean’s EJB home.
Create a new ProcessPayment bean.
Charge the customer’s credit card using the ProcessPayment bean.
Generate a new Ticket
with all the pertinent
information describing the customer’s purchase.
From a design standpoint, encapsulating the workflow in a stateful
session bean means a less complex interface for the client and more
flexibility for implementing changes. We could, for example, easily
change the bookPassage()
method to include a check
for overlapped booking (when a customer books passage on two
different cruises that overlap). If Titan’s customers often
book passage on cruises that overlap, we could add logic to
bookPassage()
to detect the problem. This type of
enhancement would not change the remote interface, so the client
application wouldn’t need modification. Encapsulating workflow
in stateful session beans allows the system to evolve over time
without impacting clients.
In addition, the type of clients used can change. One of the biggest problems with two-tier architectures—besides scalability and transactional control—is that the business logic is intertwined with the client logic. This makes it 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 bean 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 bean 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 bean also provides transactional integrity for
processing the customer’s reservation. As explained in Chapter 3, if any one of the operations within the body
of the bookPassage()
method fails, all the
operations are rolled back so that none of the changes are accepted.
If the credit card can’t be charged by the ProcessPayment bean,
the newly created Reservation bean and its associated record are
removed. The transactional aspects of the TravelAgent bean are
explained in
detail in Chapter 8.
Although the Reservation bean is an entity bean, understanding its design and purpose is important to understanding how and why it’s used within the TravelAgent bean. For this reason, we will depart momentarily from our discussion of session beans, and the TravelAgent bean in particular, to expand our understanding of the Reservation bean. (The code for this bean is available on the O’Reilly web site. See the preface for details.)
The Reservation bean represents an immutable record in the database: a reservation. It records an event in the history of the reservation system. If you examine the Reservation bean closely, you will discover that you cannot modify its contents once it is created. This means it can’t be changed, although it can be deleted. So why make it an entity bean at all? Why not simply write records into the database?
Titan discovered that customers were averaging two calls to confirm their reservations between the time they made the reservation and the time they actually went on the trip. This is a lot of calls when you consider that ships book 2,000 to 3,000 passengers each. Titan also discovered that about 15% of its reservations are canceled. Canceling a reservation means deleting a record, which can be tricky business. In short, Titan was accessing reservation information a lot, mostly to confirm data, but also to delete reservations for cancellations. To provide consistent and safe access to reservation data, Titan created a Reservation bean. This helped encapsulate the logic for creating, reading, and deleting reservation data, so that it could be reused in several different kinds of workflow. In addition to the TravelAgent session, the ConfirmReservation and CancelReservation beans also use the Reservation bean (we don’t create bean types like these in this book). These other stateful session beans have different workflow and reuse the Reservation bean differently. Encapsulating the reservation record as an entity bean ensures consistent and safe access to sensitive data.
Another advantage of the Reservation bean is that it can help prevent
double booking—when two different customers book the same cabin
on the same cruise. There is a gap between the time the customer
chooses a cabin and cruise and the time when
bookPassage()
is invoked. During this time, some
other reservation agent could book the same cabin and cruise for a
different customer. To prevent a double booking, we can place a
unique index on the CABIN_ID
and
CRUISE_ID
in the RESERVATION
table. Any attempt to add a new record—by creating a
Reservation bean—with the same CABIN_ID
and
CRUISE_ID
will result in an
SQLException
, effectively preventing a double
booking.
EJB provides its own strategy for
handling duplicate records through the DuplicateKeyException
. The
DuplicateKeyException
is a type of
CreateException
that is thrown by the
create()
methods
of the home interfaces for entity beans if the bean cannot be created
because a bean with that same primary key already exists. The
Reservation bean’s primary key is defined as follows:
package com.titan.reservation; public class ReservationPK implements java.io.Serializable { public int cruiseID; public int cabinID; public ReservationPK(){} public ReservationPK(int crsID, int cbnID) { cruiseID = crsID; cabinID = cbnID; } ...// equals() & hashCode() methods not shown }
Notice that the cruiseID
and
cabinID
combination is used to define the
uniqueness of a Reservation entity. With
container-managed
persistence, the DuplicateKeyException
is thrown
automatically; with
bean-managed persistence, we need to capture the SQLException
resulting from the attempted insert and throw the
DuplicateKeyException
explicitly. Either way, the DuplicateKeyException
causes the bookPassage()
method to fail,
preventing the customer from double booking. In our example, we
capture the DuplicateKeyException
and rethrow it
as a DoubleBookedException
, which is more
understandable from the client’s perspective.
If we have a Reservation bean, why do we need a TravelAgent bean? Good question! The TravelAgent bean uses the Reservation bean to create a reservation, but it also has to charge the customer and generate a ticket. These are not activities that are specific to the Reservation bean, so they need to be captured in a stateful session bean that can manage workflow and transactional scope. In addition, the TravelAgent bean also provides listing behavior, which spans concepts in Titan’s system. It would have been inappropriate to include any of these other behaviors in the Reservation entity bean.
As promised, we are going to bring back the cabin-listing behavior we played around with in Chapter 4. This time, however, we are not going to use the Cabin bean to get the list; instead, we will access the database directly. Accessing the database directly is a double-edged sword. On one hand, we don’t want to access the database directly if entity beans exist that can access the same information. Entity beans provide a safe and consistent interface for a particular set of data. Once an entity bean has been tested and proven, it can be reused throughout the system, substantially reducing data integrity problems. The Reservation bean is an example of that kind of usage. In addition, entity beans can pull together disjointed data and apply additional business logic such as validation, limits, and security to ensure that data access follows the business rules.
But entity beans cannot define every possible data access needed, and they shouldn’t. One of the biggest problems with entity beans is that they tend to become bloated over time. A development effort that relies too heavily on entity beans will create beans that have more functionality than is normally needed. Huge entity beans with dozens of methods are a sure sign of poor design. Entity beans should be focused on providing data access to a very limited, but conceptually bound, set of data. You should be able to update, read, and insert records or data specific to that concept. Data access that spans concepts, however, should not be encapsulated in one entity bean. An example of this is listing behavior.
Systems always need listing behavior to present clients with choices.
In the reservation system, for example, customers will want to choose
a cabin from a list of available cabins. The
word available is key to the definition of this
behavior. The Cabin bean can provide us with a list of cabins, but
not available cabins. That’s because a cabin doesn’t know
if it is available; it only knows the details that describe it. The
question of whether a cabin is available or not is relevant to the
process using it—in this case
TravelAgent
—but is not relevant to the cabin
itself. As an analogy, an automobile entity would not care what road
it’s on; it is only concerned with characteristics that
describe its state and behavior. An automobile-tracking system would
be concerned with the location of individual automobiles.
To get availability information, we need to compare the list of
cabins on our ship to the list of cabins that have already been
reserved. The listAvailableCabins()
method does
exactly that. It uses a complex SQL query to produce a list of cabins
that have not yet been reserved for the cruise chosen by the client:
public String [] listAvailableCabins(int bedCount) throws IncompleteConversationalState { // EJB 1.0: also throws RemoteException if (cruise == null) throw new IncompleteConversationalState(); Connection con = null; PreparedStatement ps = null;; ResultSet result = null; try { int cruiseID = ((CruisePK)cruise.getPrimaryKey()).id; int shipID = cruise.getShipID(); con = getConnection(); ps = con.prepareStatement( "select ID, NAME, DECK_LEVEL from CABIN "+ "where SHIP_ID = ? and ID NOT IN "+ "(SELECT CABIN_ID FROM RESERVATION WHERE CRUISE_ID = ?)"); ps.setInt(1,shipID); ps.setInt(2,cruiseID); result = ps.executeQuery(); Vector vect = new Vector(); while(result.next()) { StringBuffer buf = new StringBuffer(); buf.append(result.getString(1)); buf.append(','), buf.append(result.getString(2)); buf.append(','), buf.append(result.getString(3)); vect.addElement(buf.toString()); } String [] returnArray = new String[vect.size()]; vect.copyInto(returnArray); return returnArray; } catch (Exception e) { // EJB 1.0: throw new RemoteException("",e); throw new EJBException(e); } finally { try { if (result != null) result.close(); if (ps != null) ps.close(); if (con!= null) con.close(); }catch(SQLException se){se.printStackTrace();} } }
As you can see, the SQL query is complex. It could have been defined
in a bean-managed Cabin bean using a method like
Cabin.findAvailableCabins(Cruise
cruise)
but this would be a poor design choice.
First, the Cabin bean would need to access the
RESERVATION
table, which is not a part of its
definition. Entity beans should focus on only the data that defines
them.
It might make a little more sense to make this behavior a find method
of the Cruise bean. One can imagine a method like
Cruise.findAvailableCabins()
. You can certainly
argue that a cruise should be aware of its own reservations. But this
behavior is not very reusable. In other words, this kind of listing
behavior is only used in the reservation system. Making it part of
the Cruise bean’s behavior suggests that is universally useful,
which it’s not.
The
listAvailableCabins()
method returns an array of
String
objects. This is important because we could
have opted to return an collection of Cabin
remote
references, but we didn’t. The reason is simple: we want to
keep the client application as lightweight as possible. A list of
String
objects is much more lightweight than the
alternative, a collection of remote references. In addition, a
collection of remote references means that client would be working
with many stubs, each with its own connection to EJB objects on the
server. By returning a lightweight string array, we reduce the number
of stubs on the client, which keeps the client simple and conserves
resources on the server.
To make this method work, you need to create a
getConnection()
method for obtaining a database
connection and add it to the TravelAgentBean
:
// EJB 1.1: getConnection() private Connection getConnection() throws SQLException { try { DataSource ds = (DataSource)jndiContext.lookup( "java:comp/env/jdbc/titanDB"); return ds.getConnection(); } catch(NamingException ne) {throw new EJBException(ne);} } // EJB 1.0: getConnection() private Connection getConnection() throws SQLException { return DriverManager.getConnection( ejbContext.getEnvironment().getProperty("jdbcURL")); }
Change the remote interface for TravelAgent to include the
listAvailableCabins()
method as shown in the
following code:
throws RemoteException,IncompleteConversationalState;
public interface TravelAgent extends javax.ejb.EJBObject { public void setCruiseID(int cruise) throws RemoteException, FinderException; public int getCruiseID() throws RemoteException; public void setCabinID(int cabin) throws RemoteException, FinderException; public int getCabinID() throws RemoteException; public int getCustomerID() throws RemoteException; public Ticket bookPassage(CreditCard card, double price) public String [] listAvailableCabins(int bedCount) throws RemoteException, IncompleteConversationalState; }
Use the following
XML deployment descriptor when
deploying the TravelAgent bean. The most important difference between
this descriptor and the deployment descriptor used for the
ProcessPayment bean is the <session-type>
tag, which states that this bean is stateful, and the use of the
<ejb-ref>
elements to describe beans that
are referenced through the ENC:
<?xml version="1.0"?> <!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 1.1//EN" "http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd"> <ejb-jar> <enterprise-beans> <session> <description> Acts as a travel agent for booking passage on a ship. </description> <ejb-name>TravelAgentBean</ejb-name> <home>com.titan.travelagent.TravelAgentHome</home> <remote>com.titan.travelagent.TravelAgent</remote> <ejb-class>com.titan.travelagent.TravelAgentBean</ejb-class> <session-type>Stateful</session-type> <transaction-type>Container</transaction-type> <ejb-ref> <ejb-ref-name>ejb/ProcessPaymentHome</ejb-ref-name> <ejb-ref-type>Session</ejb-ref-type> <home>com.titan.processpayment.ProcessPaymentHome</home> <remote>com.titan.processpayment.ProcessPayment</remote> </ejb-ref> <ejb-ref> <ejb-ref-name>ejb/CabinHome</ejb-ref-name> <ejb-ref-type>Entity</ejb-ref-type> <home>com.titan.cabin.CabinHome</home> <remote>com.titan.cabin.Cabin</remote> </ejb-ref> <ejb-ref> <ejb-ref-name>ejb/CruiseHome</ejb-ref-name> <ejb-ref-type>Entity</ejb-ref-type> <home>com.titan.cruise.CruiseHome</home> <remote>com.titan.cruise.Cruise</remote> </ejb-ref> <ejb-ref> <ejb-ref-name>ejb/CustomerHome</ejb-ref-name> <ejb-ref-type>Entity</ejb-ref-type> <home>com.titan.customer.CustomerHome</home> <remote>com.titan.customer.Customer</remote> </ejb-ref> <ejb-ref> <ejb-ref-name>ejb/ReservationHome</ejb-ref-name> <ejb-ref-type>Entity</ejb-ref-type> <home>com.titan.reservation.ReservationHome</home> <remote>com.titan.reservation.Reservation</remote> </ejb-ref> <resource-ref> <description>DataSource for the Titan database</description> <res-ref-name>jdbc/titanDB</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref> </session> </enterprise-beans> <assembly-descriptor> <security-role> <description> This role represents everyone who is allowed full access to the TravelAgent bean. </description> <role-name>everyone</role-name> </security-role> <method-permission> <role-name>everyone</role-name> <method> <ejb-name>TravelAgentBean</ejb-name> <method-name>*</method-name> </method> </method-permission> <container-transaction> <method> <ejb-name>TravelAgentBean</ejb-name> <method-name>*</method-name> </method> <trans-attribute>Required</trans-attribute> </container-transaction> </assembly-descriptor> </ejb-jar>
Once you have generated the deployment descriptor,
jar
the TravelAgent bean and deploy it in your
EJB server. You will also need to deploy the Reservation, Cruise, and
Customer beans that you downloaded earlier. Based on the business
methods in the remote interface of the TravelAgent bean and your past
experiences with the Cabin, Ship, and ProcessPayment beans, you
should be able to create your own client application to test this
code.
For EJB 1.0, we need to make one change to
the deployment descriptor for the TravelAgent bean that we created in
Chapter 4. We need to change the state management
type from stateless to stateful. Here is the new definition of the
MakeDD
class used to generate the deployment
descriptor for the TravelAgent bean:
package com.titan.travelagent; import javax.ejb.deployment.*; import javax.naming.CompoundName; import java.util.*; import java.io.*; public class MakeDD { public static void main(String args []) { try { if (args.length <1) { System.out.println("must specify target directory"); return; } SessionDescriptor sd = new SessionDescriptor(); sd.setEnterpriseBeanClassName( "com.titan.travelagent.TravelAgentBean"); sd.setHomeInterfaceClassName( "com.titan.travelagent.TravelAgentHome"); sd.setRemoteInterfaceClassName( "com.titan.travelagent.TravelAgent"); sd.setSessionTimeout(60); sd.setStateManagementType(SessionDescriptor.STATEFUL_SESSION); ControlDescriptor cd = new ControlDescriptor(); cd.setIsolationLevel(ControlDescriptor.TRANSACTION_READ_COMMITTED); cd.setMethod(null); cd.setRunAsMode(ControlDescriptor.CLIENT_IDENTITY); cd.setTransactionAttribute(ControlDescriptor.TX_REQUIRED); ControlDescriptor [] cdArray = {cd}; sd.setControlDescriptors(cdArray); // Set enterprise bean's environment properties. Properties ep = new Properties(); ep.put("CruiseHome","CruiseHome"); ep.put("CabinHome","CabinHome"); ep.put("ReservationHome","ReservationHome"); ep.put("ProcessPaymentHome","ProcessPaymentHome"); ep.put("ShipHome","ShipHome"); ep.put("jdbcURL","jdbc:subprotocol
:subname
"); sd.setEnvironmentProperties(ep); Properties jndiProps = new Properties(); CompoundName jndiName = new CompoundName("TravelAgentHome",jndiProps); sd.setBeanHomeName(jndiName); String fileSeparator = System.getProperties().getProperty("file.separator"); if (! args[0].endsWith(fileSeparator)) args[0] += fileSeparator; FileOutputStream fis = new FileOutputStream(args[0]+"TravelAgentDD.ser"); ObjectOutputStream oos = new ObjectOutputStream(fis); oos.writeObject(sd); oos.flush(); oos.close(); fis.close(); } catch(Throwable t) {t.printStackTrace();} } }
In addition to changing the state management type, we added several environment properties that help us to locate the beans that the TravelAgent bean needs to work with.
Once you have generated the deployment descriptor,
jar
the TravelAgent bean and deploy it in your
EJB server. You will also need to deploy the Reservation, Cruise, and
Customer beans that you downloaded earlier. Based on the business
methods in the remote interface of the TravelAgent bean and your past
experiences with the Cabin, Ship, and ProcessPayment beans, you
should be able to create your own client application to test this
code.
[30] This is a conceptual model. Some EJB containers may actually use instance swapping with stateful session beans but make it appear as if the same instance is servicing all requests. Conceptually, however, the same stateful session bean instance services all requests.
[31] This is a conceptual model. Some EJB containers may actually use separate EJB objects for concurrent access to the same entity, relying on the database to control concurrency. Conceptually, however, the end result is the same.
[32] If you’re
modifying the bean developed in Chapter 4,
remember to delete the listCabin()
method. We will
add a new implementation of that method later in this chapter.
[33] The advantage gained by maintaining the
connection will be marginal if the container calls
ejbPassivate()
after every method call—which
some implementations do.