In Chapter 4, we started developing some simple enterprise beans, skipping over a lot of the details in the process. In this chapter, we’ll take a thorough look at the process of developing entity beans. On the surface, some of this material may look familiar, but it is much more detailed and specific to entity beans.
Entity beans model business concepts that can be expressed as nouns. This is a rule of thumb rather than a requirement, but it helps in determining when a business concept is a candidate for implementation as an entity bean. In grammar school you learned that nouns are words that describe a person, place, or thing. The concepts of “person” and “place” are fairly obvious: a person bean might represent a customer or a passenger, and a place bean might represent a city or a port-of-call. Similarly, entity beans often represent “things”: real-world objects like ships, cabins, and so on. A bean can even represent a fairly abstract “thing,” such as a ticket or a reservation. Entity beans describe both the state and behavior of real-world objects and allow developers to encapsulate the data and business rules associated with specific concepts; a cabin bean encapsulates the data and business rules associated with a cabin, and so on. This makes it possible for data associated with a concept to be manipulated consistently and safely.
In Titan’s cruise ship business, we can identify hundreds of business concepts that are nouns and therefore could conceivably be modeled by entity beans. We’ve already seen a simple Cabin bean in Chapter 4, and we’ll develop a Ship bean in this chapter. Titan could clearly make use of a PortOfCall bean, a Passenger bean, and many others. Each of these business concepts represents data that needs to be tracked and possibly manipulated. Entities really represent data in the database, so changes to an entity bean result in changes to the database.
There are many advantages to
using entity beans instead of accessing the database directly.
Utilizing entity beans to objectify data provides programmers with a
simpler mechanism for accessing and changing data. It is much easier,
for example, to change a ship’s name by calling
ship.setName()
than to execute an SQL command
against the database. In addition, objectifying the data using entity
beans also provides for more software reuse. Once an entity bean has
been defined, its definition can be used throughout Titan’s
system in a consistent manner. The concept of ship, for example, is
used in many areas of Titan’s business, including booking,
scheduling, and marketing. A Ship bean provides Titan with one
complete way of accessing ship information, and thus it ensures that
access to the information is consistent and simple. Representing data
as entity beans makes development easier and more cost effective.
When a new bean is created, a new record must be inserted into the database and a bean instance must be associated with that data. As the bean is used and its state changes, these changes must be synchronized with the data in the database: entries must be inserted, updated, and removed. The process of coordinating the data represented by a bean instance with the database is called persistence.
There are two types of entity beans, and they are distinguished by how they manage persistence. Container-managed beans have their persistence automatically managed by the EJB container. The container knows how a bean instance’s fields map to the database and automatically takes care of inserting, updating, and deleting the data associated with entities in the database. Beans using bean-managed persistence do all this work explicitly: the bean developer must write the code to manipulate the database. The EJB container tells the bean instance when it is safe to insert, update, and delete its data from the database, but it provides no other help. The bean instance does all the persistence work itself.
The next two sections will describe how EJB works with container-managed and bean- managed entity beans.
Container-managed entity beans are the simplest to develop because they allow you to focus on the business logic, delegating the responsibility of persistence to the EJB container. When you deploy the bean, you identify which fields in the entity are managed by the container and how they map to the database. Once you have defined the fields that will be automatically managed and how they map to the database, the container generates the logic necessary to save the bean instance’s state automatically.
Fields that are mapped to the database are called container-managed fields. Container-managed fields can be any Java primitive type or serializable objects. Most beans will use Java primitive types when persisting to a relational database, since it’s easier to map Java primitives to relational data types.
EJB 1.1 also allows references to other beans to be container-managed fields. The EJB vendor must support converting bean references (remote or home interface types) from remote references to something that can be persisted in the database and converted back to a remote reference automatically. Vendors will normally convert remote references to primary keys, Handle
or HomeHandle
objects, or some other proprietary pointer type, which can be used to preserve the bean reference in the database. The container will manage this conversion from remote reference to persistent pointer and back automatically.
The advantage of container-managed persistence is that the bean can be defined independently of the database used to store its state. Container-managed beans can take advantage of a relational database or an object-oriented database. The bean state is defined independently, which makes the bean more reusable and flexible across applications.
The disadvantage of container-managed beans is that they require sophisticated mapping tools to define how the bean’s fields map to the database. In some cases, this may be a simple matter of mapping each field in the bean instance to a column in the database or of serializing the bean to a file. In other cases, it may be more difficult. The state of some beans, for example, may be defined in terms of a complex relational database join or mapped to some kind of legacy system such as CICS or IMS.
In Chapter 4, we developed our first container-managed bean, the Cabin bean. During the development of the Cabin bean, we glossed over some important aspects of container-managed entity beans. In this section, we will create a new container-managed entity bean, the Ship bean, but this time we will examine it in detail.
Let’s start by thinking about what we’re trying to do. An
enormous amount of data would go into a complete description of a
ship, but for our purposes we will limit the scope of the data to a
small set of information. For now, we can say that a ship has the
following characteristics or attributes: its name, passenger
capacity, and tonnage (i.e., size). The Ship bean will encapsulate
this data; we’ll need to create a SHIP
table
in our database to hold this data. Here is the definition for the
SHIP
table expressed in
standard SQL:
CREATE TABLE SHIP (ID INT PRIMARY KEY, NAME CHAR(30), CAPACITY INT, TONNAGE DECIMAL(8,2))
When defining any bean, we start by coding the remote interfaces. This focuses our attention on the most important aspect of any bean: its business purpose. Once we have defined the interfaces, we can start working on the actual bean definition.
For the Ship bean we will
need a Ship
remote interface. This interface
defines the business methods that clients will use to interact with
the bean. When defining the remote interface, we will take into
account all the different areas in Titan’s system that may want
to use the ship concept. Here is the remote interface for
Ship
:
package com.titan.ship; import javax.ejb.EJBObject; import java.rmi.RemoteException; public interface Ship extends javax.ejb.EJBObject { public String getName() throws RemoteException; public void setName(String name) throws RemoteException; public void setCapacity(int cap) throws RemoteException; public int getCapacity() throws RemoteException; public double getTonnage() throws RemoteException; public void setTonnage(double tons) throws RemoteException; }
We put this interface into the com.titan.ship
package, which we will use for all the components of the Ship bean.
This means that the code should reside in a development directory
named dev/com/titan/ship
. This is the same
convention for package and directory names that we used for the Cabin
bean.
The Ship
definition uses a series of
accessor methods whose
names begin with
set
and get
. This is not a
required signature pattern, but it is the naming convention used by
most Java developers when obtaining and changing the values of object
attributes or fields. These methods are often referred to as
setters and getters (a.k.a.
mutators and
accessors) and the attributes that they manipulate can be
called
properties.[16] These properties
should be defined independently of the anticipated storage structure
of the data. In other words, you should design the remote interface
to model the business concepts, not the underlying
data. Just because there’s a capacity
property doesn’t mean that there has to be a capacity field in
the bean or the database; the getCapacity()
method
could conceivably compute the capacity from a list of cabins, by
looking up the ship’s model and configuration, or with some
other algorithm.
Defining entity properties according to the business concept and not the underlying data is not always possible, but you should try to employ this strategy whenever you can. The reason is two-fold. First, the underlying data doesn’t always clearly define the business purpose or concept being modeled by the entity bean. Remote interfaces will be used by developers who know the business, not the database configuration. It is important to them that the entity bean reflect the business concept. Second, defining the properties of the entity bean independent of the data allows the bean and data to evolve separately. This is important because it allows a database implementation to change over time; it also allows for new behavior to be added to the entity bean as needed. If the bean’s definition is independent of the data source, the impact of these evolutions is limited.
A primary key is an object that uniquely identifies an entity bean according to the bean type, home interface, and container context from which it is used.
EJB 1.1: In container-managed persistence, a primary key can be a serializable object defined specifically for the bean by the bean developer, or its definition can be deferred until deployment. We will examine deployer-defined primary keys later. For now we will consider primary keys defined by the bean developer.
For our purposes, we will define all primary keys as serializable
classes with names that match the pattern
BeanName
PK
. Therefore,
the primary key for the Ship bean will be ShipPK
.
Unlike the remote interface and the home interface, the
primary key is a class, and its
definition is normally bound to the bean class definition, which we
have not yet addressed. Peeking ahead, however, we can make a
preliminary definition of a primary key that wraps an integer value
called id
. Later, we will have to make sure that
this field has a corresponding field in the bean class with a
matching identifier (name) and data type.
package com.titan.ship; import java.io.Serializable; public class ShipPK implements java.io.Serializable { public int id; public ShipPK() {} public ShipPK(int value) { id = value; } public boolean equals(Object obj) { if (obj == null || !(obj instanceof ShipPK)) return false; else if (((ShipPK)obj).id == id) return true; else return false; } public int hashCode(){ return id; } public String toString(){ return String.valueOf(id); } }
The primary key defines attributes that can be used to locate a
specific bean in the database. In this case, we need only one
attribute, id
, but in other cases, a primary key
may have several attributes, all of which uniquely identify a
bean’s data.
As discussed in Chapter 4, primary keys should
override the
equals()
and hashCode()
methods of Object
to
ensure that these method behave properly when invoked. For example,
two ShipPK
objects with the same
id
value may not evaluate to the same hash code
unless the
hashCode()
method is overridden as shown in the previous code example. This can
cause problems if you store primary keys in a hash table and expect
that primary keys for the same entity will evaluate to the same
position in the table. In addition, we have overridden the
toString()
method to return a meaningful value.
(The default implementation defined in Object
returns the class name of the object appended to the object identity
for that name space. Our implementation simply returns the
String
value of the id
, which
has more meaning.)
The primary key for the Ship bean is fairly simple. More complex keys—ones with multiple values—will require more intelligent hash code algorithms to ensure as few collisions in a hash table as possible.
The
ShipPK
class also defines two constructors: a
no-argument constructor and an overloaded constructor that sets the
id
field. The overloaded constructor is a
convenience method that reduces the number of steps required to
create a primary key. The no-argument constructor is
required for
container-managed persistence. When a new bean is created, the
container automatically instantiates the primary key using the
Class.newInstance()
method, and populates it from the bean
class’s container-managed fields. A no-argument constructor
must exist in order for that to work. You’ll learn more about
the relationship between the primary key and the bean class later in
this section.
The EJB specification requires that all fields in the primary key
class be declared public
. This
requirement ensures that the container can read the fields at runtime
via Java reflection. Some EJB servers may be able to read fields with
more restrictive access modifiers depending on how the security
manager is designed, but making the fields public
ensures that fields are always accessible, regardless of the
server’s vendor. Portability is the key reason that the primary
key’s fields must be public.
Because the primary key will be used in remote invocations, it must also adhere to the restrictions imposed by Java RMI-IIOP. These are addressed in Chapter 5, but for most cases, you just need to make the primary key serializable.
Both EJB 1.0 and EJB 1.1 specifications allow two types of primary
keys:
compound and single-field keys. In either case, the
primary key must fulfill two criteria: it
must be a valid Java RMI type ( Java RMI-IIOP value type for EJB
1.1), so it must be serializable; and it must implement
equals()
and hashCode()
methods
appropriately.
A compound primary key is a class that implements
Serializable
and contains one or more public
fields whose names and types match a subset of the container-managed
fields in the bean class. ShipPK
is a typical
example of a compound primary key. In this class, the
ShipPK.id
field must map to a
ShipBean.id
field of type int
in the ShipBean
class. A compound key may have
several fields that map to corresponding fields in the bean class.
The String
class and the
wrapper classes for the
primitive data types can also be
used as primary keys. In the case of the ShipBean
,
for example, we could have specified an Integer
type as the primary key:
public interface ShipHome extends javax.ejb.EJBHome {
public Ship findByPrimaryKey(java.lang.Integer key
)
throws FinderException, RemoteException;
...
}
In this case, there is no explicit primary key class. However, there
must still be an identifiable primary key within the bean class
itself. That is, there must be a single field in the bean class with
the appropriate type. For the ShipBean
, we would
need to change the id
field to be of type
java.lang.Integer
.
Although primary keys can be primitive wrappers
(Integer
, Double
,
Long
, etc.), primary keys cannot be primitive
types (int
, double
,
long
, etc.); some of the semantics of EJB
interfaces prohibit the use of primitives. For example, the
EJBObject.getPrimaryKey()
method returns an
Object
type, thus forcing primary keys to be
Object
s. As you learn more about the EJB,
you’ll discover other reasons that primitives can’t be
used for single-field keys.
In EJB 1.0, the specification is unclear about whether or not single-field types like String or primitive wrapper types are supported. Some EJB 1.0 servers support them, while others only support compound primary keys. Consult your vendor documentation to be sure, or use compound primary keys. When single-field types are supported, there must be only one container-managed field of that type in the bean class. Otherwise, the container doesn’t know to which field it should map the primary key.
EJB 1.1 is unambiguous in its support for single-field keys. With
single-field types, you cannot identify the matching field in the
bean class by name, since the primary key is not a named field.
Instead, you use the
<primkey-field>
tag in the deployment descriptor to
specify one of the bean’s container-managed fields as the
primary key:
<ejb-jar>
<enterprise-beans>
<entity>
<primkey-field>id</primkey-field>
...
</ejb-jar>
The primkey-field
(single-field
keys) is not used in the Ship bean example. The Ship bean uses
primary key class, ShipPK
, but the use of
primkey-field
is explored in more Chapter 10.
One objective of EJB is to create a market for third-party components that can be used independently. Container-managed persistence beans provide an excellent model for a component market because they make few assumptions about the underlying database. One problem with container-managed persistence in EJB 1.0 was that the bean developer had to define the primary key before the bean was deployed. In turn, this requirement forced the developer to make assumptions about the environment in which the bean would be used, and thus it limited the bean’s portability across databases. For example, a relational database will use a set of columns in a table as the primary key, to which bean fields map nicely. An object database, however, uses a completely different mechanism for indexing objects to which a primary key may not map very well. The same is true for legacy systems and Enterprise Resource Planing (ERP) systems. To overcome this problem, EJB 1.1 allows the primary key to remain undefined until the bean is deployed. An undefined primary key allows the deployer to choose a system-specific key at deployment time. An object database may generate an Object ID, while an ERP system may generate some other primary key. These keys are generated by the database or backend system automatically. This may require that the CMP bean be altered or extended to support the key, but this is immaterial to the bean developer; she concentrates on the business logic of the bean and leaves the indexing to the container.
To facilitate an undefined primary key, the bean class and its
interfaces use the
Object
type to identify the primary key.
The following code shows how the home interface and bean class would
be defined for a container-managed bean with an undefined primary
key:
public interface ShipHome extends EJBHome { public Ship findByPrimaryKey(java.lang.Object primaryKey
) throws RemoteException, FinderException; ... } public class ShipBean extends EntityBean { public String name; public int capacity; public double tonnage; publicjava.lang.Object
ejbCreate() { ... } ... }
The use of an undefined primary key means that the bean developer and
application developer (client code) must work with a
java.lang.Object
type and not a specific primary
key type, which
can be limiting.
The home interface of any
entity bean is used to create, locate, and remove objects from EJB
systems. Each entity bean type has its own home interface. The home
interface defines two basic kinds of methods: zero or more
create methods and one or more
find methods.[17] The create methods
act like remote constructors and define how new Ship beans are
created. (In our home interface, we only provide a single
create()
method.) The find method is used to
locate a specific ship or ships.
The following code contains the complete definition of the
ShipHome
interface:
package com.titan.ship; import javax.ejb.EJBHome; import javax.ejb.CreateException; import javax.ejb.FinderException; import java.rmi.RemoteException; import java.util.Enumeration; public interface ShipHome extends javax.ejb.EJBHome { public Ship create(int id, String name, int capacity, double tonnage) throws RemoteException,CreateException; public Ship create(int id, String name) throws RemoteException,CreateException; public Ship findByPrimaryKey(ShipPK primaryKey) throws FinderException, RemoteException; public Enumeration findByCapacity(int capacity) throws FinderException, RemoteException; }
Enterprise JavaBeans specifies that create methods in the home
interface must throw the
javax.ejb.CreateException
. In the case of container-managed persistence, the container needs a
common exception for communicating problems experienced during the
create process.
With container-managed persistence,
implementations of the find
methods are generated automatically at deployment time. Different EJB
container vendors employ different strategies for defining how the
find methods work. Regardless of the implementation, when you deploy
the bean, you’ll need to do some work to define the rules of
the find method.
findByPrimaryKey()
is a standard method that all home interfaces for entity beans must
support. This method locates beans based on the attributes of the
primary key. In the case of the Ship bean, the primary key is the
ShipPK
class, which has one attribute,
id
. With relational databases, the primary key
attributes usually map to a primary key in a table. In the
ShipPK
, for example, the id
attribute maps to the ID
primary key column in the
SHIP
table. In an object-oriented database, the
primary key’s attributes might point to some other unique
identifier.
EJB allows you to specify other find methods in the home interface,
in addition to findByPrimaryKey()
. All find
methods must have names that match the pattern
find
lookup-type
. So,
for example, if we were to include a find method based on the Ship
bean’s capacity, it might be called
findByCapacity(int
capacity)
.
In container-managed persistence, any find method included in the
home interface must be explained to the container. In other words,
the deployer needs to define how the find method should work in terms
that the container understands. This is done at deployment time,
using the vendor’s deployment tools and syntax specific to the
vendor.
Find methods return either the remote-interface type appropriate for
that bean, or an instance of
java.util.Enumeration
.
The EJB 1.1 specification also allows multiple references to be returned as a java.util.Collection
type, which provides more flexibility to application and bean developers.
Specifying a remote-interface type indicates that the method only
locates one bean. The findByPrimaryKey()
method
obviously returns one remote reference because there is a one-to-one
relationship between a primary key’s value and an entity. The
findByCapacity(int
capacity)
method, however, could return several remote references, one for
every ship that has a capacity equal to the parameter
capacity
. The possibility of returning several
remote references requires the use of the
Enumeration
type or a Collection
type (EJB 1.1
only). Enterprise JavaBeans specifies that any find method used in a
home interface must throw the
javax.ejb.FinderException
. Find methods that return a single remote reference throw a
FinderException
if an
application error occurs and a
javax.ejb.ObjectNotFoundException
if a matching
bean cannot be found. The
ObjectNotFoundException
is a subtype of FinderException
and is only thrown
by find methods that return single remote references.
Find methods that return an Enumeration
or
Collection
type (multi-entity finders) return an
empty collection (not a null reference) if no matching beans can be
found or throw a FinderException
if an application
error occurs.
Find methods that return an Enumeration
return
null
if no matching beans can be found or throw a
FinderException
if a failure in the request
occurs.
How find methods are mapped to the database for container-managed persistence is not defined in the EJB specification; it is vendor-specific. Consult the documentation provided by your EJB vendor to determine how find methods are defined at deployment time.
Both the
remote interface and
home
interface
extend, indirectly, the
java.rmi.Remote
interface. Remote interfaces must
follow several guidelines, some of which apply to the return types
and parameters that are allowed. To be compatible, the
actual return types and parameter types used in
the java.rmi.Remote
interfaces must be primitives,
String
types, java.rmi.Remote
types, or serializable types.
There is a difference between
declared
types, which are checked by the compiler, and
actual types, which are checked by the runtime.
The types which may be used in Java RMI are
actual types, which are either primitive types,
object types implementing (even indirectly)
java.rmi.Remote
, or object types implementing
(even indirectly) java.io.Serializable
. The
java.util.Enumeration
type returned by
multi-entity find methods is, for example, is a perfectly valid
return type for a remote method, provided that the concrete class
implementing Enumeration
is
Serializable
. So 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 the
runtime.
Here is a list of the types that can be passed as parameters or returned in Java RMI:
All methods defined in remote interfaces must throw
java.rmi.RemoteException
. A
RemoteException
is thrown by the underlying system
(the EJB object) when a communication error or system failure of some
kind occurs. Although methods in the remote interface and home
interface are required by the compiler to declare that they throw
RemoteException
, it’s not required that the
matching methods in the bean class actually throw it.
No bean is complete without
itsimplementation class. Now that we have
defined the Ship bean’s remote interfaces and primary key, we
are ready to define the ShipBean
itself. The
ShipBean
will reside on the EJB server. When a
client application or bean invokes a business method on the Ship
bean’s remote interface, that method invocation is received by
the EJB object, which then delegates it to the
ShipBean
.
When developing any bean, we have to use the bean’s remote
interfaces as a guide. Business methods defined in the remote
interface must be duplicated in the bean class. In container-managed
beans, the create methods of the home interface must also have
matching methods in the bean class according to the EJB
specification. Finally, callback methods defined by the
javax.ejb.EntityBean
interface must be
implemented. Here is the code for the ShipBean
class. We have omitted the ejbCreate()
method,
which will be discussed later because it’s different in EJB 1.0
and EJB 1.1:
package com.titan.ship; import javax.ejb.EntityContext; public class ShipBean implements javax.ejb.EntityBean { public int id; public String name; public int capacity; public double tonnage; public EntityContext context; /************************************* * ejbCreate() method goes here *************************************/ public void ejbPostCreate(int id, String name, int capacity, double tonnage){ ShipPK pk = (ShipPK)context.getPrimaryKey(); // Do something useful with the primary key. } public void ejbPostCreate(int id, String name) { Ship myself = (Ship)context.getEJBObject(); // Do something useful with the EJBObject reference. } public void setEntityContext(EntityContext ctx) { context = ctx; } public void unsetEntityContext() { context = null; } public void ejbActivate() {} public void ejbPassivate() {} public void ejbLoad() {} public void ejbStore() {} public void ejbRemove() {} public String getName() { return name; } public void setName(String n) { name = n; } public void setCapacity(int cap) { capacity = cap; } public int getCapacity() { return capacity; } public double getTonnage() { return tonnage; } public void setTonnage(double tons) { tonnage = tons; } }
The Ship bean defines four
persistent fields:
id
, name
,
capacity
, and tonnage
. No
mystery here: these fields represent the persistent state of the Ship
bean; they are the state that defines a unique ship entity in the
database. The Ship bean also defines another field,
context
, which holds the bean’s
EntityContext
. We’ll have more to say about
this later.
The set and get methods are the
business methods we defined for the Ship
bean; both the remote interface and the bean class must support them.
This means that the signatures of these methods must be exactly the
same, except for the javax.ejb.RemoteException
.
The bean class’s business methods aren’t required to
throw the RemoteException
. This makes sense
because these methods aren’t actually invoked
remotely—they’re invoked by the EJB object. If a
communication problem occurs, the container will throw the
RemoteException
for the bean
automatically.
To make the ShipBean
an entity bean, it must implement the
javax.ejb.EntityBean
interface. The
EntityBean
interface contains a number of callback
methods that the container uses to alert the bean instance of various
runtime events:
public interface javax.ejb.EntityBean extends javax.ejb.EnterpriseBean { public abstract void ejbActivate() throws RemoteException; public abstract void ejbPassivate() throws RemoteException; public abstract void ejbLoad() throws RemoteException; public abstract void ejbStore() throws RemoteException; public abstract void ejbRemove() throws RemoteException; public abstract void setEntityContext(EntityContext ctx) throws RemoteException; public abstract void unsetEntityContext() throws RemoteException; }
Each callback method is called at a specific time during the life
cycle of a ShipBean
. In many cases,
container-managed beans, like the ShipBean
,
don’t need to do anything when a callback method is invoked.
Container-managed beans have persistence managed automatically, so
many of the resources and logic that might be managed by these
methods are already handled by the container. Except for the
EntityContext
methods, we will leave the
discussion of these methods for the section on bean-managed
persistence. This version of the Ship bean has empty implementations
for its callback methods. It is important to note, however, that even
a container-managed bean can take advantage of these callback methods
if needed; we just don’t need them in our
ShipBean
at this time.
The
first method called after a bean instance is created is
setEntityContext()
. As the method signature
indicates, this method passes the bean instance a reference to a
javax.ejb.EntityContext
, which is really the bean
instance’s interface to the container. The definition of
EntityContext
is as follows:
public interface javax.ejb.EntityContext extends javax.ejb.EJBContext { public abstract EJBObject getEJBObject() throws IllegalStateException; public abstract Object getPrimaryKey() throws IllegalStateException; }
The setEntityContext()
method is called prior to
the bean instance’s entry into the instance pool. In Chapter 3, we discussed the instance pool that EJB
containers maintain, where instances of entity and stateless session
beans are kept ready to use. EntityBean
instances
in the instance pool are not associated with any data in the
database; their state is not unique. When a request for a specific
entity is made by a client, an instance from the pool is chosen,
populated with data from the database, and assigned to service the
client.
When an entity from the pool is assigned to service a client, the instance is associated with or “wrapped” by an EJB object. The EJB object provides the remote reference, or stub, that implements the bean’s remote interface and is used by the client. When the client invokes methods on the stub, the EJB object receives the message and delegates it to the bean instance. The EJB object protects the bean instance from direct contact with the client by intervening in business method invocations to ensure that transactions, security, concurrency, and other primary services are managed appropriately.
The EJB object also maintains the bean instance’s identity,
which is available from the EntityContext
. The
EntityContext
allows the bean instance to obtain
its own primary key and a remote reference to the EJB object.
Containers often use a swapping strategy to get maximum use of bean
instances. Swapping doesn’t impact the client reference to the
stub because the stub communicates with the EJB object, not the bean
instance. So, as bean instances are assigned to and removed from
association with the EJB object server, the server maintains a
constant connection to the stub on the client.
When a method is invoked on the EJB object via the stub, a bean
instance from the pool is populated with the appropriate data and
assigned to the EJB object. When a bean instance is assigned to an
EJB object, its EntityContext
changes so that the
primary key and EJB object obtainable through the
EntityContext
match the EJB object the bean
instance is currently associated with. Because the bean
instance’s identity changes every time the bean is swapped into
a different EJB object, the values returned by the
EntityContext
change depending on which bean
instance it is associated with.
At the end of the bean instance’s life, after the bean instance
is removed permanently from the
instance pool and before the bean
instance is garbage collected, the
unsetEntityContext()
method is called, indicating
that the bean instance’s EntityContext
is no
longer implemented by the container.
When
a create method is invoked on the home interface, the EJB home
delegates it to the bean instance in the same way that business
methods on the remote interface are handled. This means that we need
an ejbCreate()
method in the bean class that
corresponds to each create()
method in the home
interface. Here are the ejbCreate()
methods that
we omitted from the source code for the ShipBean
class. Note the difference between EJB 1.0 and 1.1:
// For EJB 1.1, returns a ShipPK public ShipPK ejbCreate(int id, String name, int capacity, double tonnage) { this.id = id; this.name = name; this.capacity = capacity; this.tonnage = tonnage; return null; } public ShipPK ejbCreate(int id, String name) { this.id = id; this.name = name; capacity = 0; tonnage = 0; } // For EJB 1.0: returns void public void ejbCreate(int id, String name, int capacity, double tonnage) { this.id = id; this.name = name; this.capacity = capacity; this.tonnage = tonnage; } public void ejbCreate(int id, String name) { this.id = id; this.name = name; capacity = 0; tonnage = 0; }
The ejbCreate()
method returns
void
in EJB 1.0 and a null
value of type ShipPK
for the bean’s primary
key in EJB 1.1. The end result is the same: in both EJB 1.0 and EJB
1.1, the return value of the ejbCreate()
method
for a container-managed bean is ignored. EJB 1.1 changed its return
value from void
to the primary key type to
facilitate subclassing; the change was made so that it’s easier
for a bean-managed bean to extend a container-managed bean. In EJB
1.0, this is not possible because Java won’t allow you to
overload methods with different return values. By changing this
definition so that a bean-managed bean can extend a container-managed
bean, the new specification allows vendors to support
container-managed persistence by extending the container-managed bean
with a generated bean-managed bean—a fairly simple solution to
a difficult problem. Bean developers can also take advantage of
inheritance to change an existing CMP bean into a BMP bean, which may
be needed to overcome difficult persistence problems.
For every create()
method defined in the entity
bean’s home interface, there must be a corresponding
ejbPostCreate()
method in the bean instance class.
In other words, ejbCreate()
and
ejbPostCreate()
methods occur in pairs with
matching signatures; there must be one pair for each
create()
method defined in the home interface.
In a container-managed bean, the ejbCreate()
method is called just prior to writing the bean’s
container-managed fields to the database. Values passed in to the
ejbCreate()
method should be used to initialize
the fields of the bean instance. Once the
ejbCreate()
method completes, a new record, based
on the container-managed fields, is written to the database.
Each ejbCreate()
method must have parameters that
match a create()
method in the home interface. The
ShipHome
, for example, specifies two
create()
methods. According
to the EJB specification, our ShipBean
class must
therefore have two ejbCreate()
methods that match
the parameters of the ShipHome
create()
methods. If you look at the
ShipBean
class definition and compare it to the
ShipHome
definition, you can see how the
parameters for the create methods match exactly in type and sequence.
This enables the container to delegate the
create()
method on the home interface to the
proper ejbCreate()
method in the bean instance.
The EntityContext
maintained by the bean instance
does not provide it with the proper identity until the
ejbCreate()
method has completed. This means that
during the course of the ejbCreate()
method, the
bean instance doesn’t have access to its primary key or EJB
object.[18] The
EntityContext
does, however, provide the bean with
information about the caller’s identity, access to its EJB home
object, and properties.
In EJB 1.1, the bean can also use the JNDI environment naming context to access other beans and resource managers like javax.sql.DataSource
.
The bean developer must ensure that the
ejbCreate()
method sets the persistent fields that
correspond to the fields of the
primary key. When a primary key is defined for a container-managed
bean, it must define fields that match one or more of the
container-managed (persistent) fields in the bean class. The fields
must match with regard to type and name exactly. At runtime, the
container will assume that fields in the primary key match some or
all of the fields in the bean class. When a new bean is created, the
container will use those container- managed fields in the bean class
to instantiate and populate a primary key for the bean automatically.
In the case of the ShipBean
, the container-managed
id
field corresponds to the
ShipPK.id
field. When the record is inserted, the
ShipBean.id
field is used to populate a newly
instantiated ShipPK
object.
Once the bean’s state has been populated and its
EntityContext
established, an
ejbPostCreate()
method is invoked. This method
gives the bean an opportunity to perform any post-processing prior to
servicing client requests.
The bean identity isn’t available to the bean during the call
to ejbCreate()
, but is available in the
ejbPostCreate()
method. This means that the bean
can access its own primary key and EJB object, which can be useful
for initializing the bean instance prior to
servicing business method invocations. You can use the
ejbPostCreate()
method to perform any additional
initialization. Each ejbPostCreate()
method must
have the same parameters as its corresponding
ejbCreate()
method. The
ejbPostCreate()
method returns
void
.
It may seem a little silly to define the
ejbPostCreate()
method with the same parameters as
its matching ejbCreate()
method, especially in the
ShipBean
where the instance variables are just as
easily retrieved from the bean instance’s fields. There are,
however, two very good reasons for matching the parameter lists.
First, it indicates which ejbPostCreate()
method
is associated with which ejbCreate()
method. This
ensures that the container calls the correct
ejbPostCreate()
method after
ejbCreate()
is done. Second, it is possible that
one of the parameters passed is not assigned to a bean instance field
or is only relevant to the ejbPostCreate()
. In
either case, you would need to duplicate the parameters of the
ejbCreate()
method to have that information
available in the ejbPostCreate()
method.
To understand how a bean instance gets up and running, we have to think of a bean in the context of its life cycle. Figure 6.1 shows the sequence of events during a portion of the bean’s life cycle, as defined by the EJB specification. Every EJB vendor must support this sequence of events.
The process begins when the client invokes one of the
create()
methods on the bean’s EJB home. A
create()
method is invoked on the EJB home stub
(step 1), which communicates the method to the EJB home across the
network (step 2). The EJB home plucks a ShipBean
instance from the pool and invokes its corresponding
ejbCreate()
method (step 3).
The create()
and ejbCreate()
methods pair are responsible for initializing the bean instance so
that the container can insert a record into the database. In the case
of the ShipBean
, the minimal information required
to add a new ship to the system is the ship’s unique
id
and a name
. These fields are
initialized during the ejbCreate()
method
invocation (step 4).
In a container-managed EntityBean
, the container
uses the bean instance’s public fields (id
,
name
, capacity
, and
tonnage
) to insert a record in the database which
it reads from the bean (step 5). Only those fields described as
container-managed in the deployment descriptor are accessed
automatically. Once the container has read the container-managed
fields from the bean instance, it will automatically insert a new
record into the database using those fields (step 6). How the data is
written to the database is defined when the bean’s fields are
mapped at deployment time. In our example, a new record is inserted
into the SHIP
table.
Once the record has been inserted into the database, the bean
instance is ready to be assigned to an EJB object (step 7). Once the
bean is assigned to an EJB object, the bean’s identity is
available. This is when the ejbPostCreate()
method
is invoked (step 8).
Finally, when the ejbPostCreate()
processing is
complete, the bean is ready to service client requests. The EJB
object stub is created and returned to client application, which will
use it to invoke business methods on the bean (step 9).
The
process of ensuring
that the database record and the entity bean instance are equivalent
is called
synchronization
. In container-managed
persistence, the bean’s container-managed fields are
automatically synchronized with the
database. In most
cases, we will not need the ejbLoad()
and
ejbStore()
methods because persistence in
container-managed beans is uncomplicated.
Leveraging the
ejbLoad()
and
ejbStore()
callback methods in container-managed
beans, however, can be useful if more sophisticated logic is needed
when synchronizing container-managed fields. Data intended for the
database can be reformatted or compressed to conserve space; data
just retrieved from the database can be used to calculate derived
values for nonpersistent properties.
Imagine a hypothetical bean class that includes an
array of Strings
that you want to store in the
database. Relational databases do not support arrays, so you need to
convert the array into some other format. Using the
ejbLoad()
and ejbStore()
methods in a container-managed bean allows the bean instance to
reformat the data as appropriate for the state of the bean and the
structure of the database. Here’s how this might work:
public class HypotheticalBean extends javax.ejb.EntityBean { public transient String [] array_of_messages; public String messages; // Parses the messages into an array_of_messages. This is called on a // container-managed bean just after the bean instance is synchronized // with the database (right after the bean gets its data). public void ejbLoad() { StringTokenizer st = new StringTokenizer(messages, "~"); array_of_messages = new String[st.countTokens()]; for (int i = 0; st.hasMoreTokens(); i++) { array_of_messages[i] = st.nextToken(); } } // Creates a '~' delimited string of messages from the // array_of_messages. This method is called immediately // prior to synchronization of the database with the bean; // just before the bean is written to the database. public void ejbStore() { messages = new String(); int i = 0; for (; i < array_of_messages.length-1;i++) { messages += array_of_messages[i]+"~"; } messages += array_of_messages[i]; } // a business method that uses the array_of_messages public String [] getMessages() { return array_of_messages; } ... }
Just before the container reads the container-managed field
messages
, it calls the
ejbStore()
method. This method makes a tilde (~)
delimited string from the array_of_messages
and
places the new string in the messages
field. This
trick formats the messages so the database can store them easily.
Just after the container updates the fields of the
HypotheticalBean
with fresh data from the
database, it calls the ejbLoad()
method, which
parses the tilde-delimited message
field and
populates the array_of_messages
with the strings.
This reformats the database data into an array that is easier for the
HypotheticalBean
to return to the client.
With a complete definition of the Ship bean, including the remote interface, home interface, and primary key, we are ready to create a deployment descriptor. The following listing shows the bean’s XML deployment descriptor. This deployment descriptor is not significantly different from the descriptor we created for the Cabin bean in Chapter 4, so it won’t be discussed in detail.
<?xml version="1.0"?> <!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 1.1//EN" "http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd"> <ejb-jar> <enterprise-beans> <entity> <description> This bean represents a cruise ship. </description> <ejb-name>ShipBean</ejb-name> <home>com.titan.ship.ShipHome</home> <remote>com.titan.ship.Ship</remote> <ejb-class>com.titan.ship.ShipBean</ejb-class> <persistence-type>Container</persistence-type> <prim-key-class>com.titan.ship.ShipPK</prim-key-class> <reentrant>False</reentrant> <cmp-field><field-name>id</field-name></cmp-field> <cmp-field><field-name>name</field-name></cmp-field> <cmp-field><field-name>capacity</field-name></cmp-field> <cmp-field><field-name>tonnage</field-name></cmp-field> </entity> </enterprise-beans> <assembly-descriptor> <security-role> <description> This role represents everyone who is allowed full access to the Ship bean. </description> <role-name>everyone</role-name> </security-role> <method-permission> <role-name>everyone</role-name> <method> <ejb-name>ShipBean</ejb-name> <method-name>*</method-name> </method> </method-permission> <container-transaction> <method> <ejb-name>ShipBean</ejb-name> <method-name>*</method-name> </method> <trans-attribute>Required</trans-attribute> </container-transaction> </assembly-descriptor> </ejb-jar>
Save the Ship bean’s deployment descriptor into the
com/titan/ship
directory as
ejb-jar.xml
.
Now that you have put all the necessary files in one directory,
creating the JAR file is easy. Position yourself in the
dev
directory that is just above the
com/titan/ship
directory tree, and execute the
jar
utility as you did in Chapter 4:
dev % jar cf ship.jar com/titan/ship/*.class META-INF/ejb-jar.xml F:..dev>jar cf ship.jar com itanship*.class META-INFejb-jar.xml
The c
option tells the jar
utility to create a new JAR file that contains the files indicated in
subsequent parameters. It also tells the jar
utility to stream the resulting JAR file to standard output. The
f
option tells jar to
redirect the standard output to a new file named in the second
parameter (ship.jar
). It’s important to get
the order of the option letters and the command-line parameters to
match.
If you’re
using EJB 1.0, you need to create an
old-style deployment descriptor, which is a serialized Java object.
To create the deployment descriptor, we write a
MakeDD
application, just as we did in Chapter 4 for the Cabin bean. Other than changing the
name of classes, the JNDI name, and the container-managed fields,
there isn’t much difference between Ship’s
MakeDD
application and the Cabin bean’s.
package com.titan.ship; import javax.ejb.deployment.EntityDescriptor; import javax.ejb.deployment.ControlDescriptor; import javax.naming.CompoundName; import java.util.Properties; import java.io.FileOutputStream; import java.io.ObjectOutputStream; import java.lang.reflect.Field; public class MakeDD { public static void main(String args []) { try { if (args.length <1) { System.out.println("must specify target directory"); return; } EntityDescriptor shipDD = new EntityDescriptor(); shipDD.setEnterpriseBeanClassName("com.titan.ship.ShipBean"); shipDD.setHomeInterfaceClassName("com.titan.ship.ShipHome"); shipDD.setRemoteInterfaceClassName("com.titan.ship.Ship"); shipDD.setPrimaryKeyClassName("com.titan.ship.ShipPK"); Class beanClass = ShipBean.class; Field [] persistentFields = new Field[4]; persistentFields[0] = beanClass.getDeclaredField("id"); persistentFields[1] = beanClass.getDeclaredField("name"); persistentFields[2] = beanClass.getDeclaredField("capacity"); persistentFields[3] = beanClass.getDeclaredField("tonnage"); shipDD.setContainerManagedFields(persistentFields); shipDD.setReentrant(false); CompoundName jndiName = new CompoundName("ShipHome", new Properties()); shipDD.setBeanHomeName(jndiName); ControlDescriptor cd = new ControlDescriptor(); cd.setIsolationLevel(ControlDescriptor.TRANSACTION_READ_COMMITTED); cd.setMethod(null); cd.setRunAsMode(ControlDescriptor.CLIENT_IDENTITY); cd.setTransactionAttribute(ControlDescriptor.TX_REQUIRED); ControlDescriptor [] cdArray = {cd}; shipDD.setControlDescriptors(cdArray); // Set the name to associate with the enterprise bean // in the JNDI name space. String fileSeparator = System.getProperties().getProperty("file.separator"); if (! args[0].endsWith(fileSeparator)) args[0] += fileSeparator; FileOutputStream fis = new FileOutputStream(args[0]+"ShipDD.ser"); ObjectOutputStream oos = new ObjectOutputStream(fis); oos.writeObject(shipDD); oos.flush(); oos.close(); fis.close(); } catch (Throwable t){t.printStackTrace();} } }
Compile this class and run it:
dev % java com.titan.ship.MakeDD com/titan/ship F:..dev>java com.titan.ship.MakeDD com itanship
If you run this application, you should end up with a file called
ShipDD.ser in the
com/titan/ship
directory. This is your
serialized DeploymentDescriptor
for the Ship bean.
We examined the code for this type of MakeDD
application in detail in Chapter 4, so we
won’t do it again here.
Next, place the Ship bean in a JAR file using the same process we
used for the Cabin and TravelAgent beans in Chapter 4. First, we have to specify a manifest file for
the Ship
bean, which we will save in the
com/titan/ship
directory:
Name: com/titan/ship/ShipDD.ser Enterprise-Bean: True
Now that the manifest is ready, we can JAR the Ship bean so that it’s ready for deployment. Again, we use the same process that we used for the Cabin and TravelAgent beans:
dev % jar cmf com/titan/ship/manifest ship.jar com/titan/ship/*.class com/titan/ship/*.ser F:..dev>jar cmf com itanshipmanifest ship.jar com itanship*.class com itanship*.ser
The Ship bean is now complete and ready to be deployed. Use the wizards and deployment utilities provided by your vendor to deploy the Ship bean into the EJB server.
In Chapter 4 and Chapter 5, you learned how to write a Java client that uses the EJB client API to access and work with enterprise beans. Here’s a simple client that accesses the Ship bean; it creates a single ship, the Paradise, that can handle 3,000 passengers:
package com.titan.ship; import javax.naming.InitialContext; import javax.naming.Context; import javax.naming.NamingException; import java.rmi.RemoteException; import java.util.Properties; public class Client_1 { public static void main(String [] args) { try { Context ctx = getInitialContext(); // EJB 1.0: Use native cast instead of narrow() Object ref = ctx.lookup("ShipHome"); ShipHome home = (ShipHome) PortableRemoteObjectnarrow(ref,ShipHome.class); Ship ship = home.create(1,"Paradise",3000,100000); int t = ship.getCapacity(); System.out.println("Capacity = " +t); } catch (Exception e){e.printStackTrace();} } public static Context getInitialContext() throws javax.naming.NamingException { Properties p = new Properties(); // ... Specify the JNDI properties specific to the vendor. return new javax.naming.InitialContext(p); } }
Once you have created the ship, you should be able to modify the client to look up the ship using its primary key, as shown in the following code:
package com.titan.ship; import javax.naming.InitialContext; import javax.naming.Context; import javax.naming.NamingException; import java.rmi.RemoteException; import java.util.Properties; import java.util.Enumeration; public class Client_2 { public static void main(String [] args){ try { Context ctx = getInitialContext(); // EJB 1.0: Use native cast instead of narrow(). Object ref = ctx.lookup("ShipHome"); ShipHome home = (ShipHome) PortableRemoteObject.narrow(ref,ShipHome.class); home.create(2,"Utopia",4500,8939); home.create(3,"Valhalla",3300,93939); ShipPK pk = new ShipPK(); pk.id = 1; Ship ship = home.findByPrimaryKey(pk); ship.setCapacity(4500); int capacity = ship.getCapacity(); Enumeration enum = home.findByCapacity(4500); while (enum.hasMoreElements()) { // EJB 1.0: Use native cast instead of narrow() ref = enum.nextElement(); Ship aShip = (Ship) PortableRemoteObject.narrow(ref,Ship.class); System.out.println(aShip.getName()); } } catch (Exception e){e.printStackTrace();} } public static Context getInitialContext() throws javax.naming.NamingException { Properties p = new Properties(); // ... Specify the JNDI properties specific to the vendor. return new javax.naming.InitialContext(p); } }
The preceding client code demonstrates that the ship was
automatically inserted into the database by the container. Now any
changes you make to ship attributes (name
,
capacity
, tonnage
) will result
in a change to the database.
Spend some time creating, finding, and removing Ships using the
client application. You should also explore the use of all the
methods in the EJBObject
and
EJBHome
interfaces as you did in Chapter 5
with the
Cabin and TravelAgent beans.
[16] Although EJB is different from its GUI counterpart, JavaBeans, the concept of accessors and properties are similar. You can learn about this idiom by reading Developing Java Beans™by Rob Englander (O’Reilly).
[18] Information that is not specific to the bean
identity, such as the environment properties, may be available during
the ejbCreate()
.