In Chapter 4, "Developing Your First Enterprise Beans", 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, "Developing Your First Enterprise Beans", 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.
NOTE
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, "Developing Your First Enterprise Beans", 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 calledproperties.[1] 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.
[1]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 Beansby Rob Englander (O'Reilly).
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.
NOTE
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 BeanNamePK. 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, "Developing Your First Enterprise Beans", 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, "The Client View", 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 Objects. 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, "XML Deployment Descriptors".
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; public java.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.[2] 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.
[2]Chapter 9, "Design Strategies" explains when you should not define any create methods in the home interface.
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 findlookup-type. So, for example, if we were to include a find method based on the Ship bean's capacity, it might be called findByCapacity(intcapacity). 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.
NOTE
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(intcapacity) 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:
Primitives: byte, boolean, char, short, int, long, double, float
Java serializable types: any class that implements or any interface that extends java.io.Serializable
Java RMI remote types: any class that implements or any interface that extends java.rmi.Remote
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, "Resource Management and the Primary Services", 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 ShipHomecreate() 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.[3] The EntityContext does, however, provide the bean with information about the caller's identity, access to its EJB home object, and properties.
[3] Information that is not specific to the bean identity, such as the environment properties, may be available during the ejbCreate().
NOTE
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, "Developing Your First Enterprise Beans", 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, "Developing Your First Enterprise Beans":
\dev % jar cf ship.jar com/titan/ship/*.class META-INF/ejb-jar.xml F:\..\dev>jar cf ship.jar com\titan\ship\*.class META-INF\ejb-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, "Developing Your First Enterprise Beans" 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\titan\ship
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, "Developing Your First Enterprise Beans", 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, "Developing Your First Enterprise Beans". 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\titan\ship\manifest ship.jar com\titan\ship\*.class com\titan\ship\*.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, "Developing Your First Enterprise Beans" and Chapter 5, "The Client View", 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, "The Client View" with the Cabin and TravelAgent beans.
Copyright © 2001 O'Reilly & Associates. All rights reserved.