Chapter 4. Developing Your First Enterprise Beans
4.1 Choosing and Setting Up an EJB ServerOne of the most important features of EJB is that enterprise beans work with containers from different vendors. However, that doesn't mean that selecting a server and installing your enterprise beans on that server are trivial processes.[1] The EJB server you choose should provide a utility for deploying an enterprise bean. It doesn't matter whether the utility is command-line oriented or graphical, as long as it does the job. The deployment utility should allow you to work with prepackaged enterprise beans, i.e., enterprise beans that have already been developed and archived in a JAR file. Finally, the EJB server should support an SQL-standard relational database that is accessible using JDBC. For the database, you should have privileges sufficient for creating and modifying a few simple tables in addition to normal read, update, and delete capabilities. If you have chosen an EJB server that does not support an SQL-standard relational database, you may need to modify the examples to work with the product you are using. This book does not say very much about how to install and deploy enterprise beans. That task is largely server-dependent. I'll provide some general ideas about how to organize JAR files and create deployment descriptors, but for a complete description of the deployment process, you'll have to refer to your vendor's documentation or look at the workbook for your vendor. This chapter provides you with your first opportunities to use a workbook. Throughout the rest of this book, you will see callouts that direct you to exercises in the workbook. A callout will look something like the following: Please refer to Workbook Exercise 4.2, A Simple Session Bean. This workbook is available free, in PDF format, at http://www.oreilly.com/catalog/entjbeans3/workbooks. As was mentioned in the Preface, the workbooks can be downloaded in PDF format from http://www.oreilly.com/catalog/entjbeans3/ or http://www.titan-books.com—some workbooks may even be available in paper book form and can be ordered directly from http://www.titan-books.com. 4.1.1 Setting Up Your Java IDETo get the most from this chapter, it helps to have an IDE that has a debugger and allows you to add Java files to its environment. Several Java IDEs—such as WebGain's Visual Cafe, IBM's VisualAge, Borland's JBuilder, and Sun's Forte—fulfill this simple requirement. Some EJB products, such as IBM's WebSphere, are tightly coupled with an IDE that makes life a lot easier when it comes to writing, deploying, and debugging your applications. Once you have an IDE set up, you need to include the Enterprise JavaBeans package, javax.ejb. You also need the JNDI packages, including javax.naming, javax.naming.directory, and javax.naming.spi. In addition, you will need the javax.rmi and javax.jms packages. All these packages can be downloaded from Sun's Java site (http://www.javasoft.com) in the form of ZIP or JAR files. They may also be accessible in the subdirectories of your EJB server, normally under the lib directory. 4.2 Developing an Entity BeanThere seems to be no better place to start than the Cabin EJB, which we have been examining throughout the previous chapters. The Cabin EJB is an entity bean that encapsulates the data and behavior associated with a cruise ship cabin in Titan's business domain. 4.2.1 Cabin: The Remote InterfaceWhen developing an entity bean, we first want to define the enterprise bean's remote interface. The remote interface defines the enterprise bean's business purpose; the methods of this interface must capture the concept of the entity. We defined the remote interface for the Cabin EJB in Chapter 2; here, we add two new methods for setting and getting the ship ID and the bed count. The ship ID identifies the ship to which the cabin belongs, and the bed count tells how many people the cabin can accommodate: package com.titan.cabin; import java.rmi.RemoteException; public interface CabinRemote extends javax.ejb.EJBObject { public String getName() throws RemoteException; public void setName(String str) throws RemoteException; public int getDeckLevel() throws RemoteException; public void setDeckLevel(int level) throws RemoteException; public int getShipId() throws RemoteException; public void setShipId(int sp) throws RemoteException; public int getBedCount() throws RemoteException; public void setBedCount(int bc) throws RemoteException; } The CabinRemote interface defines four properties: the name, deckLevel, shipId, and bedCount. Properties are attributes of an enterprise bean that can be accessed by public set and get methods. The methods that access these properties are not explicitly defined in the CabinRemote interface, but the interface clearly specifies that these attributes are readable and changeable by a client. Notice that we have made the CabinRemote interface a part of a new package named com.titan.cabin. Place all the classes and interfaces associated with each type of bean in a package specific to the bean.[2] Because our beans are for the use of the Titan cruise line, we placed these packages in the com.titan package hierarchy. We also created directory structures that match package structures. If you are using an IDE that works directly with Java files, create a new directory somewhere called dev (for development) and create the directory structure shown in Figure 4-1. Copy the CabinRemote interface into your IDE and save its definition to the cabin directory. Compile the CabinRemote interface to ensure that its definition is correct. The CabinRemote.class file, generated by the IDE's compiler, should be written to the cabin directory, the same directory as the CabinRemote.java file. The rest of the Cabin bean's classes will be placed in this same directory. Figure 4-1. Directory structure for the Cabin bean
4.2.2 CabinHome: The Remote Home InterfaceOnce we have defined the remote interface of the Cabin EJB, we have defined the remote view of this simple entity bean. Next, we need to define the Cabin EJB's remote home interface, which specifies how the enterprise bean can be created, located, and destroyed by remote clients; in other words, the Cabin EJB's life-cycle behavior. Here is a complete definition of the CabinHomeRemote home interface: package com.titan.cabin; import java.rmi.RemoteException; import javax.ejb.CreateException; import javax.ejb.FinderException; public interface CabinHomeRemote extends javax.ejb.EJBHome { public CabinRemote create(Integer id) throws CreateException, RemoteException; public CabinRemote findByPrimaryKey(Integer pk) throws FinderException, RemoteException; } The CabinHomeRemote interface extends the javax.ejb.EJBHome and defines two life-cycle methods: create() and findByPrimaryKey(). These methods create and locate remote references to Cabin EJBs. Remove methods (for deleting enterprise beans) are defined in the javax.ejb.EJBHome interface, so the CabinHomeRemote interface inherits them. 4.2.3 CabinBean: The Bean ClassWe have now defined the complete client-side API for creating, locating, removing, and using the Cabin EJB. Now we need to define CabinBean, the class that provides the implementation on the server for the Cabin EJB. The CabinBean class is an entity bean that uses container-managed persistence, so its definition will be fairly simple. In addition to the callback methods discussed in Chapter 2 and Chapter 3, we must also define accessor methods for the CabinRemote interface and an implementation of the create method defined in the CabinHomeRemote interface. Throughout this book we will show both the EJB 2.0 and EJB 1.1 code when they are different. In many cases the component interfaces are the same, but the bean class code and XML deployment descriptors will be different. This is the case with the Cabin EJB, so two code listings are shown for the bean class: the first is for EJB 2.0, and the second is for EJB 1.1. EJB 2.0 readers should ignore the EJB 1.1 listing and EJB 1.1 readers should ignore the EJB 2.0 code listing. 4.2.3.1 EJB 2.0: The CabinBean classHere is the complete definition of the CabinBean class in EJB 2.0: package com.titan.cabin; import javax.ejb.EntityContext; public abstract class CabinBean implements javax.ejb.EntityBean { public Integer ejbCreate(Integer id){ this.setId(id); returns null; } public void ejbPostCreate(Integer id){ } public abstract void setId(Integer id); public abstract Integer getId(); public abstract void setShipId(int ship); public abstract int getShipId(); public abstract void setName(String name); public abstract String getName(); public abstract void setBedCount(int count); public abstract int getBedCount(); public abstract void setDeckLevel(int level); public abstract int getDeckLevel(); public void setEntityContext(EntityContext ctx) { // Not implemented. } public void unsetEntityContext() { // Not implemented. } public void ejbActivate() { // Not implemented. } public void ejbPassivate() { // Not implemented. } public void ejbLoad() { // Not implemented. } public void ejbStore() { // Not implemented. } public void ejbRemove() { // Not implemented. } } The CabinBean class can be divided into four sections for discussion: declarations for the container-managed fields, the ejbCreate()/ejbPostCreate() methods, the callback methods, and the remote-interface implementations. The CabinBean defines several abstract accessor methods that appear in pairs. For example, setName() and getName() are a pair of abstract accessor methods. These methods are responsible for setting and getting the entity bean's name field. When the bean is deployed, the EJB container automatically implements all the abstract accessor methods so that the bean state can be synchronized with the database. These implementations map the abstract accessor methods to fields in the database. Although all the abstract accessor methods have corresponding methods in the remote interface, CabinRemote, it's not necessary that they do so. Some accessor methods are for the entity bean's use only and are never exposed to the client through the remote or local interfaces. It's customary in EJB 2.0 to consider the abstract accessor methods as providing access to virtual fields and to refer to those fields by their method names, less the get or set prefix. For example, the getName()/setName() abstract accessor methods define a virtual container-managed persistence (CMP) field called name (the first letter is always changed to lowercase). The getDeckLevel()/setDeckLevel() abstract accessor methods define a virtual CMP field called deckLevel, and so on. The name, deckLevel, shipId, and bedCount virtual CMP fields represent the Cabin EJB's persistent state. They will be mapped to the database at deployment time. These fields are also publicly available through the entity bean's remote interface. Invoking the getBedCount() method on a CabinRemote EJB object at runtime causes the container to delegate that call to the corresponding getBedCount() method on the CabinBean instance. Unlike the matching methods in the remote interface, the abstract accessor methods do not throw RemoteExceptions. There is no requirement that CMP fields must be exposed. The id field is another container-managed field, but its abstract accessor methods are not exposed to the client through the CabinRemote interface. This field is the primary key of the Cabin EJB; it's the entity bean's index to its data in the database. It's bad practice to expose the primary key of an entity bean because you don't want client applications to be able to change that key. 4.2.3.2 EJB 1.1: The CabinBean classHere is the complete definition of the CabinBean class in EJB 1.1: package com.titan.cabin; import javax.ejb.EntityContext; public class CabinBean implements javax.ejb.EntityBean { public Integer id; public String name; public int deckLevel; public int shipId; public int bedCount; public Integer ejbCreate(Integer id) { this.id = id; return null; } public void ejbPostCreate(Integer id) { // Do nothing. Required. } public String getName() { return name; } public void setName(String str) { name = str; } public int getShipId() { return shipId; } public void setShipId(int sp) { shipId = sp; } public int getBedCount() { return bedCount; } public void setBedCount(int bc) { bedCount = bc; } public int getDeckLevel() { return deckLevel; } public void setDeckLevel(int level ) { deckLevel = level; } public void setEntityContext(EntityContext ctx) { // Not implemented. } public void unsetEntityContext() { // Not implemented. } public void ejbActivate() { // Not implemented. } public void ejbPassivate() { // Not implemented. } public void ejbLoad() { // Not implemented. } public void ejbStore() { // Not implemented. } public void ejbRemove() { // Not implemented. } } Declared fields in a bean class can be persistent fields or property fields. These categories are not mutually exclusive. Persistent field declarations describe the fields that will be mapped to the database. A persistent field is often a property (in the JavaBeans sense): any attribute that is available using public set and get methods. Of course, a bean can have any fields that it needs; they need not all be persistent, or properties. Fields that aren't persistent won't be saved in the database. In CabinBean, all the fields are persistent. The id field is persistent, but it is not a property. In other words, id is mapped to the database but cannot be accessed through the remote interface. The name, deckLevel, shipId, and bedCount fields are also persistent fields. They will be mapped to the database at deployment time. These fields are also properties, because they are publicly available through the remote interface. 4.2.3.3 EJB 2.0 and 1.1: The callback methodsIn the case of the Cabin EJB, there was only one create() method, so there is only one corresponding ejbCreate() method and one ejbPostCreate() method. When a client invokes the create() method on the remote home interface, it is delegated to a matching ejbCreate() method on the entity bean instance. The ejbCreate() method initializes the fields; in the case of the CabinBean, it sets the name field. The ejbCreate() method always returns the primary key type; with container-managed persistence, this method returns the null value. It's the container's responsibility to create the primary key. Why does it return null? Simply put, this convention makes it easier for a bean-managed enterprise bean (i.e., an enterprise bean that explicitly manages its own persistence) to extend a container-managed enterprise bean. This functionality is valuable for EJB vendors who support container-managed persistence beans by extending them with bean-managed persistence bean implementations—it's a technique that is more common in EJB 1.1. Bean-managed persistence beans, which are covered in Chapter 10, always return the primary key type. Once the ejbCreate() method has executed, the ejbPostCreate() method is called to perform any follow-up operations. The ejbCreate() and ejbPostCreate() methods must have signatures that match the parameters and (optionally) the exceptions of the home interface's create() method. The ejbPostCreate() method is used to perform any postprocessing on the bean after it is created, but before it can be used by the client. Both methods will execute, one right after the other, when the client invokes the create() method on the remote home interface. The findByPrimaryKey() method is not defined in container-managed bean classes. Instead, find methods are generated at deployment and implemented by the container. With bean-managed entity beans, find methods must be defined in the bean class. In Chapter 10, when you develop bean-managed entity beans, you will define the find methods in the bean classes you develop. The CabinBean class implements javax.ejb.EntityBean, which defines seven callback methods: setEntityContext(), unsetEntityContext(), ejbActivate(), ejbPassivate(), ejbLoad(), ejbStore(), and ejbRemove(). The container uses these callback methods to notify the CabinBean of certain events in its life cycle. Although the callback methods are implemented, the implementations are empty. The CabinBean is simple enough that it doesn't need to do any special processing during its life cycle. When we study entity beans in more detail in Chapter 6 through Chapter 11, we will take advantage of these callback methods. 4.2.4 The Deployment DescriptorYou are now ready to create a deployment descriptor for the Cabin EJB. The deployment descriptor performs a function similar to a properties file. It describes which classes make up a enterprise bean and how the enterprise bean should be managed at runtime. During deployment, the deployment descriptor is read and its properties are displayed for editing. The deployer can then modify and add settings as appropriate for the application's operational environment. Once the deployer is satisfied with the deployment information, she uses it to generate the entire supporting infrastructure needed to deploy the enterprise bean in the EJB server. This may include resolving enterprise bean references, adding the enterprise bean to the naming system, and generating the enterprise bean's EJB object and EJB home, persistence infrastructure, transactional support, and so forth. Although most EJB server products provide a wizard for creating and editing deployment descriptors, we will create ours directly so that the enterprise bean is defined in a vendor-independent manner.[3] This requires some manual labor, but it gives you a much better understanding of how deployment descriptors are created. Once the deployment descriptor is finished, the enterprise bean can be placed in a JAR file and deployed on any EJB-compliant server of the appropriate version. An XML deployment descriptor for every example in this book has already been created and is available from the download site. The following sections offer a quick peek at the EJB 2.0 and 1.1 deployment descriptors for the Cabin EJB, so you can get a feel for how an XML deployment descriptor is structured and the type of information it contains. 4.2.4.1 EJB 2.0: The Cabin EJB's deployment descriptorIn EJB 2.0, the deployment descriptor looks like this: <!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 2.0//EN" "http://java.sun.com/dtd/ejb-jar_2_0.dtd"> <ejb-jar> <enterprise-beans> <entity> <ejb-name>CabinEJB</ejb-name> <home>com.titan.cabin.CabinHomeRemote</home> <remote>com.titan.cabin.CabinRemote</remote> <ejb-class>com.titan.cabin.CabinBean</ejb-class> <persistence-type>Container</persistence-type> <prim-key-class>java.lang.Integer</prim-key-class> <reentrant>False</reentrant> <abstract-schema-name>Cabin</abstract-schema-name> <cmp-field><field-name>id</field-name></cmp-field> <cmp-field><field-name>name</field-name></cmp-field> <cmp-field><field-name>deckLevel</field-name></cmp-field> <cmp-field><field-name>shipId</field-name></cmp-field> <cmp-field><field-name>bedCount</field-name></cmp-field> <primkey-field>id</primkey-field> <security-identity><use-caller-identity/></security-identity> </entity> </enterprise-beans> <assembly-descriptor> ... </assembly-descriptor> </ejb-jar> 4.2.4.2 EJB 1.1: The Cabin EJB's deployment descriptorIn EJB 1.1, the deployment descriptor looks like this: <!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> <entity> <ejb-name>CabinEJB</ejb-name> <home>com.titan.cabin.CabinHomeRemote</home> <remote>com.titan.cabin.CabinRemote</remote> <ejb-class>com.titan.cabin.CabinBean</ejb-class> <persistence-type>Container</persistence-type> <prim-key-class>java.lang.Integer</prim-key-class> <reentrant>False</reentrant> <cmp-field><field-name>id</field-name></cmp-field> <cmp-field><field-name>name</field-name></cmp-field> <cmp-field><field-name>deckLevel</field-name></cmp-field> <cmp-field><field-name>shipId</field-name></cmp-field> <cmp-field><field-name>bedCount</field-name></cmp-field> <primkey-field>id</primkey-field> </entity> </enterprise-beans> <assembly-descriptor> ... </assembly-descriptor> </ejb-jar> 4.2.4.3 EJB 2.0 and 1.1: Defining the XML elementsThe <!DOCTYPE> element describes the purpose of the XML file, its root element, and the location of its DTD. The DTD is used to verify that the document is structured correctly. This element is discussed in detail in Chapter 16. One important distinction between EJB 2.0 and EJB 1.1 is that they use different DTDs for deployment descriptors. EJB 2.0 specifies the ejb-jar_2_0.dtd, while EJB 1.1 specifies the ejb-jar_1_1.dtd. The rest of the XML elements are nested one within another and are delimited by beginning and ending tags. The structure is not complicated. If you have done any HTML coding, you should already understand the format. An element always starts with a <name_of_tag> tag and ends with a </name_of_tag> tag. Everything in between—even other elements—is part of the enclosing element. The first major element is the <ejb-jar> element, which is the root of the document. All the other elements must lie within this element. Next is the <enterprise-beans> element. Every bean declared in an XML file must be included in this section. This file describes only the Cabin EJB, but we could define several beans in one deployment descriptor. The <entity> element shows that the beans defined within this tag are entity beans. Similarly, a <session> element describes session beans; since the Cabin EJB is an entity bean, we don't need a <session> element. In addition to a description, the <entity> element provides the fully qualified class names of the remote interface, home interface, bean class, and primary key. The <cmp-field> elements list all the container-managed fields in the entity bean class. These are the fields that will persist in the database and be managed by the container at runtime. The <entity> element also includes a <reentrant> element that can be set as True or False depending on whether the bean allows reentrant loopbacks or not. EJB 2.0 specifies a name, which is used in EJB QL (Query Language) to identify the entity bean. This isn't important right now. The 2.0 deployment descriptor also specifies the <security-identity> as <use-caller-identity/>, which simply means the bean will propagate the calling client's security identity when it accesses resources or other beans. This was covered in detail in Chapter 3. The section of the XML file after the <enterprise-beans> element is enclosed by the <assembly-descriptor> element, which describes the security roles and transaction attributes of the bean. This section of the XML file is the same for both EJB 2.0 and EJB 1.1 in this example: <ejb-jar> <enterprise-beans> ... <enterprise-beans> <assembly-descriptor> <security-role> <description> This role represents everyone who is allowed full access to the Cabin EJB. </description> <role-name>everyone</role-name> </security-role> <method-permission> <role-name>everyone</role-name> <method> <ejb-name>CabinEJB</ejb-name> <method-name>*</method-name> </method> </method-permission> <container-transaction> <method> <ejb-name>CabinEJB</ejb-name> <method-name>*</method-name> </method> <trans-attribute>Required</trans-attribute> </container-transaction> </assembly-descriptor> </ejb-jar> It may seem odd to separate the <assembly-descriptor> information from the <enterprise-beans> information, since it clearly applies to the Cabin EJB, but in the scheme of things it's perfectly natural. A single XML deployment descriptor can describe several beans, which might all rely on the same security roles and transaction attributes. To make it easier to deploy several beans together, all this common information is grouped in the <assembly-descriptor> element. There is another (perhaps more important) reason for separating information about the bean itself from the security roles and transaction attributes. Enterprise JavaBeans defines the responsibilities of different participants in the development and deployment of beans. We don't address these development roles in this book because they are not critical to learning the fundamentals of EJB. For now, it's enough to know that the person who develops the beans and the person who assembles the beans into an application have separate responsibilities and therefore deal with separate parts of the XML deployment descriptor. The bean developer is responsible for everything within the <enterprise-beans> element; the bean assembler is responsible for everything within the <assembly-descriptor>. Throughout this book you will play both roles, developing the beans and assembling them. Other roles you will fill are that of the deployer, who is the person who actually loads the enterprise beans into the EJB container; and the administrator, who is responsible for tuning the EJB server and managing it at runtime. In real projects, all these roles may be filled by one or two people, or by several different individuals or even teams. The <assembly-descriptor> contains the <security-role> elements and their corresponding <method-permission> elements, which were described in Chapter 3 under Section 3.2.7. In this example, there is one security role, everyone, which is mapped to all the methods in the Cabin EJB using the <method-permission> element. (The * in the <method-name> element means "all methods.") As already mentioned, for EJB 2.0 you'll have to specify a security identity; in this case, it's the caller's identity. The <container-transaction> element declares that all the methods of the Cabin EJB have a Required transaction attribute, which means that all the methods must be executed within a transaction. Transaction attributes are explained in more detail in Chapter 14. The deployment descriptor ends with the closing tag of the <ejb-jar> element. Copy the Cabin EJB's deployment descriptor into the same directory as the class files for the Cabin EJB files (Cabin.class, CabinHome.class, CabinBean.class, and CabinPK.class) and save it as ejb-jar.xml. You have now created all the files you need to package your Cabin EJB. Figure 4-2 shows all the files that should be in the cabin directory. Figure 4-2. The Cabin EJB files
4.2.5 cabin.jar: The JAR FileThe JAR file is a platform-independent file format for compressing, packaging, and delivering several files together. Based on the ZIP file format and the ZLIB compression standards, the JAR ( Java archive) tool and packages were originally developed to make downloads of Java applets more efficient. As a packaging mechanism, however, the JAR file format is a very convenient way to "shrink-wrap" components and other software for delivery to third parties. The original JavaBeans component architecture depends on JAR files for packaging, as does Enterprise JavaBeans. The goal in using the JAR file format in EJB is to package all the classes and interfaces associated with a bean, including the deployment descriptor, into one file. Creating the JAR file for deployment is easy. Position yourself in the dev directory that is just above thecom/titan/cabin directory tree, and execute the following command: \dev % jar cf cabin.jar com/titan/cabin/*.class META-INF/ejb-jar.xml F:\..\dev>jar cf cabin.jar com\titan\cabin\*.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 c option tells the jar utility to create a new JAR file that contains the files indicated in subsequent parameters. It also tells the jar utility to stream the resulting JAR file to standard output. The f option tells jar to redirect the standard output to a new file named in the second parameter (cabin.jar). It's important to get the order of the option letters and the command-line parameters to match. You can learn more about the jar utility and the java.util.zip package in Java™ in a Nutshell by David Flanagan, or Learning Java™ by Pat Niemeyer and Jonathan Knudsen (both published by O'Reilly). The jar utility creates the file cabin.jar in the dev directory. If you're interested in looking at the contents of the JAR file, you can use any standard ZIP application (WinZip, PKZIP, etc.), or you can use the command jar tvfcabin.jar. 4.2.6 Creating a CABIN Table in the DatabaseOne of the primary jobs of a deployment tool is mapping entity beans to databases. In the case of the Cabin EJB, we must map its id, name, deckLevel, shipId, and bedCount container-managed fields to some data source. Before proceeding with deployment, you need to set up a database and create a CABIN table. You can use the following standard SQL statement to create a CABIN table that will be consistent with the examples provided in this chapter: create table CABIN ( ID int primary key NOT NULL, SHIP_ID int, BED_COUNT int, NAME char(30), DECK_LEVEL int ) This statement creates a CABIN table that has five columns corresponding to the container-managed fields in the CabinBean class. Once the table is created and connectivity to the database is confirmed, you can proceed with the deployment process. 4.2.7 Deploying the Cabin EJBDeployment 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 any 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 use. A deployment tool reads the JAR file and looks for the ejb-jar.xml file. In a graphical deployment wizard, the deployment descriptor elements are presented in a set of property sheets similar to those used to customize visual components in environments such as Visual Basic, PowerBuilder, JBuilder, and Symantec Café. Figure 4-3 shows the deployment wizard used in the J2EE 1.3 SDK (Reference Implementation) server. Figure 4-3. J2EE 1.3 SDK Reference Implementation's deployment wizard
The J2EE Reference Implementation's deployment wizard has fields and panels that match the XML deployment descriptor. You can map security roles to user groups, set the JNDI lookup name, map the container-managed fields to the database, etc. Different EJB deployment tools 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. The documentation for your vendor's deployment tool will show you how to create this mapping. Once you have finished the mapping, you can complete the deployment of the Cabin EJB and prepare to access it from the EJB server. 4.2.8 Creating a Client ApplicationNow that the Cabin EJB has been deployed in the EJB server, we want to access it from a remote client. When we say remote, we are usually talking about either a client application that is located on a different computer or a different process on the same computer. In this section, we will create a remote client that will connect to the EJB server, locate the EJB remote home for the Cabin EJB, and create and interact with several Cabin EJBs. The following code shows a Java application that is designed to create a new Cabin EJB, set its name, deckLevel, shipId, and bedCount properties, and then locate it again using its primary key: package com.titan.cabin; import com.titan.cabin.CabinHomeRemote; import com.titan.cabin.CabinRemote; import javax.naming.InitialContext; import javax.naming.Context; import javax.naming.NamingException; import java.rmi.RemoteException; import java.util.Properties; import javax.rmi.PortableRemoteObject; public class Client_1 { public static void main(String [] args) { try { Context jndiContext = getInitialContext(); Object ref = jndiContext.lookup("CabinHomeRemote"); CabinHomeRemote home = (CabinHomeRemote) PortableRemoteObject.narrow(ref,CabinHomeRemote.class); CabinRemote cabin_1 = home.create(new Integer(1)); cabin_1.setName("Master Suite"); cabin_1.setDeckLevel(1); cabin_1.setShipId(1); cabin_1.setBedCount(3); Integer pk = new Integer(1); CabinRemote cabin_2 = home.findByPrimaryKey(pk); System.out.println(cabin_2.getName()); System.out.println(cabin_2.getDeckLevel()); System.out.println(cabin_2.getShipId()); 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 service that is 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 getInitialContext() method contains logic that uses JNDI to obtain a network connection to the EJB server. The code used to obtain the JNDI Context differs depending on which EJB vendor you use. Consult your vendor's documentation to find out how to obtain a JNDI Context appropriate to your product. For example, the code used to obtain a JNDI Context in WebSphere might look something like the following: public static Context getInitialContext() throws javax.naming.NamingException { java.util.Properties properties = new java.util.Properties(); properties.put(javax.naming.Context.PROVIDER_URL, "iiop:///"); properties.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, "com.ibm.ejs.ns.jndi.CNInitialContextFactory"); return new InitialContext(properties); } 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.WLInitialContextFactory"); 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 getInitialContext() method, the context can be used to look up the EJB home of the Cabin EJB. The Client_1 application uses the PortableRemoteObject.narrow() method: Object ref = jndiContext.lookup("CabinHome"); CabinHome home = (CabinHomeRemote) PortableRemoteObject.narrow(ref,CabinHomeRemote.class); The PortableRemoteObject.narrow() method was first introduced in EJB 1.1 and continues to be used on remote clients in EJB 2.0. 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 CabinHomeRemote, 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 EJB'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. 4.2.8.1 Creating a new Cabin EJBOnce we have a remote reference to the EJB home, we can use it to create a new Cabin entity: CabinRemote cabin_1 = home.create(new Integer(1)); We create a new Cabin entity using the create(Integer id) method defined in the remote home interface of the Cabin EJB. When this method is invoked, the EJB home works with the EJB server to create a Cabin EJB, adding its data to the database. The EJB server then creates an EJB object to wrap the Cabin EJB 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 EJB we just created. We don't need to use the PortableRemoteObject.narrow() method to get the EJB object from the home reference, because it was declared as returning the CabinRemote type; no casting was required. We don't need to explicitly narrow remote references returned by findByPrimaryKey() for the same reason. With the remote reference to the EJB object, we can update the name, deckLevel, shipId, and bedCount of the Cabin EJB: CabinRemote cabin_1 = home.create(new Integer(1)); cabin_1.setName("Master Suite"); cabin_1.setDeckLevel(1); cabin_1.setShipId(1); cabin_1.setBedCount(3); Figure 4-4 shows how the relational database table we created should look after executing this code. It should contain one record. Figure 4-4. CABIN table with one cabin 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, Integer. When we invoke the finder method on the home interface using the primary key, we get back a remote reference to the EJB object. We can now interrogate the remote reference returned by findByPrimaryKey() to get the Cabin EJB's name, deckLevel, shipId, and bedCount: Integer pk = new Integer(1); CabinRemote cabin_2 = home.findByPrimaryKey(pk); System.out.println(cabin_2.getName()); System.out.println(cabin_2.getDeckLevel()); System.out.println(cabin_2.getShipId()); System.out.println(cabin_2.getBedCount()); You are now ready to create and run the Client_1 application against the Cabin EJB you deployed earlier. Compile the client application and deploy the Cabin EJB into the container system. Then run the Client_1 application. Please refer to Workbook Exercise 4.1, A Simple Entity Bean. This workbook is available free, in PDF format, at http://www.oreilly.com/catalog/entjbeans3/workbooks. When you run the Client_1 application, your output should look something like the following: Master Suite 1 1 3 Congratulations! 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.CabinHomeRemote; import com.titan.cabin.CabinRemote; import javax.naming.InitialContext; import javax.naming.Context; import javax.naming.NamingException; import javax.ejb.CreateException; import java.rmi.RemoteException; import java.util.Properties; import javax.rmi.PortableRemoteObject; public class Client_2 { public static void main(String [] args) { try { Context jndiContext = getInitialContext(); Object ref = jndiContext.lookup("CabinHomeRemote"); CabinHomeRemote home = (CabinHomeRemote) PortableRemoteObject.narrow(ref,CabinHomeRemote.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++){ Integer pk = new Integer(i); CabinRemote cabin = home.findByPrimaryKey(pk); System.out.println("PK = "+i+", Ship = "+cabin.getShipId() + ", 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(CabinHomeRemote home, int fromId, int toId, int deckLevel, int shipNumber) throws RemoteException, CreateException { int bc = 3; for (int i = fromId; i <= toId; i++) { CabinRemote cabin = home.create(new Integer(i)); int suiteNumber = deckLevel*100+(i-fromId); cabin.setName("Suite "+suiteNumber); cabin.setDeckLevel(deckLevel); bc = (bc==3)?2:3; cabin.setBedCount(bc); cabin.setShipId(shipNumber); } } } Create and run the Client_2 application against the Cabin EJB you deployed earlier. Client_2 produces a lot of output that lists all the new Cabin EJBs you just added to the database: 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. 4.3 Developing a Session BeanSession 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 certain tasks. Since we have defined only one entity bean so far, we will focus on a complex manipulation of the Cabin EJB rather than the interactions of the Cabin EJB with other entity beans. The interactions of entity beans within session beans will be explored in greater detail in Chapter 12. Client applications and other beans use the Cabin EJB in a variety of ways. Some of these uses were predictable when the Cabin EJB was defined, but many 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 may need to list and report on cabins in ways that were not predictable when the Cabin EJB was defined. Rather than change the Cabin EJB 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 be done only 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 EJB 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 EJB 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 EJB. 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 12, we will rewrite the listing method. The "list cabins" behavior developed here will be used by travel agents to provide customers with a list of cabins that can accommodate their needs. The Cabin EJB does not directly support this kind of list, nor should it. The list we need is specific to the TravelAgent EJB, so it's the TravelAgent EJB's responsibility to query the Cabin EJB and produce the list. You will need to create a development directory for the TravelAgent EJB, as we did for the Cabin EJB. We will name this directory travelagent and nest it below the /dev/com/titan directory, which also contains the cabin directory (see Figure 4-5). Figure 4-5. Directory structure for the TravelAgent EJB
You will be placing all the Java files and the XML deployment descriptor for the TravelAgent EJB into this directory. 4.3.1 TravelAgentRemote: The Remote InterfaceAs 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 EJB 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 need only a list of cabin names and deck levels, we'll define listCabins() to return an array of Strings. Here's the remote interface for TravelAgentRemote: package com.titan.travelagent; import java.rmi.RemoteException; import javax.ejb.FinderException; public interface TravelAgentRemote extends javax.ejb.EJBObject { // String elements follow the format "id, name, deck level" public String [] listCabins(int shipID, int bedCount) throws RemoteException; } 4.3.2 TravelAgentHomeRemote: The Remote Home InterfaceThe second step in the development of the TravelAgent EJB bean is to create the remote home interface. The remote 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 TravelAgentHomeRemote extends javax.ejb.EJBHome { public TravelAgentRemote create() throws RemoteException, CreateException; } In the case of the TravelAgent EJB, we need only a simple create() method to get a reference to the bean. Invoking this create() method returns the TravelAgent EJB's remote reference, which the client can use for the reservation process. 4.3.3 TravelAgentBean: The Bean ClassUsing 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: package com.titan.travelagent; import com.titan.cabin.CabinRemote; import com.titan.cabin.CabinHomeRemote; import java.rmi.RemoteException; import javax.naming.InitialContext; import javax.naming.Context; import java.util.Properties; import java.util.Vector; import javax.rmi.PortableRemoteObject; import javax.ejb.EJBException; 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/CabinHomeRemote"); CabinHomeRemote home = (CabinHomeRemote) PortableRemoteObject.narrow(obj,CabinHomeRemote.class); Vector vect = new Vector(); for (int i = 1; ; i++) { Integer pk = new Integer(i); CabinRemote cabin; try { cabin = home.findByPrimaryKey(pk); } catch(javax.ejb.FinderException fe) { break; } // Check to see if the bed count and ship ID match. if (cabin.getShipId() == 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 CabinHomeRemote: javax.naming.Context jndiContext = new InitialContext(); Object obj = jndiContext.lookup("java:comp/env/ejb/CabinHomeRemote"); CabinHomeRemote home = (CabinHomeRemote) javax.rmi.PortableRemoteObject.narrow(obj, CabinHomeRemote.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 Cabin EJB in the Client_1 and Client_2 applications we developed earlier. All beans have a default JNDI context called the environment naming context, which was discussed briefly 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 shortly when we take a look at the deployment descriptor for the TravelAgent EJB. As you learned in Chapter 2, enterprise beans in EJB 2.0 may have remote and/or local component interfaces. However, to keep things simple with this first set of examples, we are working with only the remote component interfaces—Chapter 5 will explain how this example may have been implemented with local interfaces. Once the remote EJB home of the Cabin EJB 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 EJBs and produces a list that includes only those cabins with the ship and bed count specified: Vector vect = new Vector(); for (int i = 1; ; i++) { Integer pk = new Integer(i); CabinRemote cabin; try { cabin = home.findByPrimaryKey(pk); } catch(javax.ejb.FinderException fe){ break; } // Check to see if the bed count and ship ID match. if (cabin.getShipId() == 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 EJB in the system and checking whether its shipId and bedCount match the parameters passed. The for loop continues until a FinderException is thrown, which will probably occur when a primary key that isn't associated with a bean is used. (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 EJBs—we will define a better method in Chapter 12—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 EJB, 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. 4.3.4 The TravelAgent EJB's Deployment DescriptorThe TravelAgent EJB uses an XML deployment descriptor similar to the one used for the Cabin entity bean. The following sections contain the ejb-jar.xml file used to deploy the TravelAgent bean in EJB 2.0 and 1.1, respectively. In Chapter 12, you will learn how to deploy several beans in one deployment descriptor, but for now the TravelAgent and Cabin EJBs are deployed separately. 4.3.4.1 EJB 2.0: Deployment descriptorIn EJB 2.0, the deployment descriptor for the TravelAgent EJB looks like this: <!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 2.0//EN" "http://java.sun.com/dtd/ejb-jar_2_0.dtd"> <ejb-jar> <enterprise-beans> <session> <ejb-name>TravelAgentEJB</ejb-name> <home>com.titan.travelagent.TravelAgentHomeRemote</home> <remote>com.titan.travelagent.TravelAgentRemote</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/CabinHomeRemote</ejb-ref-name> <ejb-ref-type>Entity</ejb-ref-type> <home>com.titan.cabin.CabinHomeRemote</home> <remote>com.titan.cabin.CabinRemote</remote> </ejb-ref> <security-identity><use-caller-identity/></security-identity> </session> </enterprise-beans> <assembly-descriptor> ... </assembly-descriptor> </ejb-jar> 4.3.4.2 EJB 1.1: Deployment descriptorIn EJB 1.1, the TravelAgent EJB's deployment descriptor looks like this: <!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>TravelAgentEJB</ejb-name> <home>com.titan.travelagent.TravelAgentHomeRemote</home> <remote>com.titan.travelagent.TravelAgentRemote</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.CabinHomeRemote</home> <remote>com.titan.cabin.CabinRemote</remote> </ejb-ref> </session> </enterprise-beans> <assembly-descriptor> ... </assembly-descriptor> </ejb-jar> 4.3.4.3 EJB 2.0 and 1.1: Defining the XML elementsThe only significant difference between the 2.0 and 1.1 deployment descriptors is the name of the DTD and the addition of a <security-identity> element in EJB 2.0, which simply propagates the caller's identity. Other than the <session-type> and <ejb-ref> elements, the TravelAgent EJB's XML deployment descriptor should be familiar: it uses many of the same elements as the Cabin EJB's. The <session-type> element can be Stateful or Stateless, to indicate which type of session bean is used. In this case we are defining a stateless session bean. The <ejb-ref> element is used at deployment time to map the bean references used within the TravelAgent EJB. In this case, the <ejb-ref> element describes the Cabin EJB, which we already deployed. The <ejb-ref-name> element specifies the name that must be used by the TravelAgent EJB to obtain a reference to the Cabin EJB'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 EJB in the EJB server. This is a vendor-specific process, but the outcome should always be the same. When the TravelAgent EJB does a JNDI lookup using the context name "java:comp/env/ejb/CabinHomeRemote", it will obtain a remote reference to the Cabin EJB's home. The purpose of the <ejb-ref> element is to eliminate network specific and implementation specific use of JNDI to obtain remote 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. However, as you will learn in Chapter 5, with EJB 2.0 it's always preferable to use local references instead of remote references when beans access each other within the same server. Local references are specified using the <ejb-local-ref> element, which looks just like the <ejb-ref> element. The <assembly-descriptor> section of the deployment descriptor is the same for both EJB 2.0 and EJB 1.1: <assembly-descriptor> <security-role> <description> This role represents everyone who is allowed full access to the Cabin EJB. </description> <role-name>everyone</role-name> </security-role> <method-permission> <role-name>everyone</role-name> <method> <ejb-name>TravelAgentEJB</ejb-name> <method-name>*</method-name> </method> </method-permission> <container-transaction> <method> <ejb-name>TravelAgentEJB</ejb-name> <method-name>*</method-name> </method> <trans-attribute>Required</trans-attribute> </container-transaction> </assembly-descriptor> 4.3.5 Deploying the TravelAgent EJBOnce you've defined the XML deployment descriptor, you are ready to place the TravelAgent EJB in its own JAR file and deploy it into the EJB server. To make your TravelAgent EJB 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 EJB 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 EJB is using the Cabin EJB and is not itself persistent. However, you will need to map the <ejb-ref> element in the TravelAgent EJB's deployment descriptor to the Cabin EJB. You EJB server's deployment tool will provide a mechanism for doing this. Deploy the TravelAgent EJB and proceed to the next section. Use the same process to JAR the TravelAgent EJB as was used for the Cabin EJB. Shrink-wrap the TravelAgent EJB class and its deployment descriptor into a JAR file and save the file 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 EJB is now complete and ready to be deployed. Next, use your EJB container's proprietary tools to deploy the TravelAgent EJB into the container system. 4.3.6 Creating a Client ApplicationTo 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 EJB: it creates a context for looking up TravelAgentHomeRemote, creates a TravelAgent EJB, and invokes listCabins() to generate a list of the cabins available. Here's the code: import com.titan.cabin.CabinHomeRemote; import com.titan.cabin.CabinRemote; import javax.naming.InitialContext; import javax.naming.Context; import javax.naming.NamingException; import javax.ejb.CreateException; import java.rmi.RemoteException; import java.util.Properties; import javax.rmi.PortableRemoteObject; public class Client_3 { public static int SHIP_ID = 1; public static int BED_COUNT = 3; public static void main(String [] args) { try { Context jndiContext = getInitialContext(); Object ref = jndiContext.lookup("TravelAgentHomeRemote"); TravelAgentHomeRemote home = (TravelAgentHomeRemote) PortableRemoteObject.narrow(ref,TravelAgentHomeRemote.class); TravelAgentRemote travelAgent = home.create(); // Get a list of all cabins on ship 1 with a bed count of 3. String list [] = travelAgent.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); } } When you have successfully run Client_3, 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 EJB entity. Please refer to Workbook Exercise 4.2, A Simple Session Bean. This workbook is available free, in PDF format, at http://www.oreilly.com/catalog/entjbeans3/workbooks.
| ||||||
|