Chapter 5. The Client ViewDeveloping the Cabin EJB and the TravelAgent EJB should have raised your confidence, but it also should have raised a lot of questions. So far, we have glossed over most of the details involved in developing, deploying, and accessing these enterprise beans. In this chapter and the ones that follow, we will slowly peel away the layers of the Enterprise JavaBeans onion to expose the details of EJB application development. This chapter focuses specifically on the client's view of entity and session beans. Message-driven beans are not covered in this chapter—they are covered in detail in Chapter 13. The client, whether it is an application or another enterprise bean, doesn't work directly with the beans in the EJB system. Instead, clients interact with a set of interfaces that provide access to beans and their business logic. These interfaces consist of the JNDI API and an EJB client-side API. JNDI allows us to find and access enterprise beans regardless of their location on the network; the EJB client-side API is the set of interfaces and classes developers use on the client to interact with enterprise beans. The best approach to this chapter is to read about several features of the client view and then follow the workbook examples to see those features in action. This will provide you with hands-on experience and a much clearer understanding of the concepts. Have fun, experiment, and you'll be sure to understand the fundamentals. 5.1 Locating Beans with JNDIIn Chapter 4, the client application started by creating an InitialContext, which it then used to get a remote reference to the homes of the Cabin and TravelAgent EJBs. The InitialContext is part of a larger API called the Java Naming and Directory Interface ( JNDI). We use JNDI to look up an EJB home in an EJB server just like you might use a phone book to find the home number of a friend or business associate. JNDI is a standard Java optional package that provides a uniform API for accessing a wide range of services. In this respect, it is somewhat similar to JDBC, which provides uniform access to different relational databases. Just as JDBC lets you write code that doesn't care whether it's talking to an Oracle database or a Sybase database, JNDI lets you write code that can access different directory and naming services, such as LDAP, Novell NetWare NDS, Sun Solaris NIS+, CORBA Naming Service, and the naming services provided by EJB servers. EJB servers are required to support JNDI by organizing beans into a directory structure and providing a JNDI driver, called a service provider, for accessing that directory structure. Using JNDI, an enterprise can organize its beans, services, data, and other resources in a large virtual-directory structure, which can provide a powerful mechanism for binding together disparate systems. Two of JNDI's greatest features are that it is virtual and dynamic. JNDI is virtual because it allows one directory service to be linked to another through simple URLs. The URLs in JNDI are analogous to HTML links. Clicking on a link in HTML allows a user to load the contents of a web page. The new web page can be downloaded from the same host as the starting page or from a completely different web site—the location of the linked page is transparent to the user. Likewise, using JNDI, you can drill down through directories to files, printers, EJB home objects, and other resources using links that are similar to HTML links. The directories and subdirectories can be located in the same host or can be physically hosted at different locations. The user doesn't know or care where the directories are actually located. As a developer or administrator, you can create virtual directories that span a variety of services over many different physical locations. JNDI is dynamic because it allows the JNDI drivers (a.k.a. service providers) for specific types of directory services to be loaded at runtime. A driver maps a specific kind of directory service into the standard JNDI class interfaces. When a link to a different directory service is chosen, the driver for that type of directory service is automatically loaded from the directory's host, if it is not already resident on the user's machine. Automatically downloading JNDI drivers makes it possible for a client to navigate across arbitrary directory services without knowing in advance what kinds of services it is likely to find. JNDI allows the application client to view the EJB server as a set of directories, like directories in a common filesystem. After the client application locates and obtains a remote reference to the EJB home using JNDI, the client can use the EJB home to obtain an EJB object reference to an enterprise bean. In the TravelAgent EJB and the Cabin EJB with which you worked in Chapter 4, you used the method getInitialContext() to get a JNDI InitialContext object, which looked like this: 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); } An initial context is the starting point for any JNDI lookup—it's similar in concept to the root of a filesystem. The way you create an initial context is peculiar, but not fundamentally difficult. You start with a properties table of type Properties. This is essentially a hash table to which you add various values that determine the kind of initial context you get. Of course, as mentioned in Chapter 4, this code will change depending on how your EJB vendor has implemented JNDI. For example, with the Pramati Application Server, getInitialContext() might look something like this: public static Context getInitialContext() throws Exception { Hashtable p = new Hashtable(); p.put(Context.INITIAL_CONTEXT_FACTORY, "com.pramati.naming.client.PramatiClientContextFactory"); p.put(Context.PROVIDER_URL, "rmi://127.0.0.1:9191"); return new InitialContext(p); } For a more detailed explanation of JNDI, see O'Reilly's Java™ Enterprise in a Nutshell by David Flanagan, Jim Farley, William Crawford, and Kris Magnusson. 5.2 The Remote Client APIEnterprise bean developers are required to provide a bean class, component interfaces, and, for entity beans, a primary key. Only the component interfaces and primary key class are visible to the client; the bean class is not. The component interfaces and primary key contribute to the client-side API in EJB. In EJB 1.1, all clients, whether they are in the same container system or not, must use the Remote Client API, which means they must use the remote interface, the remote home interface, and Java RMI in all their interactions. In EJB 2.0, remote clients still must use the Remote Client API, but enterprise beans that are located in the same EJB container system have the option of using the Local Client API. The Local Client API provides local component interfaces and avoids the restrictions and overhead of the Remote Client API. This section examines in more detail the remote component interfaces and the primary key, as well as other Java types that make up EJB's remote client-side API, which is used in both EJB 2.0 and EJB 1.1. This will provide you with a better understanding of how the remote client-side API is used and its relationship with the bean class on the EJB server. At the end of this chapter, in Section 5.3, we will examine the use of local component interfaces for EJB 2.0 readers. 5.2.1 Java RMI-IIOPEnterprise JavaBeans 2.0 and 1.1 define an enterprise bean's remote interfaces in terms of Java RMI-IIOP, which enforces compliance with CORBA. This means that the underlying protocol used by remote clients to access enterprise beans can be anything the vendor wants as long as it supports the types of interfaces and arguments that are compatible with Java RMI-IIOP. EJB 1.1 required only that the wire protocol used by vendors utilize types that were compatible with Java RMI-IIOP. In other words, the interface types and values used in remote references had to be compliant with the types allowed for Java RMI-IIOP. This ensured that early Java RMI-IIOP adopters were supported and made for a seamless transition for other vendors who wanted to use real Java RMI-IIOP in EJB 2.0. In EJB 2.0, vendors can still offer other Java RMI-IIOP-compatible protocols, but in addition to any proprietary protocols they support, they must also support the CORBA IIOP 1.2 protocol as defined in the CORBA 2.3.1 specification. To comply with Java RMI-IIOP types, EJB vendors have to restrict the definition of interfaces and arguments to types that map nicely to IIOP 1.2. These restrictions are really not all that bad, and you probably won't even notice them while developing your beans, but it's important to know what they are. The next few sections discuss the Java RMI-IIOP programming model for both EJB 2.0 and EJB 1.1. Note that EJB 2.0's local component interfaces are not Java RMI interfaces and do not have to support IIOP 1.2 or use types compliant with the Java RMI-IIOP protocol. 5.2.1.1 Java RMI return types, parameters, and exceptionsThe supertypes of the remote home interface and remote interface, javax.ejb.EJBHome and javax.ejb.EJBObject, both extend java.rmi.Remote. As Remote interface subtypes, they are expected to adhere to the Java RMI specification for Remote interfaces. 5.2.1.1.1 Return types and parametersThe remote component interfaces must follow several guidelines, some of which apply to the return types and parameters that are allowed. There are two kinds of return and parameter types: declared types, which are checked by the compiler; and actual types, which are checked by the runtime. The types that may be used in Java RMI are actual types. To be compatible with Java RMI, the actual types used in the java.rmi.Remote interfaces must be primitives, String types, java.rmi.Remote types, or serializable types. java.rmi.Remote types and serializable types do not have to explicitly implement java.rmi.Remote and java.io.Serializable. For example, the java.util.Collection type, which does not explicitly extend java.io.Serializable, is a perfectly valid return type for a remote finder method, provided that the concrete class implementing Collection, the actual type, does implement java.io.Serializable. Java RMI has no special rules regarding declared return types or parameter types. At runtime, a type that is not a java.rmi.Remote type is assumed to be serializable; if it is not, an exception is thrown. The actual type passed cannot be checked by the compiler, it must be checked at runtime. Here is a list of the types that can be passed as parameters or returned in Java RMI:
Serializable objects are passed by copy (a.k.a., passed by value), not by reference, which means that changes in a serialized object on one tier are not automatically reflected on the others. Objects that implement Remote, like TravelAgentRemote or CabinRemote, are passed as remote references, which are a little different. A remote reference is a Remote interface implemented by a distributed object stub. When a remote reference is passed as a parameter or returned from a method, it is the stub that is serialized and passed by value, not the object server remotely referenced by the stub. In Chapter 12, the home interface for the TravelAgent EJB is modified so that the create() method takes a reference to a Customer EJB as its only argument: public interface TravelAgentHomeRemote extends javax.ejb.EJBHome { public TravelAgentRemote create(CustomerRemote customer) throws RemoteException, CreateException; } The customer argument is a remote reference to a Customer EJB that is passed into the create() method. When a remote reference is passed or returned in Enterprise JavaBeans, the EJB object stub is passed by copy. The copy of the EJB object stub points to the same EJB object as the original stub. This results in both the enterprise bean instance and the client having remote references to the same EJB object. Thus, changes made on the client using the remote reference will be reflected when the enterprise bean instance uses the same remote reference. Figures 5-1 and 5-2 show the difference between a serializable object and a remote reference argument in Java RMI. Figure 5-1. Serializable arguments in Java RMI
Figure 5-2. Remote reference arguments in Java RMI
5.2.1.1.2 ExceptionsThe Java RMI specification states that every method defined in a Remote interface must throw the java.rmi.RemoteException. The RemoteException is used when problems occur with distributed object communications, such as a network failure or inability to locate the object server. In addition, Remote interface types can throw any application-specific exceptions (exceptions defined by the application developer) that are necessary. The following code shows the remote interface to the TravelAgent EJB discussed in Chapter 2. This remote interface is similar to the one defined in Chapter 4. TravelAgentRemote has several remote methods, including bookPassage(). The bookPassage() method can throw a RemoteException (as required) in addition to an application exception, IncompleteConversationalState: public interface TravelAgentRemote extends javax.ejb.EJBObject { public void setCruiseID(int cruise) throws RemoteException, FinderException; public int getCruiseID() throws RemoteException; public void setCabinID(int cabin) throws RemoteException, FinderException; public int getCabinID() throws RemoteException; public int getCustomerID() throws RemoteException; public Ticket bookPassage(CreditCardRemote card, double price) throws RemoteException,IncompleteConversationalState; public String [] listAvailableCabins(int bedCount) throws RemoteException; } 5.2.1.2 Java RMI-IIOP type restrictionsIn addition to the Java RMI programming model discussed earlier, Java RMI-IIOP imposes additional restrictions on the remote interfaces and value types used in the Remote Client API. These restrictions are born of limitations inherit in the Interface Definition Language (IDL) upon which CORBA IIOP 1.2 is based. The exact nature of these limitations is outside the scope of this book. Here are two of the restrictions; the others, like IDL name collisions, are so rarely encountered that it wouldn't be constructive to mention them:[1]
Figure 5-3. Overloading rules for remote interface inheritance in Java RMI-IIOP
5.2.1.3 Explicit narrowing using PortableRemoteObjectIn Java RMI-IIOP, remote references must be explicitly narrowed using the javax.rmi.PortableRemoteObject.narrow() method. The typical practice in Java is to cast the reference to the more specific type: javax.naming.Context jndiContext; ... CabinHomeRemote home = (CabinHomeRemote)jndiContext.lookup("CabinHomeRemote"); The javax.naming.Context.lookup() method returns an Object. In EJB 2.0's Local Client API, we can assume that it is legal to cast the return argument. However, the Remote Client API must be compatible with Java RMI-IIOP, which means that clients must adhere to limitations imposed by the IIOP 1.2 protocol. To accommodate all languages, many of which have no concept of casting, IIOP 1.2 does not support stubs that implement multiple interfaces. The stub returned in IIOP implements only the interface specified by the return type of the remote method that was invoked. If the return type is Object, as is the remote reference returned by the lookup() method, the stub will implement only methods specific to the Object type. Of course, some means for converting a remote reference from a more general type to a more specific type is essential in an object-oriented environment, so Java RMI-IIOP provides a mechanism for explicitly narrowing references to a specific type. The javax.rmi.PortableRemoteObject.narrow() method abstracts this narrowing to provide narrowing in IIOP as well as other protocols. Remember that while the Remote Client API requires that you use Java RMI-IIOP reference and argument types, the wire protocol need not be IIOP 1.2. Other protocols besides IIOP may also require explicit narrowing. The PortableRemoteObject abstracts the narrowing process so that any protocol can be used. To narrow the return argument of the Context.lookup() method to the appropriate type, we must explicitly ask for a remote reference that implements the interface we want: import javax.rmi.PortableRemoteObject; ... javax.naming.Context jndiContext; ... Object ref = jndiContext.lookup("CabinHomeRemote"); CabinHomeRemote home = (CabinHomeRemote) PortableRemoteObject.narrow(ref, CabinHomeRemote.class); When the narrow() method has successfully executed, it returns a stub that implements the Remote interface specified. Because the stub is known to implement the correct type, you can then use Java's native casting to narrow the stub to the correct Remote interface. The narrow() method takes two arguments: the remote reference that is to be narrowed and the type to which it should be narrowed. The definition of the narrow() method is:[2] package javax.rmi; public class PortableRemoteObject extends java.lang.Object { public static java.lang.Object narrow(java.lang.Object narrowFrom, java.lang.Class narrowTo) throws java.lang.ClassCastException; ... } The narrow() method needs to be used only when a remote reference to an EJB home or EJB object is returned without a specific Remote interface type. This occurs in six circumstances:
The PortableRemoteObject.narrow() method is not required when the remote type is specified in the method signature. This is true of the create methods and find methods in remote home interfaces that return a single bean. For example, the create() and findByPrimaryKey() methods defined in the CabinHomeRemote interface (Chapter 4) do not require the use of the narrow() method because these methods already return the correct EJB object type. Business methods that return the correct type do not need to use the narrow() method either, as the following code illustrates: /* The CabinHomeRemote.create() method specifies * the CabinRemote interface as the return type, * so explicit narrowing is not needed.*/ CabinRemote cabin = cabinHome.create(12345); /* The CabinHomeRemote.findByPrimaryKey() method specifies * the CabinRemote interface as the return type, * so explicit narrowing is not needed.*/ CabinRemote cabin = cabinHome.findByPrimaryKey(12345); /* The ShipRemote.getCrewman() business method specifies * the CrewmanRemote interface as the return type, * so explicit narrowing is not needed.*/ CrewmanRemote crew = ship.getCrewman("Burns", "John", "1st Lieutenant"); 5.2.2 The Remote Home InterfaceThe remote home interface provides life-cycle operations and metadata for the bean. When you use JNDI to access a bean, you obtain a remote reference, or stub, to the bean's EJB home, which implements the remote home interface. Every bean type may have one home interface, which extends the javax.ejb.EJBHome interface. Here is the EJBHome interface: public interface javax.ejb.EJBHome extends java.rmi.Remote { public abstract EJBMetaData getEJBMetaData() throws RemoteException; public HomeHandle getHomeHandle() // new in 1.1 throws RemoteException; public abstract void remove(Handle handle) throws RemoteException, RemoveException; public abstract void remove(Object primaryKey) throws RemoteException, RemoveException; } 5.2.2.1 Removing beansThe EJBHome.remove() methods are responsible for deleting an enterprise bean. The argument is either the javax.ejb.Handle of the enterprise bean or, if it's an entity bean, its primary key. The Handle will be discussed in more detail later, but it is essentially a serializable pointer to a specific enterprise bean. When either of the EJBHome.remove() methods are invoked, the remote reference to the enterprise bean on the client becomes invalid: the stub to the enterprise bean that was removed no longer works. If for some reason the enterprise bean can't be removed, a RemoveException is thrown. The impact of the EJBHome.remove() on the enterprise bean itself depends on the type of bean. For session beans, the EJBHome.remove() methods end the session's service to the client. When EJBHome.remove() is invoked, the remote reference to the session bean becomes invalid, and any conversational state maintained by the session bean is lost. The TravelAgent EJB you created in Chapter 4 is stateless, so no conversational state exists (you'll read more about this in Chapter 12). When a remove() method is invoked on an entity bean, the remote reference becomes invalid, and any data it represents is actually deleted from the database. This is a far more destructive activity, because once an entity bean is removed, the data it represents no longer exists. The difference between using a remove() method on a session bean and using remove() on an entity bean is similar to the difference between hanging up on a telephone conversation and actually killing the caller on the other end. Both end the conversation, but the methods are a little different. The following code fragment is taken from the main() method of a client application that is similar to the clients we created to exercise the Cabin and TravelAgent EJBs. It shows that you can remove enterprise beans using a primary key (for entity beans only) or a Handle. Removing an entity bean deletes the entity from the database; removing a session bean results in the remote reference becoming invalid. Here's the code: Context jndiContext = getInitialContext(); // Obtain a list of all the cabins for ship 1 with bed count of 3. Object ref = jndiContext.lookup("TravelAgentHomeRemote"); TravelAgentHomeRemote agentHome = (TravelAgentHomeRemote) PortableRemoteObject.narrow(ref,TravelAgentHomeRemote.class); TravelAgentRemote agent = agentHome.create(); String list [] = agent.listCabins(1,3); System.out.println("1st List: Before deleting cabin number 30"); for(int i = 0; i < list.length; i++){ System.out.println(list[i]); } // Obtain the home and remove cabin 30. Rerun the same cabin list. ref = jndiContext.lookup("CabinHomeRemote"); CabinHomeRemote c_home = (CabinHomeRemote) PortableRemoteObject.narrow(ref, CabinHomeRemote.class); Integer pk = new Integer(30); c_home.remove(pk); list = agent.listCabins(1,3); System.out.println("2nd List: After deleting cabin number 30"); for (int i = 0; i < list.length; i++) { System.out.println(list[i]); } First, the application creates a list of cabins, including the cabin with the primary key 30. Then it removes the Cabin EJB with this primary key and creates the list again. The second time the iteration is performed, cabin 30 will not be listed; the listCabin() method will be unable to find a cabin with a primary key equal to 30 because the bean and its data are no longer in the database. Your output should look something like this: 1st List: Before deleting cabin number 30 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 29,Suite 309 ,3 30,Suite 309 ,3 2nd List: After deleting cabin number 30 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 29,Suite 308 ,3 5.2.2.2 Bean metadataEJBHome.getEJBMetaData() returns an instance of javax.ejb.EJBMetaData that describes the remote home interface, remote interface, and primary key classes and indicates whether the enterprise bean is a session or entity bean.[3] This type of metadata is valuable to Java tools such as IDEs that have wizards or other mechanisms for interacting with an enterprise bean from a client's perspective. A tool could, for example, use the class definitions provided by the EJBMetaData with Java reflection to create an environment in which deployed enterprise beans can be "wired" together by developers. Of course, information such as the JNDI names and URLs of the enterprise beans is also needed. Most application developers rarely use the EJBMetaData. Knowing that it's there, however, is valuable when you need to create automatic code generators or some other automatic facility. In those cases, familiarity with the Reflection API is necessary.[4] The following code shows the interface definition for EJBMetaData. Any class that implements the EJBMetaData interface must be serializable; it cannot be a stub to a distributed object. This allows IDEs and other tools to save the EJBMetaData for later use: public interface javax.ejb.EJBMetaData { public abstract EJBHome getEJBHome(); public abstract Class getHomeInterfaceClass(); public abstract Class getPrimaryKeyClass(); public abstract Class getRemoteInterfaceClass(); public abstract boolean isSession(); } The following code shows how the EJBMetaData for the Cabin EJB could be used to get more information about the enterprise bean. Notice that there is no way to get the bean class using the EJBMetaData; the bean class is not part of the client API and therefore doesn't belong to the metadata. Here's the code: Context jndiContext = getInitialContext(); Object ref = jndiContext.lookup("CabinHomeRemote"); CabinHomeRemote c_home = (CabinHomeRemote) PortableRemoteObject.narrow(ref, CabinHomeRemote.class); EJBMetaData meta = c_home.getEJBMetaData(); System.out.println(meta.getHomeInterfaceClass().getName()); System.out.println(meta.getRemoteInterfaceClass().getName()); System.out.println(meta.getPrimaryKeyClass().getName()); System.out.println(meta.isSession()); This application creates output like the following: com.titan.cabin.CabinHomeRemote com.titan.cabin.CabinRemote com.titan.cabin.CabinPK false In addition to providing the class types of the enterprise bean, the EJBMetaData makes available the remote EJB home for the bean. Once we get the remote EJB home from the EJBMetaData, we can obtain references to the remote EJB object and perform other functions. In the following code, we use the EJBMetaData to get the primary key class, create a key instance, obtain the remote EJB home, and get a remote reference to the EJB object for a specific cabin entity from the EJB home: Object primKeyType = meta.getPrimaryKeyClass(); if(primKeyType instanceof java.lang.Integer){ Integer pk = new Integer(1); Object ref = meta.getEJBHome(); CabinHomeRemote c_home2 = (CabinHomeRemote) PortableRemoteObject.narrow(ref,CabinHomeRemote.class); CabinRemote cabin = c_home2.findByPrimaryKey(pk); System.out.println(cabin.getName()); } 5.2.2.3 The HomeHandleThe HomeHandle is accessed by calling the EJBHome.getHomeHandle() method. This method returns a javax.ejb.HomeHandle object that provides a serializable reference to an enterprise bean's remote home. The HomeHandle allows a remote home reference to be stored and used later. It is similar to the javax.ejb.Handle and is discussed in more detail a little later. 5.2.2.4 Creating and finding beansIn addition to the standard javax.ejb.EJBHome methods that all remote home interfaces inherit, remote home interfaces also include special create and find methods for the bean. We have already talked about create and find methods, but a little review will solidify your understanding of the remote home interface's role in the Remote Client API. The following code shows the remote home interface defined for the Cabin EJB: public interface CabinHomeRemote extends javax.ejb.EJBHome { public CabinRemote create(Integer id) throws CreateException, RemoteException; public CabinRemote findByPrimaryKey(Integer pk) throws FinderException, RemoteException; } Create methods throw a CreateException if something goes wrong during the creation process; find methods throw a FinderException if there is an error. Since these methods are defined in an interface that subclasses Remote, they must also declare that they throw the RemoteException. The create and find methods are specific to each enterprise bean, so it is up to the bean developer to define the appropriate create and find methods in the remote home interface. CabinHomeRemote currently has only one create method, which creates a cabin with a specified ID, and one find method, which looks up an enterprise bean given its primary key. However, it is easy to imagine methods that would create and find a cabin with particular properties—for example, a cabin with three beds, or a deluxe cabin with blue wallpaper. Only entity beans have find methods; session beans do not. Entity beans represent unique identifiable data within a database and therefore can be found. Session beans, on the other hand, do not represent data: they are created to serve a client application and are not persistent, so there is nothing to find. A find method for a session bean would be meaningless. In EJB 2.0, the create methods were expanded so that a method name could be used as suffix. In other words, all create methods may take the form create<SUFFIX>(). For example, the Customer EJB might define a remote home interface with several create methods, each of which takes a different String type parameter but has a different method names: public interface CustomerHome extends javax.ejb.EJBHome { public CustomerRemote createWithSSN(Integer id, String socialSecurityNumber) throws CreateException, RemoteException; public CustomerRemote createWithPIN(Integer personalIdNumber) throws CreateException, RemoteException; public CustomerRemote createWithBLN(Integer id, String businessLicenseNumber) throws CreateException, RemoteException; public Customer findByPrimaryKey(Integer id) throws FinderException, RemoteException; } While the use of a suffix in the create method names in EJB 2.0 is allowed, it is not required. EJB 1.1 doesn't support the use of suffixes in create method names. The create and find methods defined in the remote home interfaces are straightforward and can be easily employed by the client. The create methods on the home interface have to match the ejbCreate() and ejbPostCreate() methods on the bean class. The create(), ejbCreate(), and ejbPostCreate() methods match when they have the same parameters, when the arguments are of the same type and in the same order, and when their method names are the same. This way, when a client calls the create method on the home interface, the call can be delegated to the corresponding ejbCreate() and ejbPostCreate() methods on the bean instance. The find methods in the home interface work similarly for bean-managed entities in EJB 2.0 and 1.1. Every find<SUFFIX>() method in the home interface must correspond to an ejbFind<SUFFIX>() method in the bean itself. Container-managed entities do not implement ejbFind() methods in the bean class; the EJB container supports find methods automatically. You will discover more about how to implement the ebjCreate(), ejbPostCreate(), and ejbFind() methods in the bean in Chapter 6 through 11. 5.2.2.5 EJB 2.0: Home methodsIn addition to find and create methods, the home interface of EJB 2.0 entity beans may also define home methods . The home method is a business method that can be invoked on the home interface (local or remote) and is not specific to one bean instance. For example, the Cabin EJB could define a home method that returns the number of Cabins on a specific deck level: public interface CabinHomeRemote extends javax.ejb.EJBHome { public CabinRemote create(Integer id) throws CreateException, RemoteException; public CabinRemote findByPrimaryKey(Integer pk) throws FinderException, RemoteException; public int getDeckCount(int level) throws RemoteException; } Any method defined in the home interface that is not a create or find method is assumed to be a home method and should have a corresponding ejbHome() method in the bean class, as shown here: public class CabinBean implements javax.ejb.EntityBean{ public int ejbHomeGetDeckCount(int level){ // implement logic to determine deck count } ... } Clients can use home methods from the enterprise bean's home interface. The client does not have to get a reference to a specific EJB object: Object ref = jndiContext.lookup("CabinHome"); CabinHomeRemote home = (CabinHomeRemote) PortableRemoteObject.narrow(ref, CabinHomeRemote.class); int count = home.getDeckCount(2); Home methods are only available to entity beans and not session beans. They can be used for generic business logic that applies changes across a group of entity beans or obtains information that is not specific to any one entity bean. Home methods are discussed in more detail in Chapter 11. 5.2.3 The Remote InterfaceThe business methods of an enterprise bean can be defined by the remote interface provided by the enterprise bean developer. The javax.ejb.EJBObject interface, which extends the java.rmi.Remote interface, is the base class for all remote interfaces. The following code is the remote interface for the TravelAgent bean we developed in Chapter 4: public interface TravelAgentRemote extends javax.ejb.EJBObject { public String [] listCabins(int shipID, int bedCount) throws RemoteException; } Figure 5-4 shows the TravelAgentRemote interface's inheritance hierarchy. Figure 5-4. Enterprise bean interface inheritance hierarchy
Remote interfaces are focused on the business problem and do not include methods for system-level operations such as persistence, security, concurrency, or transactions. System-level operations are handled by the EJB server, which relieves the client developer of many responsibilities. All remote interface methods for beans must throw, at the very least, a java.rmi.RemoteException, which identifies problems with distributed communications. In addition, methods in the remote interface can throw as many custom exceptions as needed to indicate abnormal business-related conditions or errors in executing the business method. You will learn more about defining custom exceptions in Chapter 12 and Chapter 14. Please refer to Workbook Exercise 5.1, The Remote Component Interfaces. This workbook is available free, in PDF format, at http://www.oreilly.com/catalog/entjbeans3/workbooks. 5.2.4 EJBObject, Handle, and Primary KeyAll remote interfaces extend the javax.ejb.EJBObject interface, which provides a set of utility methods and return types. These methods and return types are valuable in managing the client's interactions with beans. Here is the definition for the EJBObject interface: public interface javax.ejb.EJBObject extends java.rmi.Remote { public abstract EJBHome getEJBHome() throws RemoteException; public abstract Handle getHandle() throws RemoteException; public abstract Object getPrimaryKey() throws RemoteException; public abstract boolean isIdentical(EJBObject obj) throws RemoteException; public abstract void remove() throws RemoteException, RemoveException; } When the client obtains a reference to the remote interface, it is actually obtaining a remote reference to an EJB object. The EJB object implements the remote interface by delegating business method calls to the bean class; it provides its own implementations for the EJBObject methods. These methods return information about the corresponding bean instance on the server. As discussed in Chapter 2, the EJB object is automatically generated when deploying the bean in the EJB server, so the bean developer doesn't need to write an EJBObject implementation. 5.2.4.1 Getting the EJBHomeThe EJBObject.getEJBHome() method returns a remote reference to the EJB home for the bean. The remote reference is returned as a javax.ejb.EJBHome object, which can be narrowed to the specific enterprise bean's remote home interface. This method is useful when an EJB object has left the scope of the remote EJB home that manufactured it. Because remote references can be passed as references and returned from methods, like any other Java object, a remote reference can quickly find itself in a completely different part of the application from its remote home. The following code is contrived, but it illustrates how a remote reference can move out of the scope of its home and how getEJBHome() can be used to get a new reference to the EJB home at any time: public static void main(String [] args) { try { Context jndiContext = getInitialContext(); Object ref = jndiContext.lookup("TravelAgentHomeRemote"); TravelAgentHomeRemote home = (TravelAgentHomeRemote) PortableRemoteObject.narrow(ref,TravelAgentHomeRemote.class); // Get a remote reference to the bean (EJB object). TravelAgentRemote agent = home.create(); // Pass the remote reference to some method. getTheEJBHome(agent); } catch (java.rmi.RemoteException re){re.printStackTrace();} catch (Throwable t){t.printStackTrace();} } public static void getTheEJBHome(TravelAgentRemote agent) throws RemoteException { // The home interface is out of scope in this method, // so it must be obtained from the EJB object. Object ref = agent.getEJBHome(); TravelAgentHomeRemote home = (TravelAgentHomeRemote) PortableRemoteObject.narrow(ref,TravelAgentHomeRemote.class); // Do something useful with the home interface. } 5.2.4.2 Primary keyEJBObject.getPrimaryKey() returns the primary key for an entity bean. This method is supported only by EJB objects that represent entity beans. Entity beans represent specific data that can be identified using this primary key. Session beans represent tasks or processes, not data, so primary keys are meaningless for this type of bean. To better understand the nature of a primary key, we need to look beyond the boundaries of the client's view into the EJB container's layer, which was introduced in Chapter 2 and Chapter 3. The EJB container is responsible for the persistence of entity beans, but the exact mechanism for persistence is up to the vendor. To locate an instance of a bean in a persistent store, the data that makes up the entity must be mapped to some kind of unique key. In relational databases, data is uniquely identified by one or more column values that can be combined to form a primary key. In an object-oriented database, the key wraps an object ID (OID) or some kind of database pointer. Regardless of the mechanism—which isn't really relevant from the client's perspective—the unique key for an entity bean's data is encapsulated by the primary key, which is returned by the EJBObject.getPrimaryKey() method. The primary key can be used to obtain remote references to entity beans using the findByPrimaryKey() method. From the client's perspective, the primary key object can be used to identify a unique entity bean. Understanding the context of a primary key's uniqueness is important, as the following code shows: Context jndiContext = getInitialContext(); Object ref = jndiContext.lookup("CabinHomeRemote"); CabinHomeRemote home = (CabinHomeRemote) PortableRemoteObject.narrow(ref,CabinHomeRemote.class); CabinRemote cabin_1 = home.create(101); Integer pk = (Integer)cabin_1.getPrimaryKey(); CabinRemote cabin_2 = home.findByPrimaryKey(pk); In this code, the client creates a Cabin EJB, retrieves its primary key, and then uses the key to get a new reference to the same Cabin EJB. Thus, we have two variables, cabin_1 and cabin_2, which are remote references to EJB objects. These both reference the same Cabin bean, with the same underlying data, because they have the same primary key. The primary key must be used for the correct bean in the correct container. While this seems fairly obvious, the primary key's relationship to a specific container and home interface is important. The primary key can be guaranteed to return the same entity only if it is used within the container that produced the key. As an example, imagine that a third-party vendor sells the Cabin EJB as a product. The vendor sells the Cabin EJB to both Titan and a competitor. Both companies deploy the entity bean using their own relational databases with their own data. An Integer primary key with value of 20 in Titan's EJB system will not map to the same Cabin entity in the competitor's EJB system. Both cruise companies have a Cabin bean with a primary key equal to 20, but they represent different cabins for different ships. The Cabin EJBs come from different EJB containers, so their primary keys are not equivalent.[5] Every entity EJB object has a unique identity within its EJB home. If two EJB objects have the same home and same primary key, they are considered identical. A primary key must implement the java.io.Serializable interface. This means that the primary key, regardless of its form, can be obtained from an EJB object, stored on the client using the Java serialization mechanism, and deserialized when needed. When a primary key is deserialized, it can be used to obtain a remote reference to the same entity bean using findByPrimaryKey(), provided that the key is used on the right remote home interface and container. Preserving the primary key using serialization might be useful if the client application needs to access specific entity beans at a later date. The following code shows a primary key that is serialized and then deserialized to reobtain a remote reference to the same bean: // Obtain cabin 101 and set its name. Context jndiContext = getInitialContext(); Object ref = jndiContext.lookup("CabinHomeRemote"); CabinHomeRemote home = (CabinHomeRemote) PortableRemoteObject.narrow(ref, CabinHomeRemote.class); Integer pk_1 = new Integer(101); CabinRemote cabin_1 = home.findByPrimaryKey(pk_1); cabin_1.setName("Presidential Suite"); // Serialize the primary key for cabin 101 to a file. FileOutputStream fos = new FileOutputStream("pk101.ser"); ObjectOutputStream outStream = new ObjectOutputStream(fos); outStream.writeObject(pk_1); outStream.flush(); outStream.close(); pk_1 = null; // Deserialize the primary key for cabin 101. FileInputStream fis = new FileInputStream("pk101.ser"); ObjectInputStream inStream = new ObjectInputStream(fis); Integer pk_2 = (Integer)inStream.readObject(); inStream.close(); // Reobtain a remote reference to cabin 101 and read its name. CabinRemote cabin_2 = home.findByPrimaryKey(pk_2); System.out.println(cabin_2.getName()); 5.2.4.3 Comparing beans for identityThe EJBObject.isIdentical() method compares two EJB object remote references. It's worth considering why Object.equals() isn't sufficient for comparing EJB objects. An EJB object is a distributed object stub and therefore contains a lot of networking and other state. As a result, references to two EJB objects may be unequal, even if they both represent the same unique bean. The EJBObject.isIdentical() method returns true if two EJB object references represent the same bean, even if the EJB object stubs are different object instances. The following code shows how this works. It starts by creating two remote references to the TravelAgent EJB. These remote EJB objects both refer to the same type of enterprise bean; comparing them with isIdentical() returns true. The two TravelAgent EJBs were created separately, but because they are stateless they are considered to be equivalent. If TravelAgent EJB had been a stateful bean (which it becomes in Chapter 12) the outcome would have been different. Comparing two stateful beans in this manner will result in false because stateful beans have conversational state, which makes them unique. When we use CabinHomeRemote.findByPrimaryKey() to locate two EJB objects that refer to the same Cabin entity bean, we know the entity beans are identical, because we used the same primary key. In this case, isIdentical() also returns true because both remote EJB object references point to the same entity. Here's the code: Context ctx = getInitialContext(); Object ref = ctx.lookup("TravelAgentHomeRemote"); TravelAgentHomeRemote agentHome =(TravelAgentHomeRemote) PortableRemoteObject.narrow(ref, TravelAgentHomeRemote.class); TravelAgentRemote agent_1 = agentHome.create(); TravelAgentRemote agent_2 = agentHome.create(); boolean x = agent_1.isIdentical(agent_2); // x will equal true; the two EJB objects are equal. ref = ctx.lookup("CabinHomeRemote"); CabinHomeRemote c_home = (CabinHomeRemote) PortableRemoteObject.narrow(ref, CabinHomeRemote.class); Integer pk_1 = new Integer(101); Integer pk_2 = new Integer(101); CabinRemote cabin_1 = c_home.findByPrimaryKey(pk_1); CabinRemote cabin_2 = c_home.findByPrimaryKey(pk_2); x = cabin_1.isIdentical(cabin_2); // x will equal true; the two EJB objects are equal. The Integer primary key used in the Cabin bean is simple. More complex, custom-defined primary keys require us to override Object.equals() and Object.hashCode() for the EJBObject.isIdentical() method to work. Chapter 11 discusses the development of more complex custom primary keys, which are called compound primary keys. 5.2.4.4 Removing beansThe EJBObject.remove() method is used to remove session and entity beans. The impact of this method is the same as the EJBHome.remove() method discussed previously. For session beans, remove() causes the session to be released and the remote EJB object reference to become invalid. For entity beans, the actual entity data is deleted from the database and the remote reference becomes invalid. The following code shows the EJBObject.remove() method in use: Context jndiContext = getInitialContext(); Object ref = jndiContext.lookup("CabinHomeRemote"); CabinHomeRemote c_home = (CabinHomeRemote) PortableRemoteObject.narrow(ref,CabinHomeRemote.class); Integer pk = new Integer(101); CabinRemote cabin = c_home.findByPrimaryKey(pk); cabin.remove(); The remove() method throws a RemoveException if for some reason the reference can't be deleted. 5.2.4.5 The enterprise bean HandleThe EJBObject.getHandle() method returns a javax.ejb.Handle object. The Handle is a serializable reference to the remote EJB object. This means that the client can save the Handle object using Java serialization and then deserialize it to reobtain a reference to the same remote EJB object. This is similar to serializing and reusing the primary key. The Handle allows us to recreate a remote EJB object reference that points to the same type of session bean or the same unique entity bean from which the Handle originated. Here is the interface definition of the Handle: public interface javax.ejb.Handle { public abstract EJBObject getEJBObject() throws RemoteException; } The Handle interface specifies only one method, getEJBObject(). Calling this method returns the remote EJB object from which the Handle was created. Once you've gotten the object back, you can narrow it to the appropriate remote interface type. The following code shows how to serialize and deserialize the EJB Handle on a client: // Obtain cabin 100. Context jndiContext = getInitialContext(); Object ref = jndiContext.lookup("CabinHomeRemote"); CabinHomeRemote home = (CabinHomeRemote) PortableRemoteObject.narrow(ref,CabinHomeRemote.class); Integer pk_1 = new Integer(101); CabinRemote cabin_1 = home.findByPrimaryKey(pk_1); // Serialize the Handle for cabin 100 to a file. Handle handle = cabin_1.getHandle(); FileOutputStream fos = new FileOutputStream("handle100.ser"); ObjectOutputStream outStream = new ObjectOutputStream(fos); outStream.writeObject(handle); outStream.flush(); fos.close(); handle = null; // Deserialize the Handle for cabin 100. FileInputStream fis = new FileInputStream("handle100.ser"); ObjectInputStream inStream = new ObjectInputStream(fis); handle = (Handle)inStream.readObject(); fis.close(); // Reobtain a remote reference to cabin 100 and read its name. ref = handle.getEJBObject(); CabinRemote cabin_2 = (CabinRemote) PortableRemoteObject.narrow(ref, CabinRemote.class); if(cabin_1.isIdentical(cabin_2)) // This will always be true. At first glance, the Handle and the primary key appear to do the same thing, but in truth they are very different. Using the primary key requires you to have the correct remote EJB home—if you no longer have a reference to the EJB remote home, you must look up the container using JNDI and get a new home. Only then can you call findByPrimaryKey() to locate the actual enterprise bean. The following code shows how this might work: // Obtain the primary key from an input stream. Integer primaryKey = (Integer)inStream.readObject(); // The JNDI API is used to get a root directory or initial context. javax.naming.Context ctx = new javax.naming.InitialContext(); // Using the initial context, obtain the EJBHome for the Cabin bean. Object ref = ctx.lookup("CabinHomeRemote"); CabinHomeRemote home = (CabinHomeRemote) PortableRemoteObject.narrow(ref,CabinHomeRemote.class); // Obtain a reference to an EJB object that represents the entity instance. CabinRemote cabin_2 = home.findByPrimaryKey(primaryKey); The Handle object is easier to use because it encapsulates the details of doing a JNDI lookup on the container. With a Handle, the correct EJB object can be obtained in one method call, Handle.getEJBObject(), rather than the three method calls required to look up the context, get the home, and find the actual bean. Furthermore, while the primary key can obtain remote references to unique entity beans, it is not available for session beans; Handle, on the other hand, can be used with either type of enterprise bean. This makes using a Handle more consistent across bean types. Consistency is, of course, good in its own right, but it isn't the whole story. Normally, we think of session beans as not having identifiable instances because they exist for only the life of the client session, but this is not exactly true. We have mentioned (but not yet shown) stateful session beans, which retain state information between method invocations. With stateful session beans, two instances are not equivalent. A Handle allows you to work with a stateful session bean, deactivate the bean, and then reactivate it at a later time using the Handle. A client could, for example, be using a stateful session bean to process an order when the process is interrupted for some reason. Instead of losing all the work performed in the session, a Handle can be obtained from the EJB object and the client application can be closed down. When the user is ready to continue the order, the Handle can be used to obtain a reference to the stateful session EJB object. Note that this process is not as fault tolerant as using the Handle or primary key of an entity object. If the EJB server goes down or crashes, the stateful session bean will be lost and the Handle will be useless. It's also possible for the session bean to time out, which would cause the container to remove it from service so that it is no longer available to the client. Changes to the container technology can invalidate both Handles and primary keys. If you think your container technology might change, be careful to take this limitation into account. Primary keys obtain EJB objects by providing unique identification of instances in persistent data stores. A change in the persistence mechanism, however, can impact the integrity of the key. 5.2.4.6 HomeHandleThe javax.ejb.HomeHandle is similar in purpose to javax.ejb.Handle. Just as the Handle is used to store and retrieve references to remote EJB objects, the HomeHandle is used to store and retrieve references to remote EJB homes. In other words, the HomeHandle can be stored and later used to access an EJB home's remote reference the same way that a Handle can be serialized and later used to access an EJB object's remote reference. The following code shows how the HomeHandle can be obtained, serialized, and used: // Obtain cabin 100. Context jndiContext = getInitialContext(); Object ref = jndiContext.lookup("CabinHomeRemote"); CabinHomeRemote home = (CabinHomeRemote) PortableRemoteObject.narrow(ref,CabinHomeRemote.class); // Serialize the HomeHandle for the Cabin bean. HomeHandle homeHandle = home.getHomeHandle(); FileOutputStream fos = new FileOutputStream("handle.ser"); ObjectOutputStream outStream = new ObjectOutputStream(fos); outStream.writeObject(homeHandle); outStream.flush(); fos.close(); homeHandle = null; // Deserialize the HomeHandle for the Cabin bean. FileInputStream fis = new FileInputStream("handle.ser"); ObjectInputStream inStream = new ObjectInputStream(fis); homeHandle = (HomeHandle)inStream.readObject(); fis.close(); EJBHome homeRef = homeHandle.getEJBHome(); CabinHomeRemote home2 = (CabinHomeRemote) PortableRemoteObject.narrow(homeRef,CabinHomeRemote.class); 5.2.4.7 Inside the HandleDifferent vendors define their concrete implementations of the EJB Handle objects differently. However, thinking about hypothetical implementations of Handles will give you a better understanding of how they work. In this example, we define the implementation of a Handle for an entity bean. Our implementation encapsulates the JNDI lookup and the use of the home's findByPrimaryKey() method so that any change that invalidates the key invalidates preserved Handle objects that depend on that key. Here's the code for our hypothetical implementation: package com.titan.cabin; import javax.naming.InitialContext; import javax.naming.Context; import javax.naming.NamingException; import javax.ejb.EJBObject; import javax.ejb.Handle; import java.rmi.RemoteException; import java.util.Properties; import javax.rmi.PortableRemoteObject; public class VendorX_CabinHandle implements javax.ejb.Handle, java.io.Serializable { private Integer primary_key; private String home_name; private Properties jndi_properties; public VendorX_CabinHandle(Integer pk, String hn, Properties p) { primary_key = pk; home_name = hn; jndi_properties = p; } public EJBObject getEJBObject() throws RemoteException { try { Context ctx = new InitialContext(jndi_properties); Object ref = ctx.lookup(home_name); CabinHomeRemote home =(CabinHomeRemote) PortableRemoteObject.narrow(ref,CabinHomeRemote.class); return home.findByPrimaryKey(primary_key); } catch (javax.ejb.FinderException fe) { throw new RemoteException("Cannot locate EJB object",fe); } catch (javax.naming.NamingException ne) { throw new RemoteException("Cannot locate EJB object",ne); } } } The Handle is less stable than the primary key because it relies on the networking configuration and naming—the IP address of the EJB server and the JNDI name of the bean's home—to remain stable. If the EJB server's network address changes or the name used to identify the home changes, the Handle becomes useless. In addition, some vendors choose to implement a security mechanism in the Handle that prevents its use outside the scope of the client application that originally requested it. How this mechanism would work is unclear, but the security limitation it implies should be considered before attempting to use a Handle outside the client's scope. Please refer to Workbook Exercise 5.2, The EJBObject, Handle, and Primary Key. This workbook is available free, in PDF format, at http://www.oreilly.com/catalog/entjbeans3/workbooks. 5.3 EJB 2.0: The Local Client APIEJB 2.0 introduces the concept of local component interfaces, which are intended to provide different semantics and a different execution context for enterprise beans that work together within the same EJB container system. When two or more enterprise beans interact, they are usually co-located; that is, they are deployed in the same EJB container system and execute within the same Java Virtual Machine. Co-located beans do not need to use the network to communicate. Since they are in the same JVM, they can communicate more directly by avoiding the overhead of Java RMI-IIOP. However, the EJB 1.1 specification required that even co-located beans utilize Java RMI-IIOP semantics for communication. This specification did not require that a network protocol be used, but it did require that Java RMI-IIOP types be used and that all objects be copied, rather then referenced, when passed as arguments to methods. EJB 1.1 vendors, determined to squeeze every ounce of performance out of their servers, optimized co-located beans. The optimizations required that the EJB container interpose on invocations from one EJB to another but allowed vendors to avoid the overhead of the network; arguments and returned values were processed within the JVM by the container and not serialized over the network. However, arguments still had to be copied, rather then passed by reference, which slowed invocations down slightly. Many, if not most, vendors offered a proprietary switch that allowed deployers to turn off the copy semantics of co-located beans, so that objects passed from one enterprise bean to another in the same container system could be passed by reference rather then copied, which improved performance. Optimizations of co-located beans that included switches for toggling argument copying eventually became so pervasive across vendors that Sun Microsystems decided to make this option part of the specification. This is why local component interfaces, which make up the Local Client API, were introduced. Session and entity beans can choose to implement either remote or local component interfaces, or both. Any type of enterprise bean (entity, session, or message-driven) can become a co-located client of a session or entity bean: for example, a message-driven bean can call methods on co-located entity beans using its local component interfaces. The Local Client API is similar in many respects to the Remote Client API, but it is less complicated. The Local Client API is composed of two interfaces, the local and local home interfaces, which are similar to the remote and remote home interfaces discussed earlier in this chapter. While explaining the local and local home interfaces and how they are used, we will create these interfaces for the Cabin EJB we developed in Chapter 4. 5.3.1 The Local InterfaceThe local interface, like the remote interface, defines the enterprise bean's business methods that can be invoked by other co-located beans (co-located clients). These business methods must match the signatures of business methods defined in the bean class. For example, the CabinLocal interface is the local interface defined for the Cabin EJB: package com.titan.cabin; import javax.ejb.EJBException; public interface CabinLocal extends javax.ejb.EJBLocalObject { public String getName() throws EJBException; public void setName(String str) throws EJBException; public int getDeckLevel() throws EJBException; public void setDeckLevel(int level) throws EJBException; public int getShipId() throws EJBException; public void setShipId(int sp) throws EJBException; public int getBedCount() throws EJBException; public void setBedCount(int bc) throws EJBException; } The CabinLocal interface is basically the same as the CabinRemote interface we developed in Chapter 4, with a couple of key differences. Most importantly, the CabinLocal interface extends the javax.ejb.EJBLocalObject interface and its methods do not throw the java.rmi.RemoteException. Local interfaces must extend the javax.ejb.EJBLocalObject interface, while remote interfaces must extend the javax.ejb.EJBObject interface: package javax.ejb; import javax.ejb.EJBException; import javax.ejb.RemoteException; public interface EJBLocalObject { public EJBLocalHome getEJBLocalHome() throws EJBException; public Object getPrimaryKey() throws EJBException; public boolean isIdentical(EJBLocalObject obj) throws EJBException; public void remove() throws RemoveException, throws EJBException; } The EJBLocalObject interface defines several methods that should be familiar to you from the previous sections. The getEJBLocalHome() method returns a local home object; the getPrimaryKey() method returns the primary key (entity beans only); the isIdentical() method compares two local EJB objects; and the remove() method removes the enterprise bean. These methods work just like their corresponding methods in the javax.ejb.EJBObject interface. It is also important to point out that the EJBLocalObject, unlike the EJBObject, does not extend the java.rmi.Remote interface, because it is not a remote object. You may have noticed that the EJBLocalObject, unlike the EJBObject, does not define a getHandle() method. EJB local interfaces do not define a getHandle() method because a javax.ejb.Handle is not relevant when the client and enterprise beans are located in the same EJB container system. The Handle is a serializable reference that makes it easier for a remote client to obtain a reference to an enterprise bean on a remote node on a network. Since co-located beans are located in the same container system, not across a network, the Handle object is not necessary. The EJBLocalObject and the local interfaces that extend it do not throw a java.rmi.RemoteException. These interfaces are used for co-located beans in the same JVM, not for accessing remote enterprise beans, so the RemoteException--which is used to report network or partial failures of a remote system—is not relevant. Instead, the local interfaces and EJBLocalObject throw the EJBException. The EJBException is thrown from local interface methods automatically by the container when some kind of container system error occurs or when a transaction errors occurs that causes the bean instance to be discarded. The EJBException is a subtype of the java.lang.RuntimeException and is therefore an unchecked exception. Unchecked exceptions do not have to be declared in the throws clause of the local component interfaces and do not require the client to explicitly handle them using try/catch blocks. However, we choose to declare the EJBExeption in the methods signatures of the CabinLocal interface to better communicate to the client application that this type of exception is possible. 5.3.2 The Local Home InterfaceThe local home interface, like the remote home interface, defines the enterprise bean's life-cycle methods that can be invoked by other co-located beans (co-located clients). The life-cycle methods of the local home interface include find, create, and remove methods similar to those of the remote home interface. The CabinHomeLocal, the local home interface of the Cabin EJB, is shown in the following listing: package com.titan.cabin; import javax.ejb.EJBException; import javax.ejb.CreateException; import javax.ejb.FinderException; public interface CabinHomeLocal extends javax.ejb.EJBLocalHome { public CabinLocal create(Integer id) throws CreateException, EJBException; public CabinLocal findByPrimaryKey(Integer pk) throws FinderException, EJBException; } The CabinHomeLocal interface is very similar to its counterpart, CabinHomeRemote, which we developed in Chapter 4. However, the CabinHomeLocal extends the javax.ejb.EJBLocalHome and does not throw the RemoteException from its create and find methods. You may also have noticed that the type returned from the create() and findByPrimaryKey() methods is the CabinLocal interface, not the remote interface of the Cabin EJB. The create and find methods of local home interfaces will always return EJB objects that implement the local interface of that enterprise bean. Local interfaces must always extend the EJBLocalHome interface, which is much simpler than its remote counterpart, EJBHome: package javax.ejb; import javax.ejb.RemoveException; import javax.ejb.EJBException; public interface EJBLocalHome { public void remove(Object primaryKey) throws RemoveException, EJBException; } Unlike the EJBHome, the EJBLocalHome does not provide EJBMetaData and HomeHandle accessors. The EJBMetaData, which is primarily used by visual development tools, is not needed for co-located beans. In addition, the HomeHandle is not relevant to co-located client beans any more than the Handle was, because co-located beans do not need special network references. The EJBLocalHome does define a remove() method that takes the primary key as its argument; this method works the same as its corresponding method in the remote EJBObject interface. 5.3.3 Deployment DescriptorWhen an enterprise bean uses local component interfaces, they must be declared in the XML deployment descriptor. This is a trivial matter. The changes are highlighted below: <!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> <local-home>com.titan.cabin.CabinHomeLocal</local-home> <local>com.titan.cabin.CabinLocal</local> <ejb-class>com.titan.cabin.CabinBean</ejb-class> In addition to adding the <local-home> and <local> elements, the <ejb-ref> element is changed to an <ejb-local-ref> element, indicating that a local EJB object is being used instead of a remote one: <ejb-local-ref> <ejb-ref-name>ejb/CabinHomeLocal</ejb-ref-name> <ejb-ref-type>Entity</ejb-ref-type> <local-home>com.titan.cabin.CabinHomeLocal</local-home> <local>com.titan.cabin.CabinLocal</local> </ejb-local-ref> 5.3.4 Using the Local Client APIWe can easily redesign the TravelAgent EJB developed in Chapter 4 so that it uses the Cabin EJB's local component interfaces instead of the remote component interfaces: public String [] listCabins(int shipID, int bedCount) { try { javax.naming.Context jndiContext = new InitialContext(); CabinHomeLocal home = (CabinHomeLocal) jndiContext.lookup("java:comp/env/ejb/CabinHomeLocal"); Vector vect = new Vector(); for (int i = 1; ; i++) { Integer pk = new Integer(i); CabinLocal 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(NamingException ne) { throw new EJBException(ne); } } The bold code text shows the three small changes that were needed. The most important change is using local component interfaces for the Cabin EJB instead of remote interfaces. In addition, we do not need to use the PortableRemoteObject.narrow() method when obtaining the Cabin EJB's home object from the JNDI ENC. This is because we are not accessing the home across the network; we are accessing it from within the same JVM, so we know that the client is a Java client and that no network protocol is in use. The elimination of this method makes the code much easier to look at. We also changed the try/catch block so that it will catch the javax.naming.NamingException and not the EJBException thrown by any of the local component interface methods. In this case it is easier to allow those exception to propagate directly to the container, where they can be handled better. Chapter 14 covers exception handling in detail. Please refer to Workbook Exercise 5.3, The Local Component Interfaces. This workbook is available free, in PDF format, at http://www.oreilly.com/catalog/entjbeans3/workbooks. 5.3.5 When to Use Local Component InterfacesEntity and session beans can use either local or remote component interfaces, or they may use both so that the bean is accessible from remote and local clients. Any time you are going to have enterprise beans accessing each other from within the same container system you should seriously consider using local component interfaces, as their performance is likely to be better than that of remote component interfaces. With the Local Client API, no network infrastructure is involved and there is no argument copying. Because local clients are in the same JVM, there is no need for network comminations. When a co-located client bean invokes a method on a local EJB object, the container intervenes to apply transaction and security management, but then delegates the invocation directly to the target bean instance. No network. Very little overhead. The complete absence of a network can increase the speed of an RMI loop by a full magnitude, so it is very desirable. However, relying on the Local Client API eliminates the location transparency of enterprise bean references. In other words, you cannot move the bean to a different server on the network, because it must be co-located. With the Remote Client API, you can move enterprise beans from one server to another without impacting the bean code. In other words, the Remote Client API provides better location transparency: you are not dependent on the location of the enterprise bean in order to invoke its methods. The Local Client API also passes object arguments by reference from one bean to another, as illustrated in Figure 5-5. This means that an object passed from enterprise bean A to enterprise bean B is referenced by both beans, so if B changes its values A will see those changes. Figure 5-5. Passing by reference with the Local Client API
With the Remote Client API, objects' arguments (parameters or return values) are always copied, so changes made to one copy are not reflected in the other (see Figure 5-1). Passing by reference can create some pretty dangerous situations if the enterprise beans that share the object reference are not coded carefully. In most cases, it is best to pass immutable objects without copying them first. Chapter 16 discusses using immutable dependent objects, which are also good candidates for passing by reference. However, if you are going to pass an object that is not immutable, the object should be copied before it is passed. That's the rule of thumb, unless the passed objects are not modified by any beans other than the enterprise beans from which they came. 5.3.6 Are Local Component Interfaces Necessary?Some vendors have argued that local component interfaces were never needed in the first place and only added more complexity and no real functionality to the EJB platform. This is a reasonable argument, considering that most EJB 1.1 vendors were already optimizing the Remote Client API for co-located beans. As we discussed earlier, these vendors did not use the network for co-located bean invocations and provided argument-copy switches. So why was the Local Client API needed at all? An alternative would have been to amend the specification of the Remote Client API to account for co-located container optimizations, making those optimizations standard configurable attributes in the deployment descriptor. The only problem with that solution is semantics. The remote interfaces extend java.rmi.Remote, which is specifically intended for remote objects. In addition, all subtypes of the java.rmi.Remote interface are required to throw java.rmi.RemoteException types from methods. It may have been difficult for developers to distinguish between a co-located EJB object and a remote EJB object, which is important if one is passing objects by reference while the other passes them by copy. However, it is also going to be difficult for EJB developers to get used to local component interfaces and to use them effectively. With local component interfaces, you are locked into a single JVM, and you cannot move beans from one container to the next at will. The arguments for and against the local component interfaces both have their merits. Whether you agree with the need for the Local Client API or not, local interfaces are here to stay, and you should learn to use them appropriately.
| ||||||
|