Enterprise bean developers are required to provide a bean class, two remote interfaces, and for entity beans, a primary key class. Of these types, the remote interfaces and primary key class are visible to the client, while the bean class is not. The remote interface, home interface, and primary key contribute to the client-side API in EJB. The methods defined in these types as well as the methods of their supertypes provide the mechanisms that clients use to interact with an EJB business system.
The following sections examine in more detail the home interface, the remote interface, and the primary key, as well as other types that make up EJB’s client-side API. This will provide you with a better understanding of how the client-side API is used and its relationship with the bean class on the EJB server.
Enterprise JavaBeans 1.0 defines its distributed interfaces in terms of Java RMI. RMI assumes that both the client and server are Java applications, so it takes full advantage of Java types as arguments and return values. Enterprise JavaBeans 1.1 also defines its distributed interfaces in terms of Java RMI, but it enforces compliance with CORBA’s interface, reference, and value types by requiring that only Java RMI-IIOP types be used. In other words, the underlying protocol can be anything that the vendor wants as long as it supports the types of interfaces and arguments specified by Java RMI-IIOP. In a future version of EJB, Java RMI-IIOP ( Java RMI over IIOP) will be the required programming model for accessing beans. Requiring partial support for the Java RMI-IIOP standard ensures that early Java RMI-IIOP adopters are supported and makes for a seamless transition for other vendors in the future.
To be CORBA-compliant, Java RMI-IIOP had to restrict the definition of interfaces and arguments to types that map nicely to CORBA. The 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 paragraphs discuss the Java RMI programming model for both EJB 1.0 and EJB 1.1, and point out the additional restrictions placed on RMI-IIOP types after discussing the restrictions shared by traditional Java RMI and Java RMI-IIOP.
The supertypes of the 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. The Java RMI
specification states that every method defined in a
Remote
interface must throw a
java.rmi.RemoteException
. The
RemoteException
is used when problems occur with
the distributed object communications, like 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 bean discussed in Chapter 2. TravelAgent
has several
remote methods, including bookPassage()
. The
bookPassage()
method can throw a
RemoteException
(as required), in addition to an
application exception,
IncompleteConversationalState
.
public interface TravelAgent extends javax.ejb.EJBObject { public void setCruiseID(int cruise) throws RemoteException, FinderException; public int getCruiseID() throws RemoteException; public void setCabinID(int cabin) throws RemoteException, FinderException; public int getCabinID() throws RemoteException; public int getCustomerID() throws RemoteException; public Ticket bookPassage(CreditCard card, double price) throws RemoteException,IncompleteConversationalState; public String [] listAvailableCabins(int bedCount) throws RemoteException, IncompleteConversationalState; }
Java RMI requires that all parameters and return values be either
Java primitive types (int
,
double
, byte
, etc.) or objects
that implement java.io.Serializable
.
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 Customer
,
Cruise
, and Cabin
, are passed
as remote references—which is 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 the home interface for the TravelAgent
bean, the create()
method
takes a reference to a Customer bean as its only argument.
public interface TravelAgentHome extends javax.ejb.EJBHome { public TravelAgent create(Customer customer) throws RemoteException, CreateException; }
The customer
is a remote reference to a Customer
bean that is passed into the create()
method. When
a remote reference is passed or returned in EJB, 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 bean instance and the client having remote
references to the same EJB object. So changes made on the client
using the remote reference will be reflected when the bean instance
uses the same remote reference. Figure 5.1 and
Figure 5.2 show the difference between a
serializable object and a remote reference argument in Java RMI.
In addition to the Java RMI programming model discussed earlier, Java RMI-IIOP imposes additional restrictions on the remote interfaces and value types used in EJB 1.1. These restrictions are born of limitations inherit in the Interface Definition Language (IDL) upon which CORBA IIOP is based. The exact nature of these limitations is outside the scope of this book. I have only listed two restrictions because the others, like IDL name collisions, are so rarely encountered that it wouldn’t be constructive to mention them.[13]
Method overloading is restricted; a remote interface may not directly extend two or more interfaces that have methods with the same name (even if their arguments are different). A remote interface may, however, overload its own methods and extend a remote interface with overloaded method names. Overloading is viewed, here, as including overriding. Figure 5-3 illustrates both of these situations.
Serializable
types must not directly or indirectly
implement the java.rmi.Remote
interface.
A significant difference between EJB 1.0 and EJB 1.1 is that the new
specification requires that
remote references be explicitly
narrowed using the
javax.rmi.PortableRemoteObject.narrow()
method. The typical
practice in Java would be to cast the reference to the more specific
type, as follows:
javax.naming.Context jndiContext; ... CabinHome home = (CabinHome)jndiContext.lookup("CabinHome");
The
javax.naming.Context.lookup()
method returns an
Object
. In EJB 1.0, which uses simple Java RMI, we
can assume that it is legal to cast the return argument to a legal
Java type. However, EJB 1.1 must be compatible with Java RMI-IIOP,
which means that clients must adhere to limitations imposed by the
IIOP protocol. IIOP is not
specific to any one programming language. As part of the
CORBA standard, it must accommodate
many programming languages including C++, Ada, COBOL, and others. All
programming languages do not support casting,
and for this reason casting is not native to
IIOP. In fact, some languages have no concept of
polymorphism or inheritance (COBOL for
example), so implicit casting in CORBA is out of the
question.
To accommodate all languages, IIOP 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 only implement 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. CORBA provides a mechanism for
explicitly narrowing references to a specific type. The
javax.rmi.PortableRemoteObject.narrow()
method
abstracts this mechanism to provide narrowing in IIOP as well as
other protocols. Remember while Java RMI-IIOP defines the reference
and argument types, in EJB 1.1 it does not define the underlying
protocol. 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("CabinHome"); CabinHome home = (CabinHome) PortableRemoteObject.narrow(ref, CabinHome.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 now 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 it should be narrowed to. The definition of the
narrow()
method is:[14]
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 only needs to be used when a
remote reference to an EJB home or EJB
object is returned without a specific Remote
interface type. This occurs in six circumstances:
When an EJB home reference is obtained using the
javax.naming.Context.lookup()
method:
Object ref = jndiContext.lookup("CabinHome"); CabinHome home = (CabinHome) PortableRemoteObject.narrow(ref, CabinHome.class);
Handle handle = .... // get handle Object ref = handle.getEJBObject(); Cabin cabin = (Cabin) PortableRemoteObject.narrow(ref,Cabin.class);
HomeHandle homeHdle = ... // get home handle EJBHome ref = homeHdle.getEJBHome(); CabinHome home = (CabinHome) PortableRemoteObject.narrow(ref, CabinHome.class);
EJBMetaData metaData = homeHdle.getEJBMetaData(); EJBHome ref = metaData.getEJBHome(); CabinHome home = (CabinHome) PortableRemoteObject.narrow(ref, CabinHome.class);
When an EJB object reference is obtained from a collection returned by a Home interface finder method:
ShipHome shipHome = ... // get ship home Enumeration enum = shipHome.findByCapacity(2000); while(enum.hasMoreElements()){ Object ref = enum.nextElement(); Ship ship = (Ship) PortableRemoteObject.narrow(ref, Ship.class); // do something with Ship reference }
// Officer extends Crewman Ship ship = // get Ship remote reference Crewman crew = ship.getCrewman("Burns", "John", "1st Lieutenant"); Officer burns = (Officer) PortableRemoteObject.narrow(crew, Officer.class);
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 that return a
single bean. For example, the create()
and
findByPrimaryKey()
methods defined in the
CabinHome
interface (Chapter 4)
do not require the use of narrow()
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 CabinHome.create() method specifies * the Cabin remote interface as the return type * so explicit narrowing is not needed.*/ Cabin cabin = cabinHome.create(12345); /* The CabinHome.findByPrimaryKey() method specifies * the Cabin remote interface as the return type * so explicit narrowing is not needed.*/ Cabin cabin = cabinHome.findByPrimaryKey(12345); /* The Ship.getCrewman() business method specifies * the Crewman remote interface as the return type * so explicit narrowing is not needed.*/ Crewman crew = ship.getCrewman("Burns", "John", "1st Lieutenant");
The
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 home interface. Every bean type has one home interface, which
extends the
javax.ejb.EJBHome
interface.
Here is the EJBHome
interface for EJB 1.1:
// EJB 1.1 public interface javax.ejb.EJBHome extends java.rmi.Remote { public abstract EJBMetaDatagetEJBMetaData
() throws RemoteException; public HomeHandlegetHomeHandle
() // new in 1.1 throws RemoteException; public abstract voidremove
(Handle handle) throws RemoteException, RemoveException; public abstract voidremove
(Object primaryKey) throws RemoteException, RemoveException; }
EJB 1.1 adds the
getHomeHandle()
method for accessing the
HomeHandle
, which doesn’t exist in EJB 1.0:
// EJB 1.0 public interface javax.ejb.EJBHome extends java.rmi.Remote { public abstract EJBMetaDatagetEJBMetaData
() throws RemoteException; public abstract voidremove
(Handle handle) throws RemoteException, RemoveException; public abstract voidremove
(Object primaryKey) throws RemoteException, RemoveException; }
The
EJBHome.remove()
methods are responsible for deleting a bean. The argument is either
the javax.ejb.Handle
of the 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 bean. When either of the
EJBHome.remove()
methods are invoked, the remote
reference to the bean on the client becomes invalid: the stub to the
bean that was removed no longer works. If for some reason the bean
can’t be removed, a RemoveException
is
thrown.
The impact of the
EJBHome.remove()
on the 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 beans becomes invalid, and
any conversational state maintained by the bean is lost. The
TravelAgent bean is stateless, so no conversational state exists
(more about this in Chapter 7).
When a remove()
method is invoked on an
entity
bean, the remote reference becomes invalid, and any data that it
represents is actually deleted from the database. This is a far more
destructive activity because once an entity bean is removed, the data
that 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 end results 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 beans. It shows that
you can remove beans using a
primary key (entity 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.
Context jndiContext = getInitialContext(); // Obtain a list of all the cabins for ship 1 with bed count of 3. // EJB 1.0: Use native cast instead of narrow() Object ref = jndiContext.lookup("TravelAgentHome"); TravelAgentHome agentHome = (TravelAgentHome) PortableRemoteObject.narrow(ref,TravelAgentHome.class); TravelAgent 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 CabinHome and remove cabin 30. Rerun the same cabin list. // EJB 1.0: Use native cast instead of narrow() ref = jndiContext.lookup("CabinHome"); CabinHome c_home = (CabinHome) PortableRemoteObject.narrow(ref, CabinHome.class); CabinPK pk = new CabinPK(); pk.id = 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]); }
Your output should look something like the following:
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 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
First, we create a list of cabins, including the cabin with the
primary key 30. Then we remove the Cabin bean with this primary key
and create the list again. The second time through, cabin 30 is not
listed. Because it was removed, the listCabin()
method was unable to find a cabin with a
CabinPK.id
equal to 30, so it stopped making the
list. The bean, including its data, is no longer in the
database.
EJBHome.getEJBMetaData()
returns an instance of
javax.ejb.EJBMetaData
that describes the home interface,
remote interface, and primary key classes, plus whether the bean is a
session or entity bean. This type of metadata is valuable to Java
tools like IDEs that have wizards or other mechanisms for interacting
with a 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 where deployed beans can be “wired” together
by developers. Of course, information such as the JNDI names and URLs
of the 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.[15] 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 EJBHomegetEJBHome
(); public abstract ClassgetHomeInterfaceClass
(); public abstract ClassgetPrimaryKeyClass
(); public abstract ClassgetRemoteInterfaceClass
(); public abstract booleanisSession
(); }
The following code shows how the EJBMetaData
for
the Cabin bean could be used to get more information about the 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.
Context jndiContext = getInitialContext(); // EJB 1.0: Use native cast instead of narrow() Object ref = jndiContext.lookup("CabinHome"); CabinHome c_home = (CabinHome) PortableRemoteObject.narrow(ref, CabinHome.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 creates output like the following:
com.titan.cabin.CabinHome com.titan.cabin.Cabin com.titan.cabin.CabinPK false
In addition to providing the class types of the bean, the
EJBMetaData
also makes available the EJB home for
the bean. By obtaining the EJB home from the
EJBMetaData
, we can obtain references to the 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 EJB home, and
from it, get a remote reference to the EJB object for a specific
cabin entity:
CabinPK pk = (CabinPK)meta.getPrimaryKeyClass().newInstance(); pk.id = 1; // EJB 1.0: Use native cast instead of narrow() Object ref = meta.getEJBHome(); CabinHome c_home2 = (CabinHome) PortableRemoteObject.narrow(ref,CabinHome.class); Cabin cabin = c_home2.findByPrimaryKey(pk); System.out.println(cabin.getName());
EJB 1.1 provides a new object called a HomeHandle
,
which is accessed by calling the
EJBObject.getHomeHandle()
method. This method returns a
javax.ejb.HomeHandle
object, which provides a
serializable reference to a bean 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
near the end of the chapter.
In addition to the standard
javax.ejb.EJBHome
methods that all home interfaces
inherit, 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 home
interface’s role in the client-side API. The following code
shows the home interface defined for the Cabin bean:
public interface CabinHome extends javax.ejb.EJBHome { public Cabincreate
(int id) throws CreateException, RemoteException; public CabinfindByPrimaryKey
(CabinPK pk) throws FinderException, RemoteException; }
Create methods throw a CreateException
if
something goes wrong during the creation process; find methods throw
a FinderException
if the requested bean
can’t be located. Since these methods are defined in an
interface that subclasses Remote
, they must also
declare that they throw RemoteException
.
The
create and find methods are
specific to the bean, so it is up to the bean developer to define the
appropriate create and find methods in the home interface.
CabinHome
currently has only one create method
that creates a cabin with a specified ID and a find method that looks
up a bean given its primary key, but it’s 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. Unlike entity beans, the home interfaces
for
session beans do not have find methods. 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.
The create and find methods defined in the home interfaces are
straightforward and can be easily employed by the client. The create
methods on the home interface have to match the
ejbCreate()
methods on the bean class. A
create
() and
ejbCreate()
method match when
they have the same parameters, when the arguments are of same type
and in the same order. This way, when a client calls the create
method on the home interface, the call can be delegated to the
corresponding ejbCreate()
method on the bean
instance. The find methods in the home interface work similarly for
bean-managed entities. Every find method in the home interface must
correspond to an ejbFind()
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()
and
ejbFind()
methods in the
bean in Chapters
Chapter 6 and Chapter 7.
The
business methods of an enterprise bean are defined by the remote
interface provided by the 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 that we developed in Chapter 4:
public interface TravelAgent extends javax.ejb.EJBObject { public String [] listCabins(int shipID, int bedCount) throws RemoteException; }
Figure 5.4 shows the TravelAgent
interface’s 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 6.
All 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 EJBHomegetEJBHome
() throws RemoteException; public abstract HandlegetHandle
() throws RemoteException; public abstract ObjectgetPrimaryKey
() throws RemoteException; public abstract booleanisIdentical
(EJBObject obj) throws RemoteException; public abstract voidremove
() 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.
The
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 or cast to the specific bean’s
home interface. This method is useful when an EJB object has left the
scope of the EJB home that manufactured it. Because remote references
can be passed as references and returned from methods, like any other
Java object on the client, a remote reference can quickly find itself
in a completely different part of the application from its 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(); // EJB 1.0: Use native cast instead of narrow() Object ref = jndiContext.lookup("TravelAgentHome"); TravelAgentHome home = (TravelAgentHome) PortableRemoteObject.narrow(ref,TravelAgentHome.class); // Get a remote reference to the bean (EJB object). TravelAgent 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(TravelAgent agent) throws RemoteException { // The home interface is out of scope in this method, // so it must be obtained from the EJB object. // EJB 1.0: Use native cast instead of narrow() Object ref = agent.getEJBHome(); TravelAgentHome home = (TravelAgentHome) PortableRemoteObject.narrow(ref,TravelAgentHome.class); // Do something useful with the home interface. }
EJBObject.getPrimaryKey()
returns the primary key for a bean.
This method is only supported 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 a primary key would
be meaningless. 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 Chapters
Chapter 2 and Chapter 3.
The EJB
container is responsible for persistence of the entity beans, but the
exact mechanism for persistence is up to the vendor. In order 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 on the
home interface. 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()
// EJB 1.0: Use native cast instead of narrow()
Object ref = jndiContext.lookup("CabinHome");
CabinHome home = (CabinHome)
PortableRemoteObject.narrow(ref,CabinHome.class);
Cabin cabin_1 = home.create(101);
CabinPK pk = (CabinPK)cabin_1.getPrimaryKey();
Cabin cabin_2 = home.findByPrimaryKey(pk);
In this code, the client creates a Cabin, retrieves the primary key
of that Cabin, and then uses the key to get a new reference to the
Cabin. Thus, we have two local 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. If, for example, you were to obtain a primary key from a
Cabin EJB object and then try to use that key in the
findByPrimaryKey()
method of a different bean
type, like a Ship bean, it wouldn’t work; either it
wouldn’t compile or you would get a runtime error or
FinderException
. While this seems fairly obvious, the primary key’s
relationship to a specific container and home interface is important.
The primary key can only be guaranteed to return the same entity if
it is used within the container that produced the key. As an example,
imagine that a third-party vendor sells the Cabin bean as a product.
The vendor sells the Cabin bean to both Titan and to a competitor.
Both companies deploy the bean using their own relational databases
with their own data. A CabinPK
primary key with an
id
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 an
id
equal to 20, but they represent different
cabins for different ships. The Cabin beans come from different EJB
containers, so their primary keys are not equivalent.
Sun Microsystems’ Enterprise JavaBeans™ Specification, Versions 1.0 and 1.1, describes the primary key and object identity in the following way:
Every entity EJB object has a unique identity with its 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 that
entity using findByPrimaryKey()
, provided that the
key is used on the right 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. In
EJB 1.0, preserving the primary keys is also useful for beans that
maintain relationships to other beans. Bean relationships are
discussed in more detail in Chapter 9.
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(); // EJB 1.0: Use native cast instead of narrow() Object ref = jndiContext.lookup("CabinHome"); CabinHome home = (CabinHome) PortableRemoteObject.narrow(ref, CabinHome.class); CabinPK pk_1 = new CabinPK(); pk_1.id = 101; Cabin 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); CabinPK pk_2 = (CabinPK)inStream.readObject(); inStream.close(); // Re-obtain a remote reference to cabin 101 and read its name. Cabin cabin_2 = home.findByPrimaryKey(pk_2); System.out.println(cabin_2.getName());
The
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 might work. It starts by creating
two remote references to the TravelAgent bean. These EJB objects both
refer to the same type of bean; comparing them with
isIdentical()
returns true
. The
two TravelAgent beans were created separately, but because they are
stateless they are considered to be equivalent. If TravelAgent had
been a stateful bean (which it becomes in Chapter 7) the outcome would have been very 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
CabinHome.findByPrimaryKey()
to locate two EJB
objects that refer to the same Cabin entity bean, we know the beans
are identical, because we used the same primary key. In this case,
isIdentical()
also returns true
because both EJB object references point to the same entity.
Context ctx = getInitialContext(); // EJB 1.0: Use native cast instead of narrow() Object ref = ctx.lookup("TravelAgentHome"); TravelAgentHome agentHome =(TravelAgentHome) PortableRemoteObject.narrow(ref, TravelAgentHome.class); TravelAgent agent_1 = agentHome.create(); TravelAgent agent_2 = agentHome.create(); boolean x = agent_1.isIdentical(agent_2); // x will equal true; the two EJB objects are equal. // EJB 1.0: Use native cast instead of narrow() ref = ctx.lookup("CabinHome"); CabinHome c_home = (CabinHome) PortableRemoteObject.narrow(ref, CabinHome.class); CabinPK pk = new CabinPK(); pk.id = 101; Cabin cabin_1 = c_home.findByPrimaryKey(pk); Cabin cabin_2 = c_home.findByPrimaryKey(pk); x = cabin_1.isIdentical(cabin_2); // x will equal true; the two EJB objects are equal.
The primary key used in the Cabin bean is simple. More complex
primary keys require us to override
Object.equals()
and
Object.hashCode()
in order for the
EJBObject.isIdentical()
method to work. Chapter 6 discusses this in more detail.
The
EJBObject.remove()
method is used to remove the
session or entity bean. 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 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(); // EJB 1.0: Use native cast instead of narrow() Object ref = jndiContext.lookup("CabinHome"); CabinHome c_home = (CabinHome) PortableRemoteObject.narrow(ref,CabinHome.class); CabinPK pk = new CabinPK(); pk.id = 101; Cabin cabin = c_home.findByPrimaryKey(pk); cabin.remove();
The remove()
method throws a
RemoveException
if for some reason the reference
can’t be deleted.
The
EJBObject.getHandle()
method returns a
javax.ejb.Handle
object. The
Handle
is a serializable reference to the 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 EJB object. This
is similar to serializing and reusing the primary key. The
Handle
allows us to recreate an EJB object remote
reference that points to the same type of
session bean or the same unique entity bean that the handle came
from.
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
EJB object from which the handle was created. Once you’ve
gotten the object back, you can narrow or cast 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(); // EJB 1.0: Use native cast instead of narrow() Object ref = jndiContext.lookup("CabinHome"); CabinHome home = (CabinHome) PortableRemoteObject.narrow(ref,CabinHome.class); CabinPK pk_1 = new CabinPK(); pk_1.id = 100; Cabin 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. // EJB 1.0: Use native cast instead of narrow() ref = handle.getEJBObject(); Cabin cabin_2 = (Cabin) PortableRemoteObject.narrow(ref, Cabin.class); System.out.println(cabin_2.getName());
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 EJB home—if you no
longer have a reference to the EJB home, you must look up the
container using JNDI and get a new home. Only then can you call
findByPrimaryKey()
to locate the actual bean. The
following code shows how this might work:
// Obtain the primary key from an input stream. CabinPK primaryKey = (CabinPK)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. // EJB 1.0: Use native cast instead of narrow() Object ref = ctx.lookup("CabinHome"); CabinHome home = (CabinHome) PortableRemoteObject.narrow(ref,CabinHome.class); // Obtain a reference to an EJB object that represents the entity instance. Cabin cabin_2 = CabinHome.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 using the three
method calls required to look up the context, get the home, and find
the actual bean.
Furthermore, while the primary key can be used to obtain remote references to unique entity beans, it is not available for session beans; a handle 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 needs to be 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.
The
javax.ejb.HomeHandle
is similar in purpose to
javax.ejb.Handle
. Just as the
Handle
is used to store and retrieve references to
EJB objects, the HomeHandle
is used to store and
retrieve remote references to 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
HomeHandle
and the method
EJBHome.getHomeHandle()
are new to EJB 1.1. The
following code shows how the HomeHandle
can be
obtained, serialized, and used.
// Obtain cabin 100. Context jndiContext = getInitialContext(); // EJB 1.0: Use native cast instead of narrow() Object ref = jndiContext.lookup("CabinHome"); CabinHome home = (CabinHome) PortableRemoteObject.narrow(ref,CabinHome.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(); // EJB 1.0: Use native cast instead of narrow() EJBHome home = homeHandle.getEJBHome(); CabinHome home2 = (CabinHome) PortableRemoteObject.narrow(home,CabinHome.class);
Different
vendors define their concrete
implementations of the EJB handle differently. However, thinking
about a hypothetical implementation 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 use of the home’s
findByPrimaryKey()
method so that any change that invalidates the key invalidates
preserved handles that depend on that key. Here’s the code for
our hypothetical implementation of a Handle
:
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; public class VendorX_CabinHandle implements javax.ejb.Handle, java.io.Serializable { private CabinPK primary_key; private String home_name; private Properties jndi_properties; public VendorX_CabinHandle(CabinPK 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); // EJB 1.0: Use native cast instead of narrow() Object ref = ctx.lookup(home_name); CabinHome home =(CabinHome) PortableRemoteObject.narrow(ref,CabinHome.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.
[13] To learn more about CORBA IDL and its mapping to the Java language consult The Common Object Request Broker: Architecture and Specification and The Java Language to IDL Mapping available at the OMG site (http://www.omg.org).
[14] Other methods included in the
PortableRemoteObject
class are not important to
EJB application developers. They are intended for Java RMI
developers.
[15] The Reflection API is outside the scope of this book, but it is covered in Java™ in a Nutshell, by David Flanagan (O’Reilly).