|
|
Deploying the Cabin Bean Deployment is the process of reading the bean's JAR file, changing or adding properties to the deployment descriptor, mapping the bean to the database, defining access control in the security domain, and generating vendor-specific classes needed to support the bean in the EJB environment. Every EJB server product has its own deployment tools, which may provide a graphical user interface, a set of command-line programs, or both. Graphical deployment "wizards" are the easiest deployment tools to work with. EJB 1.1 deployment tools A deployment tool reads the JAR file and looks for the ejb-jar.xml file. In a graphical deployment wizard, the deployment descriptor elements will be presented in a set of property sheets similar to those used to customize visual components in environments like Visual Basic, PowerBuilder, JBuilder, and Symantec Cafй. Figure 4-4 shows the deployment wizard used in the J2EE Reference Implementation.
The J2EE Reference Implementation's deployment wizard has fields and panels that match the XML deployment descriptor. You can map security roles to users groups, set the JNDI look up name, map the container-managed fields to the database, etc. EJB 1.0 deployment tools A deployment tool reads the JAR file and uses the manifest to locate the bean's serialized deployment descriptor. Once the deployment descriptor file has been located, it is deserialized into an object and its properties are read by invoking its get methods. In a graphical deployment wizard, these properties will be presented to the deployer in a set of property sheets, similar to those used to customize visual components in environments like Visual Basic, PowerBuilder, JBuilder, and Symantec Cafe. Figure 4-5 shows the deployment wizard used in BEA's WebLogic EJB server.
The WebLogic deployment wizard has fields and panels that match properties and deployment classes specified in the javax.ejb.deployment package. The "CabinBean" tab, for example, contains text fields for each of the interfaces and classes that we described in the Cabin bean's EntityDescriptor, the CabinDD.ser. There is also an "Access control" tab that corresponds to the AccessControlEntry class, and a "Control descriptors" tab that corresponds to the ControlDescriptor class. In addition, there is a "Container-managed fields" tab that shows the container-managed fields we defined when creating the CabinDD.ser. Graphical deployment wizards provided by other EJB products will look different but provide the same kinds of features. At this point, you can choose to change deployment information, such as the transactional isolation level, to change the Cabin bean's JNDI name, or to deselect one of the container-managed fields. You can also add properties to the deployment descriptor, for example, by setting the AccessControlEntrys for the methods and adding environment properties. The CabinDD.ser that we created should have specified the minimum information that most EJB servers need to deploy the bean without changes. It is likely that all you will need to do is specify the persistence mapping from the container-managed fields to the CABIN table in the relational database. Different EJB deployment tools will provide varying degrees of support for mapping container-managed fields to a data source. Some provide very robust and sophisticated graphical user interfaces, while others are simpler and less flexible. Fortunately, mapping the CabinBean's container-managed fields to the CABIN table is a fairly straightforward process. Read the documentation for the deployment tool provided by your EJB vendor to determine how to do this mapping. Once you have finished the mapping, you can complete the deployment of the bean and prepare to access it from the EJB server. Creating a Client Application Now that the Cabin bean has been deployed in the EJB server, we want to access it from a remote client. When we say remote, we are not necessarily talking about a client application that is located on a different computer, just one that is not part of the EJB server. In this section, we will create a remote client that will connect to the EJB server, locate the EJB home for the Cabin bean, and create and interact with several Cabin beans. The following code shows a Java application that is designed to create a new Cabin bean, set its name, deckLevel, ship, and bedCount properties, and then locate it again using its primary key:
package com.titan.cabin;
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 java.rmi.RemoteException;
import java.util.Properties;
public class Client_1 {
public static void main(String [] args) {
try {
Context jndiContext = getInitialContext();
Object ref =
jndiContext.lookup("CabinHome");
CabinHome home = (CabinHome)
// EJB 1.0:Use Java cast instead of narrow( )
PortableRemoteObject.narrow(ref,CabinHome.class);
Cabin cabin_1 = home.create(1);
cabin_1.setName("Master Suite");
cabin_1.setDeckLevel(1);
cabin_1.setShip(1);
cabin_1.setBedCount(3);
CabinPK pk = new CabinPK();
pk.id = 1;
Cabin cabin_2 = home.findByPrimaryKey(pk);
System.out.println(cabin_2.getName());
System.out.println(cabin_2.getDeckLevel());
System.out.println(cabin_2.getShip());
System.out.println(cabin_2.getBedCount());
} catch (java.rmi.RemoteException re){re.printStackTrace();}
catch (javax.naming.NamingException ne){ne.printStackTrace();}
catch (javax.ejb.CreateException ce){ce.printStackTrace();}
catch (javax.ejb.FinderException fe){fe.printStackTrace();}
}
public static Context getInitialContext()
throws javax.naming.NamingException {
Properties p = new Properties();
// ... Specify the JNDI properties specific to the vendor.
return new javax.naming.InitialContext(p);
}
}
To access an enterprise bean, a client starts by using the JNDI package to obtain a directory connection to a bean's container. JNDI is an implementation-independent API for directory and naming systems. Every EJB vendor must provide directory services that are JNDI-compliant. This means that they must provide a JNDI service provider, which is a piece of software analogous to a driver in JDBC. Different service providers connect to different directory services--not unlike JDBC, where different drivers connect to different relational databases. The method getInitialContext() contains logic that uses JNDI to obtain a network connection to the EJB server. The code used to obtain the JNDI Context will be different depending on which EJB vendor you are using. You will need to research your EJB vendor's requirements for obtaining a JNDI Context appropriate to that product. The code used to obtain a JNDI Context in Gemstone/J, for example, might look something like the following:
public static Context getInitialContext() throws javax.naming.NamingException {
Properties p = new Properties();
p.put(com.gemstone.naming.Defaults.NAME_SERVICE_HOST,"localhost");
String port = System.getProperty("com.gemstone.naming.NameServicePort",
"10200");
p.put(com.gemstone.naming.Defaults.NAME_SERVICE_PORT, port);
p.put(Context.INITIAL_CONTEXT_FACTORY,"com.gemstone.naming.GsCtxFactory");
return new InitialContext(p);
}
The same method developed for BEA's WebLogic Server would be different:
public static Context getInitialContext()
throws javax.naming.NamingException {
Properties p = new Properties();
p.put(Context.INITIAL_CONTEXT_FACTORY,
"weblogic.jndi.TengahInitialContextFactory");
p.put(Context.PROVIDER_URL, "t3://localhost:7001");
return new javax.naming.InitialContext(p);
}
Once a JNDI connection is established and a context is obtained from the getIntialContext() method, the context can be used to look up the EJB home of the Cabin bean: EJB 1.1: Obtaining a remote reference to the home interface The previous example uses the PortableRemoteObject.narrow() method as prescribed in EJB 1.1:
Object ref = jndiContext.lookup("CabinHome");
CabinHome home = (CabinHome)
// EJB 1.0: Use Java cast instead of narrow()
PortableRemoteObject.narrow(ref,CabinHome.class);
The PortableRemoteObject.narrow() method is new to EJB 1.1. It is needed to support the requirements of RMI over IIOP. Because CORBA supports many different languages, casting is not native to CORBA (some languages don't have casting). Therefore, to get a remote reference to CabinHome, we must explicitly narrow the object returned from lookup(). This has the same effect as casting and is explained in more detail in Chapter 5. The name used to find the Cabin bean's EJB home is set by the deployer using a deployment wizard like the one pictured earlier. The JNDI name is entirely up to the person deploying the bean; it can be the same as the bean name set in the XML deployment descriptor or something completely different. EJB 1.0: Obtaining a remote reference to the home interface In EJB 1.0, you do not need to use the PortableRemoteObject.narrow() method to cast objects to the correct type. EJB 1.0 allows the use of Java native casting to narrow the type returned by the JNDI API to the home interface type. When you see the PortableRemoteObject being used, replace it with Java native casting as follows:
CabinHome home = (CabinHome)jndiContext.lookup("CabinHome");
To locate the EJB home, we specify the name that we set using the DeploymentDescriptor.setBeanHomeName(String name) method in the MakeDD application earlier. If this lookup succeeds, the home variable will contain a remote reference to the Cabin bean's EJB home. Creating a new Cabin bean Once we have a remote reference to the EJB home, we can use it to create a new Cabin entity:
Cabin cabin_1 = home.create(1);
We create a new Cabin entity using the create(int id) method defined in the home interface of the Cabin bean. When this method is invoked, the EJB home works with the EJB server to create a Cabin bean, adding its data to the database. The EJB server then creates an EJB object to wrap the Cabin bean instance and returns a remote reference to the EJB object to the client. The cabin_1 variable then contains a remote reference to the Cabin bean we just created.
With the remote reference to the EJB object, we can update the name, deckLevel, ship, and bedCount of the Cabin entity:
Cabin cabin_1 = home.create(1);
cabin_1.setName("Master Suite");
cabin_1.setDeckLevel(1);
cabin_1.setShip(1);
cabin_1.setBedCount(3);
Figure 4-6 shows how the relational database table that we created should look after executing this code. It should contain one record.
After an entity bean has been created, a client can locate it using the findByPrimaryKey() method in the home interface. First, we create a primary key of the correct type, in this case CabinPK, and set its field id to equal the id of the cabin we want. (So far, we only have one cabin available to us.) When we invoke this method on the home interface, we get back a remote reference to the EJB object. We can now interrogate the remote reference returned by findByPrimaryKey() to get the Cabin entity's name, deckLevel, ship, and bedCount:
CabinPK pk = new CabinPK();
pk.id = 1;
Cabin cabin_2 = home.findByPrimaryKey(pk);
System.out.println(cabin_2.getName());
System.out.println(cabin_2.getDeckLevel());
System.out.println(cabin_2.getShip());
System.out.println(cabin_2.getBedCount());
Copy and save the Client_1 application to any directory, and compile it. If you haven't started your EJB server and deployed the Cabin bean, do so now. When you're finished, you're ready to run the Client_1 in your IDE's debugger so that you can watch each step of the program. Your output should look something like the following:
Master Suite
1
1
3
You just created and used your first entity bean! Of course, the client application doesn't do much. Before going on to create session beans, create another client that adds some test data to the database. Here we'll create Client_2 as a modification of Client_1 that populates the database with a large number of cabins for three different ships:
package com.titan.cabin;
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_2 {
public static void main(String [] args) {
try {
Context jndiContext = getInitialContext();
Object ref =
jndiContext.lookup("CabinHome");
CabinHome home = (CabinHome)
// EJB 1.0: Use Java native cast
PortableRemoteObject.narrow(ref,CabinHome.class);
// Add 9 cabins to deck 1 of ship 1.
makeCabins(home, 2, 10, 1, 1);
// Add 10 cabins to deck 2 of ship 1.
makeCabins(home, 11, 20, 2, 1);
// Add 10 cabins to deck 3 of ship 1.
makeCabins(home, 21, 30, 3, 1);
// Add 10 cabins to deck 1 of ship 2.
makeCabins(home, 31, 40, 1, 2);
// Add 10 cabins to deck 2 of ship 2.
makeCabins(home, 41, 50, 2, 2);
// Add 10 cabins to deck 3 of ship 2.
makeCabins(home, 51, 60, 3, 2);
// Add 10 cabins to deck 1 of ship 3.
makeCabins(home, 61, 70, 1, 3);
// Add 10 cabins to deck 2 of ship 3.
makeCabins(home, 71, 80, 2, 3);
// Add 10 cabins to deck 3 of ship 3.
makeCabins(home, 81, 90, 3, 3);
// Add 10 cabins to deck 4 of ship 3.
makeCabins(home, 91, 100, 4, 3);
for (int i = 1; i <= 100; i++){
CabinPK pk = new CabinPK();
pk.id = i;
Cabin cabin = home.findByPrimaryKey(pk);
System.out.println("PK = "+i+", Ship = "+cabin.getShip()
+ ", Deck = "+cabin.getDeckLevel()
+ ", BedCount = "+cabin.getBedCount()
+ ", Name = "+cabin.getName());
}
} catch (java.rmi.RemoteException re) {re.printStackTrace();}
catch (javax.naming.NamingException ne) {ne.printStackTrace();}
catch (javax.ejb.CreateException ce) {ce.printStackTrace();}
catch (javax.ejb.FinderException fe) {fe.printStackTrace();}
}
public static 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 static void makeCabins(CabinHome home, int fromId, int toId,
int deckLevel, int shipNumber)
throws RemoteException, CreateException {
int bc = 3;
for (int i = fromId; i <= toId; i++) {
Cabin cabin = home.create(i);
int suiteNumber = deckLevel*100+(i-fromId);
cabin.setName("Suite "+suiteNumber);
cabin.setDeckLevel(deckLevel);
bc = (bc==3)?2:3;
cabin.setBedCount(bc);
cabin.setShip(shipNumber);
}
}
}
Copy this code into your IDE, save, and recompile the Client_2 application. When it compiles successfully, run it. There's lots of output--here are the first few lines:
PK = 1, Ship = 1, Deck = 1, BedCount = 3, Name = Master Suite
PK = 2, Ship = 1, Deck = 1, BedCount = 2, Name = Suite 100
PK = 3, Ship = 1, Deck = 1, BedCount = 3, Name = Suite 101
PK = 4, Ship = 1, Deck = 1, BedCount = 2, Name = Suite 102
PK = 5, Ship = 1, Deck = 1, BedCount = 3, Name = Suite 103
PK = 6, Ship = 1, Deck = 1, BedCount = 2, Name = Suite 104
PK = 7, Ship = 1, Deck = 1, BedCount = 3, Name = Suite 105
...
You now have 100 cabin records in your CABIN table, representing 100 cabin entities in your EJB system. This provides a good set of test data for the session bean we will create in the next section, and for subsequent examples throughout the book. |
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 Chapters and , 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 Figure 4-7).
TravelAgent: The Remote Interface 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 Strings. 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. TravelAgentHome: The Home Interface 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. TravelAgentBean: The Bean Class 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. EJB 1.1: TravelAgentBean 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. EJB 1.0: TravelAgentBean 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. EJB 1.1: TravelAgent Bean's Deployment Descriptor 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" "https://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. EJB 1.0: The TravelAgent Beans' 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 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
EJB 1.1: The JAR File 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. EJB 1.0: The JAR File 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
The TravelAgent bean is now complete and ready to be deployed. Deploying the TravelAgent Bean 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. Creating a Client Application 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. 1. Chapter 9 discusses how to work with servers that don't support entity beans. Chapter 6 includes a discussion of bean-managed persistence, which you can use if your server doesn't support container-managed persistence. 2. In Chapters and , we discuss implementing the hashCode()and equals()methods in more detail. 3. 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. Contribute to IDR: To contribute an article to IDR, a click here.
|
|