Session beans act as agents to the client, controlling workflow (the business process) and filling the gaps between the representation of data by entity beans and the business logic that interacts with that data. Session beans are often used to manage interactions between entity beans and can perform complex manipulations of beans to accomplish some task. Since we have only defined one entity bean so far, we will focus on a complex manipulation of the Cabin bean rather than the interactions of the Cabin bean with other entity beans. In Chapter 7, after we have had the opportunity to develop other entity beans, interactions of entity beans within session beans will be explored in greater detail.
Client applications and other beans use the Cabin bean in a variety of ways. Some of these uses were predictable when the Cabin bean was defined, but most were not. After all, an entity bean represents data—in this case, data describing a cabin. The uses to which we put that data will change over time—hence the importance of separating the data itself from the workflow. In Titan’s business system, for example, we will need to list and report on cabins in ways that were not predictable when the Cabin bean was defined. Rather than change the Cabin bean every time we need to look at it differently, we will obtain the information we need using a session bean. Changing the definition of an entity bean should only be done within the context of a larger process—for example, a major redesign of the business system.
In Chapter 1 and Chapter 2, we talked hypothetically about a TravelAgent bean that was responsible for the workflow of booking a passage on a cruise. This session bean will be used in client applications accessed by travel agents throughout the world. In addition to booking tickets, the TravelAgent bean also provides information about which cabins are available on the cruise. In this chapter, we will develop the first implementation of this listing behavior in the TravelAgent bean. The listing method we develop in this example is admittedly very crude and far from optimal. However, this example is useful for demonstrating how to develop a very simple stateless session bean and how these session beans can manage other beans. In Chapter 7, we will rewrite the listing method. This “list cabins” behavior is used by travel agents to provide customers with a list of cabins that can accommodate the customer’s needs. The Cabin bean does not directly support the kind of list, nor should it. The list we need is specific to the TravelAgent bean, so it’s the Travel- Agent bean’s responsibility to query the Cabin beans and produce the list.
Before we get started, we will need to create a
development directory for the
TravelAgent bean, as we did for the Cabin bean. We name this
directory travelagent and nest it below the
com/titan
directory, which also contains the
cabin directory (see Chapter 4).
As before, we start by defining the remote interface so that our
focus is on the business purpose of the bean, rather than its
implementation. Starting small, we know that the TravelAgent will
need to provide a method for listing all the cabins available with a
specified bed count for a specific ship. We’ll call that method
listCabins()
. Since we only need a list of cabin
names and deck levels, we’ll define
listCabins()
to return an array of
String
s. Here’s the remote interface for
TravelAgent
:
package com.titan.travelagent; import java.rmi.RemoteException; import javax.ejb.FinderException; public interface TravelAgent extends javax.ejb.EJBObject { // String elements follow the format "id, name, deck level" public String [] listCabins(int shipID, int bedCount) throws RemoteException; }
Copy the TravelAgent
interface definition into
your IDE, and save it to the travel- agent
directory. Compile the class to ensure that it is correct.
The second step in the development of any bean is to create the home interface. The home interface for a session bean defines the create methods that initialize a new session bean for use by a client.
Find methods are not used in session beans; they are used with entity beans to locate persistent entities for use on a client. Unlike entity beans, session beans are not persistent and do not represent data in the database, so a find method would not be meaningful; there is no specific session to locate. A session bean is dedicated to a client for the life of that client (or less). For the same reason, we don’t need to worry about primary keys; since session beans don’t represent persistent data, we don’t need a key to access that data.
package com.titan.travelagent; import java.rmi.RemoteException; import javax.ejb.CreateException; public interface TravelAgentHome extends javax.ejb.EJBHome { public TravelAgent create() throws RemoteException, CreateException; }
In the case of the TravelAgent bean, we only need a simple
create()
method to get a reference to the bean.
Invoking this create()
method returns a
Travel-Agent
remote reference that the client can
use for the reservation process. Copy the
TravelAgentHome
definition into your IDE and save
it to the travelagent
directory. Compile the
class to ensure that it is correct.
Using the remote interface as a guide, we can define the
TravelAgentBean
class that implements the
listCabins()
method. The following code contains
the complete definition of TravelAgentBean
for
this example. Copy the TravelAgentBean
definition
into your IDE and save it to the travelagent
directory. Compile the class to ensure that it is correct. EJB 1.1
and EJB 1.0 differ significantly in how one bean locates another, so
I have provided separate TravelAgentBean listings for each version.
Here’s the code for the EJB 1.1 version of the
TravelAgentBean
:
package com.titan.travelagent; import com.titan.cabin.Cabin; import com.titan.cabin.CabinHome; import com.titan.cabin.CabinPK; import java.rmi.RemoteException; import javax.naming.InitialContext; import javax.naming.Context; import java.util.Properties; import java.util.Vector; public class TravelAgentBean implements javax.ejb.SessionBean { public void ejbCreate() { // Do nothing. } public String [] listCabins(int shipID, int bedCount) { try { javax.naming.Context jndiContext = new InitialContext(); Object obj = jndiContext.lookup("java:comp/env/ejb/CabinHome"); CabinHome home = (CabinHome) javax.rmi.PortableRemoteObject.narrow(obj, CabinHome.class); Vector vect = new Vector(); CabinPK pk = new CabinPK(); Cabin cabin; for (int i = 1; ; i++) { pk.id = i; try { cabin = home.findByPrimaryKey(pk); } catch(javax.ejb.FinderException fe) { break; } // Check to see if the bed count and ship ID match. if (cabin.getShip() == shipID && cabin.getBedCount() == bedCount) { String details = i+","+cabin.getName()+","+cabin.getDeckLevel(); vect.addElement(details); } } String [] list = new String[vect.size()]; vect.copyInto(list); return list; } catch(Exception e) {throw new EJBException(e);} } private javax.naming.Context getInitialContext() throws javax.naming.NamingException { Properties p = new Properties(); // ... Specify the JNDI properties specific to the vendor. return new javax.naming.InitialContext(p); } public void ejbRemove(){} public void ejbActivate(){} public void ejbPassivate(){} public void setSessionContext(javax.ejb.SessionContext cntx){} }
Examining the listCabins()
method in detail, we
can address the implementation in pieces, starting with the use of
JNDI to locate the CabinHome
:
javax.naming.Context jndiContext = new InitialContext(); Object obj = jndiContext.lookup("java:comp/env/ejb/CabinHome"); CabinHome home = (CabinHome) javax.rmi.PortableRemoteObject.narrow(obj, CabinHome.class);
Beans are clients to other
beans, just like client applications. This means that they must
interact with other beans in the same way that client applications
interact with beans. In order for one bean to locate and use another
bean, it must first locate and obtain a
reference to the
bean’s EJB home. This is accomplished using JNDI, in exactly
the same way we used JNDI to obtain a reference to the
CabinHome
in the client application we developed
earlier. In EJB 1.1, all beans have a default JNDI context called the
environment context, which was discussed a little in Chapter 3. The default context exists in the name space
(directory) called "java:comp/env"
and its
subdirectories. When the bean is deployed, any beans it uses are
mapped into the subdirectory "java:comp/env/ejb"
,
so that bean references can be obtained at runtime through a simple
and consistent use of the JNDI default context. We’ll come back
to this when we take a look at the deployment descriptor for the
TravelAgent bean below.
Once the EJB home of the Cabin bean is obtained, we can use it to produce a list of cabins that match the parameters passed. The following code loops through all the Cabin beans and produces a list that includes only those cabins with the ship and bed count specified:
Vector vect = new Vector(); CabinPK pk = new CabinPK(); Cabin cabin; for (int i = 1; ; i++) { pk.id = i; try { cabin = home.findByPrimaryKey(pk); } catch(javax.ejb.FinderException fe){ break; } // Check to see if the bed count and ship ID match. if (cabin.getShip() == shipID && cabin.getBedCount() == bedCount) { String details = i+","+cabin.getName()+","+cabin.getDeckLevel(); vect.addElement(details); } }
This method simply iterates through all the primary keys, obtaining a
remote reference to each Cabin bean in the system and checking
whether its ship
and bedCount
match the parameters passed in. The for
loop
continues until a FinderException
is thrown, which
would probably occur when a primary key is used that isn’t
associated with a bean. (This isn’t the most robust code
possible, but it will do for now.) Following this block of code, we
simply copy the Vector
’s contents into an
array and return it to the client.
While this is a very crude approach to locating the right Cabin beans—we will define a better method in Chapter 7—it is adequate for our current purposes. The purpose of this example is to illustrate that the workflow associated with this listing behavior is not included in the Cabin bean nor is it embedded in a client application. Workflow logic, whether it’s a process like booking a reservation or obtaining a list, is placed in a session bean.
Here’s the code for the EJB 1.0 version of the
TravelAgentBean
:
package com.titan.travelagent; import com.titan.cabin.Cabin; import com.titan.cabin.CabinHome; import com.titan.cabin.CabinPK; import java.rmi.RemoteException; import javax.naming.InitialContext; import javax.naming.Context; import java.util.Properties; import java.util.Vector; public class TravelAgentBean implements javax.ejb.SessionBean { public void ejbCreate() { // Do nothing. } public String [] listCabins(int shipID, int bedCount) throws RemoteException { try { Context jndiContext = getInitialContext(); CabinHome home = (CabinHome)jndiContext.lookup("CabinHome"); Vector vect = new Vector(); CabinPK pk = new CabinPK(); Cabin cabin; for (int i = 1; ; i++) { pk.id = i; try { cabin = home.findByPrimaryKey(pk); } catch(javax.ejb.FinderException fe) { break; } // Check to see if the bed count and ship ID match. if (cabin.getShip() == shipID && cabin.getBedCount() == bedCount) { String details = i+","+cabin.getName()+","+cabin.getDeckLevel(); vect.addElement(details); } } String [] list = new String[vect.size()]; vect.copyInto(list); return list; } catch (javax.naming.NamingException ne) { throw new RemoteException("Unable to locate CabinHome",ne); } } private javax.naming.Context getInitialContext() throws javax.naming.NamingException { Properties p = new Properties(); // ... Specify the JNDI properties specific to the vendor. return new javax.naming.InitialContext(p); } public void ejbRemove(){} public void ejbActivate(){} public void ejbPassivate(){} public void setSessionContext(javax.ejb.SessionContext cntx){} }
The most significant difference between this code and the EJB 1.1
code is the use of JNDI to locate the CabinHome
:
Context jndiContext = getInitialContext(); CabinHome cabinHome = (CabinHome)jndiContext.lookup("CabinHome");
Beans interact with other
beans in the same way that clients interact with beans. In order for
one bean to locate and use another bean, it must first locate and
obtain a
reference to the
bean’s EJB home. This is accomplished using JNDI, in exactly
the same way we used JNDI to obtain a reference to the
CabinHome
in the client application we developed
earlier. If you take a close look at the method
getInitialContext()
, you will discover that it is
exactly the same as the getInitialContext()
method
in the client classes defined earlier. The only difference is that
the method is not static. You will need to change this code to match
the correct settings for your EJB server. Once the EJB home of the
Cabin bean is obtained, we can use it to produce our list of cabins
that match the parameters passed.
The logic for finding beans with cabins that match the desired parameters is the same in EJB 1.1 and EJB 1.0. Again, it’s a crude approach: we will define a better method in Chapter 7. Our purpose here is to demonstrate that the workflow associated with this listing behavior is not included in the Cabin bean nor is it embedded in a client application. Workflow logic, whether it’s a process like booking a reservation or obtaining a list, is placed in a session bean.
The TravelAgent bean uses an XML deployment descriptor similar to the one used for the Cabin entity bean. Here is the ejb-jar.xml file used to deploy the TravelAgent. In Chapter 10, you will learn how to deploy several beans in one deployment descriptor, but for now the TravelAgent and Cabin beans are deployed separately.
<?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> <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>Stateless</session-type> <transaction-type>Container</transaction-type> <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> </session> </enterprise-beans> <assembly-descriptor> <security-role> <description> This role represents everyone who is allowed full access to the cabin 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>
Other than the
<session-type>
and
<ejb-ref>
elements, this XML deployment
descriptor should make sense since it uses many of the same elements
as the Cabin bean’s. The
<session-type>
element can
be
Stateful
or Stateless
to
indicate which type of session bean is used.
The <ejb-ref>
element is used at deployment
time to map the bean references used within the TravelAgent bean. In
this case, the <ejb-ref>
element describes
the Cabin bean, which we already deployed. The
<ejb-ref-name>
element specifies the name that must be
used by the TravelAgent bean to obtain a reference to the Cabin
bean’s home. The
<ejb-ref-type>
tells the container what kind of bean
it is, Entity
or Session
. The
<home>
and <remote>
elements specify the fully qualified interface names of the
Cabin’s home and remote bean interfaces.
When the bean is deployed, the <ejb-ref>
will be mapped to the Cabin bean in the EJB server. This is a
vendor-specific process, but the outcome should always be the same.
When the TravelAgent does a JNDI lookup using the context name
"java:comp/env/ejb/CabinHome"
it will obtain a
remote reference to the Cabin bean’s home. The purpose of the
<ejb-ref>
element is to eliminate network
specific and implementation specific use of JNDI to obtain bean
references. This makes a bean more portable because the network
location and JNDI service provider can change without impacting the
bean code or even the XML deployment
descriptor.
Deploying the TravelAgent bean is
essentially the same as deploying the Cabin bean, except we use a
SessionDescriptor
instead of an
EntityDescriptor
. Here is the definition of the
MakeDD
for creating and serializing a
SessionDescriptor
for the
TravelAgentBean
:
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(300); sd.setStateManagementType(SessionDescriptor.STATELESS_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); CompoundName jndiName = new CompoundName("TravelAgentHome", new Properties()); 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(); } } }
The MakeDD
definition for the TravelAgent bean is
essentially the same as the one for the Cabin bean. The difference is
that we are using a SessionDescriptor
instead of
an EntityDescriptor
and the bean class names and
JNDI name are different. We do not specify any container-managed
fields because session beans are not persistent.
After instantiating the
javax.ejb.SessionDescriptor
, the
MakeDD
application sets the remote interface and
bean class names:
sd.setEnterpriseBeanClassName("com.titan.travelagent.TravelAgentBean"); sd.setHomeInterfaceClassName("com.titan.travelagent.TravelAgentHome"); sd.setRemoteInterfaceClassName("com.titan.travelagent.TravelAgent");
Next, we set two properties that control session timeouts (what happens if the bean is idle) and state management:
sd.setSessionTimeout(300); sd.setStateManagementType(SessionDescriptor.STATELESS_SESSION);
setSessionTimeout()
specifies how many seconds the
session should remain alive if it is not being used. In
MakeDD
we specify 300 seconds. This means that if
no method is invoked on the session for over five minutes, it will be
removed and will no longer be available for use.[12] If a method is invoked on a bean
that has timed out, a
javax.ejb.ObjectNotFoundException
will be thrown.
Once a stateful session bean has timed out, all of its accumulated
state is lost. When a session bean times out, the client must create
a new TravelAgent bean by invoking the
TravelAgentHome.create()
method. The
setStateManagement()
method determines whether the
bean is stateful or stateless. At this point in it its development,
the TravelAgentBean
doesn’t have any
conversational state that needs to be maintained from one method to
the next, so we make it a stateless session bean, which is more
efficient. Both of these methods are unique to session descriptors;
there are no corresponding methods in the
EntityDescriptor
class.
The next section specifies the default
ControlDescriptor
for
the TravelAgentBean
. These settings are the same
as those used in the Cabin bean. The isolation level determines the
visibility of the data being accessed. Chapter 8
explores isolation levels in more detail. The transactional
attribute, TX_REQUIRED
, tells the EJB server that
this bean must be included in the transactional scope of the client
invoking it; if the client is not in a transaction, a new transaction
must be created for the method invocation, as follows:
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);
The next section creates a JNDI name for
TravelAgent
’s EJB home. When we use JNDI to
look up the TravelAgentHome
, this will be the name
we specify:
CompoundName jndiName = new CompoundName("TravelAgentHome",new Properties());
Finally, the MakeDD
serializes the
SessionDescriptor
to a file named
TravelAgentDD.ser and saves
it to the travelagent
directory.
You will need to compile and run the MakeDD
class
before continuing:
\dev % java com.titan.travelagent.MakeDD com/titan/travelagent F:\..\dev>java com.titan.travelagent.MakeDD com\titan\travelagent
To
place the TravelAgent bean in a JAR file, we use the same process we
used for the Cabin bean. We shrink-wrap the TravelAgent bean class
and its deployment descriptor into a JAR file and save to the
com/titan/travelagent
directory:
\dev % jar cf cabin.jar com/titan/travelagent/*.class META-INF/ejb-jar.xml F:\..\dev>jar cf cabin.jar com\titan\travelagent\*.class META-INF\ejb-jar.xml
You might have to create the META-INF
directory first, and copy
ejb-jar.xml
into that directory. The TravelAgent
bean is now complete and ready to be
deployed.
To
place the TravelAgent bean in a JAR file, we use the same process we
used for the Cabin bean. First, we have to create a manifest file,
which we save in the com/titan/travelagent
directory:
Name: com/titan/travelagent/TravelAgentDD.ser Enterprise-Bean: True
Now that the manifest is ready, we can shrink-wrap the TravelAgent bean so that it’s ready for deployment:
\dev % jar cmf com/titan/travelagent/manifest \ TravelAgent.jar com/titan/travelagent/*.class com/titan/travelagent/*.ser F:\..\dev>jar cmf com\titan\travelagent\manifest TravelAgent.jar com\titan\travelagent\*.class com\titan\travelagent\*.ser
The TravelAgent bean is now complete and ready to be deployed.
To make your TravelAgent bean available to a client application, you need to use the deployment utility or wizard of your EJB server. The deployment utility reads the JAR file to add the TravelAgent bean to the EJB server environment. Unless your EJB server has special requirements, it is unlikely that you will need to change or add any new attributes to the bean. You will not need to create a database table for this example, since the TravelAgent bean is using only the Cabin bean and is not itself persistent. Deploy the TravelAgent bean and proceed to the next section.
To show that our session bean works,
we’ll create a simple client application that uses it. This
client simply produces a list of cabins assigned to ship 1 with a bed
count of 3. Its logic is similar to the client we created earlier to
test the Cabin bean: it creates a context for looking up
TravelAgentHome
, creates a TravelAgent bean, and
invokes listCabins()
to generate a list of the
cabins available. Here’s the code:
package com.titan.travelagent;
import com.titan.cabin.CabinHome;
import com.titan.cabin.Cabin;
import com.titan.cabin.CabinPK;
import javax.naming.InitialContext;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.ejb.CreateException;
import java.rmi.RemoteException;
import java.util.Properties;
public class Client_1 {
public static int SHIP_ID = 1;
public static int BED_COUNT = 3;
public static void main(String [] args) {
try {
Context jndiContext = getInitialContext();
Object ref = (TravelAgentHome)
jndiContext.lookup("TravelAgentHome");
TravelAgentHome home = (TravelAgentHome)
// EJB 1.0: Use Java cast instead of narrow()
PortableRemoteObject.narrow(ref,TravelAgentHome.class);
TravelAgent reserve = home.create();
// Get a list of all cabins on ship 1 with a bed count of 3.
String list [] = reserve.listCabins(SHIP_ID,BED_COUNT);
for(int i = 0; i < list.length; i++){
System.out.println(list[i]);
}
} catch(java.rmi.RemoteException re){re.printStackTrace();}
catch(Throwable t){t.printStackTrace();}
}
static public Context getInitialContext() throws Exception {
Properties p = new Properties();
// ... Specify the JNDI properties specific to the vendor.
return new InitialContext(p);
}
}
The output should look like this:
1,Master Suite ,1 3,Suite 101 ,1 5,Suite 103 ,1 7,Suite 105 ,1 9,Suite 107 ,1 12,Suite 201 ,2 14,Suite 203 ,2 16,Suite 205 ,2 18,Suite 207 ,2 20,Suite 209 ,2 22,Suite 301 ,3 24,Suite 303 ,3 26,Suite 305 ,3 28,Suite 307 ,3 30,Suite 309 ,3
You have now successfully created the first piece of the TravelAgent session bean: a method that obtains a list of cabins by manipulating the Cabin bean entity.
[12] Whether a session timeout is measured from creation time (the time the session bean is created) or from the time of last activity (when the last business method is invoked) is not clearly described in EJB 1.0. As a result, some vendors set the timeout relative to one of these two events (creation or last activity). Consult your vendor’s documentation to determine your EJB server’s timeout policy.
Get Enterprise JavaBeans, Second Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.