2.2 Implementation

I l @ ve RuBoard

Now, let's discuss some implementation best practices.

2.2.1 Use Local Interfaces for Entity Beans

As I said before, letting clients access entity beans directly is a bad idea, and this is why session fa §ades should be used as the intermediate layer between entity beans and the client. Because all (or most) of your entity beans will be called by session beans, it makes perfect sense to make these calls use local interfaces. So, if you made your entity beans expose local interfaces, you would eliminate all network calls occurring between business logic and persistence layers . On the other hand, the session beans making up the fa §ade would still have remote interfaces, and clients would access them remotely, which is what you want.

2.2.2 Use Business Interfaces

Because the bean implementation class does not inherit from the bean interface, it's a fairly common error to get a mismatch in business method signatures between the implementation and the interface. Typically, you would have to package and deploy the EJBs to see the error. Needless to say, this can be very frustrating at times, especially because most of these errors are simple typos or missed method parameters.

One common practice is to use business interfaces to enforce compile-time checks. To do this, create a new interface that contains only business methods of your bean, and let both the remote/local bean interface and the implementation class inherit from it. However, even though this method will work for all types of beans (CMP, BMP, local, or remote), there are some inconveniences when dealing with remote beans. Namely, because remote bean interfaces must throw RemoteException , you are also forced to do this in your business interface. Also, a minor inconvenience is that all method parameters must be Serializable .

Example 2-2 shows the key interfaces for a remote bean.

Example 2-2. The order interfaces and implementation
 // Business interface     public interface Order {    public int getQuantity(  ) throws RemoteException;    public void setQuantity (int quantity) throws RemoteException;    public double getPricePerItem(  ) throws RemoteException;    public void setPricePerItem (double price) throws RemoteException;        public double getTotalPrice(  ) throws RemoteException; }     // Remote interface     public interface OrderRemote extends Order, EJBObject {    // All methods are inherited from Order and EJBObject. }     // Implementation     public class OrderBean extends Order, EntityBean {    private int quantity;    private double pricePerItem;        // Business interface implementation        public int getQuantity(  ) {           return quantity;    }    public void setQuantity (int quantity) {           this.quantity = quantity;    }    public double getPricePerItem(  ) {           return pricePerItem;    }    public void setPricePerItem (double price) {           this.pricePerItem = pricePerItem;    }    public double getTotalPrice(  ) {           return quantity*pricePerItem;    }        // Other EntityBean methods go here . . .  } 

Notice that you did not declare RemoteException in the throws clause of the implementation class. This is not necessary because although you are allowed to override methods with definitions that have fewer exceptions, the opposite doesn't work (i.e., you can't add new exceptions to a superclass method). Beans with local interfaces would use a business interface in the same way, except that there are no RemoteException and Serializable rules to follow.

Now, you might be tempted to use a business interface instead of the real local/remote interface so that whenever you get an interface to a bean, you cast it to its business interface and use it as if it's not an EJB but a standard Java class. In general, this is usually a bad idea because an EJB exposes other methods and functionality to the client ( create , remove , find , getHandle , getPrimaryKey , etc.). If you expose these methods, there is no point in having a separate business interface because it would be identical to the remote/local interface. If you don't expose these methods, you would still have to perform a lot of extra EJB management (i.e., handle remote exceptions, terminate sessions, etc.). If you need the client to work with simple Java classes, you can write a separate class that internally handles all EJB details, and possibly exposes the business interface to the client. This is called a "business delegate," and we'll talk about it later in the chapter. So, to recap this discussion: it's a bad idea to use business interfaces for anything other than compile-time syntax checking.

2.2.3 Handle Exceptions in EJB Code Correctly

Handling exceptions in a distributed J2EE environment can be confusing and very messy at times. Because most exceptions are never thrown in a properly debugged application, you might tend to ignore them, or just print them out to see where the error is. Nevertheless, writing a robust application requires you to properly deal with all possible error conditions that might appear.

To see how and where to handle various EJB exceptions, it's useful to separate them into three basic types:

RemoteException

This exception is declared in all remote interfaces exposed by an EJB. It is meant to be caught by the client of an EJB, and it usually indicates a network problem. A class implementing an EJB really cannot throw this type of exception. If you need to propagate a network problem (i.e., call another remote object and receive a RemoteException ), you should always wrap it in an EJBException , as you'll see next .

EJBException and its subclasses

This exception is thrown by the developer in the EJB implementation class and it's caught by the container. Throwing an EJBException usually signifies a major error, in which case the container will always do a rollback of the current transaction. This exception should be treated as a NullPointerException : it's a runtime exception, and in most cases, it should never be caught by the developer. To help diagnose errors, EJBException can be constructed with a causedBy exception ”effectively wrapping another exception. A common use of this is to rethrow an SQLException by wrapping it into an EJBException .

Application-level exceptions

Unlike the previous two types of exceptions, application-level exceptions are considered "normal," as far as the container is concerned . They are meant to be used in the spirit of standard Java exceptions to report various application errors such as: " user Bob is too old." An EJB can declare and throw these exceptions, and the client will receive them just like it would any normal Java exception, but because these exceptions might potentially travel over the network (from the EJB to the client), they must implement the Serializable interface. Good examples of application-level exceptions are the ones already predefined in the EJB framework, such as CreateException or FinderException .

2.2.4 Know When to Use Compound Primary Keys

If you decide to write a compound primary key class for an entity bean, you must override the hashCode( ) and equals( ) methods. It is also common practice to override the toString( ) method for debugging purposes. Overriding hashCode( ) and equals( ) is important because the container uses these methods to compare primary keys and to look up cached entity bean instances.

Implementing the equals( ) method is pretty simple ”all you have to do is compare various components of the primary key. On the other hand, implementing hashCode( ) is harder because a poorly implemented hashCode( ) method can slow down entity bean lookups, especially if there are a lot of instantiated beans. This happens because application servers use HashMap -type structures to store primary keys, and the performance of these structures relies on the fact that hashCode( ) returns different values for different objects.

In most cases, the implementation of hashCode( ) should depend on the data that the primary key contains. However, there are a few generic algorithms that work well. The one shown in Example 2-3 is taken from the java.util.List.hashCode( ) method. This algorithm simply adds all hashcodes of the primary key fields, multiplying each intermediate result by 31 (so that it's not a simple sum of hashcodes).

Example 2-3. Compound primary key
 public class CompoundPK implements Serializable {    private String str1;    private String str2;    private int int1;    private Date date1;        // Omitting irrelevant code here        public int hashCode(  )    {           int hash = 0;           hash = hash * 31 + str1.hashCode(  );           hash = hash * 31 + str2.hashCode(  );           hash = hash * 31 + int1;           hash = hash * 31 + date1.hashCode(  );           return hash;    }        public boolean equals (Object obj)    {           if (!(obj instanceof CompoundPK)) returns false;           CompoundPK other = (CompoundPK)obj;               return str1.equals (other.str1)               && str2.equals (other.str2)               && int1 =  = other.int1               && date1.equals (other.date1);    } } 

Using compound primary keys is usually not a preferred way of identifying records in a database. If possible, it's always better to have a primary key column. With a primary key column, you can index the database, which will accelerate the querying process. A primary key column is also easier on developers because the primary key will be only one number, and not a whole object.

2.2.5 Know How to Handle Large Queries

One of the biggest performance problems with entity beans is doing queries on data that return large collections of objects. This usually happens, for example, when you need to display a list of items to a user, each corresponding to a domain object handled by an entity bean. Simply using a finder method and then getting value objects from the returned entity beans will not work very well, especially if you need this data in the read-only form just for display purposes.

A good solution is to bypass entity beans entirely and query the database directly. You can have a stateless session bean that is responsible for query operations, and it can internally do a database query and return a list of plain value objects. A problem with this scheme is that the database records might change while the client is still using the query results. This is an accepted drawback of read-only query methods, and there is nothing you can do about it.

To see how this querying scheme might be implemented, suppose you have a User value object that contains name and country fields. Example 2-4 shows how to implement your stateless session bean.

Example 2-4. A stateless session bean
 public class UserQueryBean implements SessionBean {    // Skipping EJB methods        public LinkedList findCanadianUsers(  ) {           // Do a SELECT * FROM USERS WHERE COUNTRY='Canada' ORDER BY ID           // and get a result set back.           return createListFromRS (rs);    }        protected LinkedList createListFromRS (ResultSet rs) throws SQLException {           // This is what you'll return.           LinkedList list = new LinkedList(  );               while (rs.next(  )) {                  // Create value object with primary key value.                  UserValueObject user = new UserValueObject (rs.getInt(1));                      // Add other fields to it.                  user.setName (rs.getString(2));                  user.setCountry (rs.getString(3));                      // Add it to the list.                  list.add (user);           }               return list;    } } 

Note that you would frequently need to display results in pages, so it doesn't make sense to read all records into memory. In this case, you would need to have a ranged query (i.e., with "from" and "to" parameters). Implementing a ranged query is somewhat complicated. One way to do it is to use a JDBC 2.0 scrollable result set and move to the required row, as shown in Example 2-5.

Example 2-5. Implementation of a ranged query
 // Assume you have "from" and "to" parameters.     // Create a scrollable statement. Statement stmt = conn.createStatement (ResultSet.TYPE_SCROLL_INSENSITIVE,    ResultSet.CONCUR_UPDATABLE);     // Give hints to the driver. stmt.setFetchSize (to-from+1); // how many rows to fetch stmt.setFetchDirection (ResultSet.FETCH_FORWARD);     // Create result set. ResultSet rs = stmt.executeQuery ("SELECT * FROM MYTABLE"); rs.absolute(from);     for (int i=0; i<(to-from+1); i++) {    // Read a row from the result set.        rs.next(  ); } 

This method assumes the driver will not read rows you skipped with the rs.absolute( ) call, and that it will read only the number of rows specified in the hint. This is not always the case, so you might have to rely on database-specific ways of accomplishing the ranged query.

2.2.6 Use Dirty Flags in ejbStore

The ejbStore( ) method is usually called at the end of each transaction in which an entity bean participates. Given that the container cannot know if a bean-managed persistence (BMP) entity bean actually changed (and thus needs to be saved), it will always call the entity's ejbStore( ) . Because a typical transaction involves many entity beans, at the end of the transaction the container will call ejbStore( ) on all these beans, even though only a few might have actually changed. This is usually a big performance bottleneck because most entity beans will communicate with the database.

To solve this problem, a common solution is to use "dirty flags" in your EJB implementation. Using them is very simple: have a boolean flag indicate whether the data in the entity bean has changed. Whenever one of the business methods modifies the data, set the flag to true . Then, in ejbStore( ) , check the flag and update the database if needed. Example 2-6 illustrates this.

Example 2-6. Using dirty flags in ejbStore
 public class MyBean implements EntityBean {    // Your dirty flag    private boolean isModified;        // Some data stored in this entity bean    private String someString;        // You've skipped most of the unrelated methods . . .         public void ejbLoad(  )    {           // Load the data from the database.               // So far, your data reflects the database data.           isModified = false;    }        public void ejbStore(  )    {           // No need to save?           if (!isModified) return;               // Data has been changed; update the database.           //  . . .     }        // Some business methods        public String getSomeString(  )    {           return someString;    }        public void setSomeString (String str)    {           someString = str;           isModified = true;    } } 

For further optimization, you might also want to consider having several dirty flags, one for each update statement in your ejbStore( ) .

2.2.7 Use Lazy Loading

Most BMP entity beans are implemented so that ejbLoad( ) populates all data fields in the bean. In nearly every case this is fine because all this data will be used at some point. However, there are instances when you should delay loading parts of the data until it's actually needed. This process, called lazy-loading , will save memory and decrease database access times.

The most common example of this is an entity bean that contains large binary or text data in Blob or Clob form. If this data is accessed less often than other data fields in the entity bean, it makes sense to delay reading this data until the client requests it. To implement this lazy-loading technique, you simply have to use a flag to indicate whether the data has been loaded. Example 2-7 shows a basic instance of this.

Example 2-7. Using a lazy-loading technique
 public class ForumMessageBean implements EntityBean {    // Persisted fields    private Integer id;    private String title;    private String author;        private String messageText; // This is your large data field.    private boolean isMessageTextLoaded;        // Skipping irrelevant EJB methods . . .         public Integer ejbCreate (String title, String author,                              StringBuffer message) throws CreateException {           // Create new record in the database:           //    INSERT INTO message VALUES (.....)           // and get ID back.               this.id = id;           this.title = title;           this.author = author;           this.messageText = messageText;               // Indicate that the text is in the bean.           isMessageTextLoaded = true;    }        public void ejbLoad(  ) {           // Load data with an SQL statement such as:           //   SELECT id, title, author FROM message where id=?               // Delay loading of the message text.           isMessageTextLoaded = false;    }        // Accessor methods    public String getMessageText(  ) {           // Call helper method to load the text if needed.           loadMessageText(  );               return messageText;    }        public void setMessageText (String text) {           messageText = text;               // Set the lazy-load flag to true so that getMessageText           // doesn't overwrite this value.           isMessageTextLoaded = true;    }        private void loadMessage(  ) throws SQLExcpetion {            // If it's already loaded, you have nothing to do . . .             if (isMessageTextLoaded) return;                // Load the text from the database.            //  . . .                 isMessageTextLoaded = true;    } } 

Even though lazy-loading is a useful technique, people often use it improperly. From a design perspective, check if your lazy-loaded data can be better represented as a dependant object. If it can, splitting your object into two separate entities might be better: you will have a more natural design, and there will be no need for lazy-loading. An even better alternative is to use CMP entities with container-managed relationships between them. However, a lot of CMP engines do not support Clob and Blob data types, so if you take this approach, you will have to use BMP.

2.2.8 Cache JNDI Lookup Objects

To get a DataSource , or an EJB home interface, you typically create an InitialContext , and then do a lookup for the needed resource. These operations are usually very expensive, considering the fact that you perform them all the time throughout your code.

Fortunately, you can optimize these lookups fairly easily by doing the lookup only once, and then reusing the lookup result whenever you need it again ”effectively caching it. This is usually done with a singleton class. The singleton can be very simple and cache only specified objects, or it can be a sophisticated service locator that caches many arbitrary objects. An extra benefit of the singleton scheme is that you centralize the Java Naming and Security Interface (JNDI) names of your objects, so if the names change, you have to change your code in only one place: the singleton class.

Example 2-8 shows a singleton that stores several EJB home objects.

Example 2-8. Using a singleton class to cache lookup objects
 public class EJBHomeCache {    private static EHBHomeCache instance;        protected Context ctx = null;    protected FirstEJBHome firstHome = null;    protected SecondEJBHome secondHome = null;        private EJBHomeCache(  )    {           try {                  ctx = new InitialContext(  );                  firstHome = (FirstEJBHome)PortableRemoteObject.narrow (                         ctx.lookup ("java:comp/env/FirstEJBHome"),                         FirstEJBHome.class);                  secondHome = (SecondEJBHome)PortableRemoteObject.narrow (                         ctx.lookup ("java:comp/env/SecondEJBHome"),                         FirstEJBHome.class);               } catch (Exception e) {                  // Handle JNDI exceptions here, and maybe throw                  // application-level exception.           }    }    public static synchronized EJBHomeCache getInstance(  )    {           if (instance =  = null) instance = new EJBHomeCache(  );           return instance;    }    public FirstEJBHome getFirstEJBHome(  )    {           return firstHome;    }        public SecondEJBHome getSecondEJBHome(  )    {           return secondHome;    } 

The main shortcoming of this caching scheme is that it was assumed the JNDI names would always be the same, across all components using the singleton. In fact, every J2EE component separately declares the names of the resources it uses (through its deployment descriptor). In most cases, however, all references to a particular resource have the same name because it would be very confusing if you referred to the same EJB by different names, for example.

It's also possible to cache other factories provided by the J2EE environment, such as JMS, or custom connector factories. As a general rule, most resource factories stored in the JNDI are cacheable, but whether you should cache them will probably depend on how often they are used in the application.

2.2.9 Use Business Delegates for Clients

A business delegate is a plain Java class that delegates all calls to an EJB. It might seem too simple to be useful, but it's actually commonly used. The main reason to use a business delegate is to separate the EJB handling logic (e.g., getting remote interfaces, handling remote exceptions, etc.) from the client so that developers working on the client code don't have to know and worry about various EJB details.

Another benefit of business delegates is that you can initially leave all their business methods empty and, by doing so, give client developers something to work with so that they don't have to wait for EJBs to be developed. It's also not uncommon to implement straight JDBC code in the business delegate, instead of leaving it blank, in case you'd like to have a functional prototype. Even in a fully implemented J2EE application, delegates are useful for caching common client requests and computation results from the business layer.

Example 2-9 contains a business delegate that demonstrates some of the basic benefits discussed here. It delegates its calls to a remote stateless session bean that is actually a session fa §ade, retrying several times in case of a network problem.

Example 2-9. A business delegate
 public class BeanDelegate {    private static final int NETWORK_RETRIES = 3;    private BeanRemote bean;        public void create(  ) throws ApplicationError    {            // Here you get a bean instance.            try {                   InitialContext ctx = new InitialContext(  );                   BeanHome home = (BeanHome) PortableRemoteObject.narrow (                                  ctx.lookup ("ejb/BeanExample"),                                  BeanHome.class);                       // Retry in case of network problems.                   for (int i=0; i<NETWORK_RETRIES; i++)                          try {                                 bean = home.create(  );                                 break;                          } catch (RemoteException e) {                                 if (i+1 < NETWORK_RETRIES) continue;                                 throw new ApplicationError ("Network problem "                                        + e.toString(  ));                          }                  }           } catch (NamingException e) {                  throw new ApplicationError ("Error with bean");           }    }        public void remove(  ) throws ApplicationError    {           // Release the session bean here.               // Retry in case of network problems           for (int i=0; i<NETWORK_RETRIES; i++)                  try {                         bean.remove(  );                         break;                  } catch (RemoteException e) {                         if (i+1 < NETWORK_RETRIES) continue;                         throw new ApplicationError ("Network problem "                                + e.toString(  ));                  }           }    }        public int doBusinessMethod (String param) throws ApplicationError    {           // Call a bean method here.           for (int i=0; i<NETWORK_RETRIES; i++)                  try {                         return bean.doBusinessMethod (param);                  } catch (RemoteException e) {                         if (i+1 < NETWORK_RETRIES) continue;                         throw new ApplicationError ("Network problem "                                + e.toString(  ));                  }           }    } } 

You probably noticed the repeated network retry code in all the methods. If you want to make a cleaner implementation, the best solution is an intermediate wrapper class implemented as a dynamic proxy.

2.2.10 Write Dual CMP/BMP Entity Beans

If you are deploying on different platforms, some of which do not support CMP, the usual practice is to write a bean that can support both CMP and BMP, and then set the appropriate implementation in the deployment descriptors. The advantage of this method is that you gain the performance benefits of CMP where it is available, but the application will still work in BMP mode if necessary. Obviously, there is no need to implement BMP if you know CMP will always be available, but on the other hand, it's not uncommon to write specialized BMP versions to take advantage of particular database features, or even different persistence media.

The dual beans are actually fairly simple to implement. All you have to do is write a CMP implementation class, and then write a BMP implementation that overrides accessor methods and persistence- related methods ( ejbCreate( ) , ejbRemove( ) , etc.). You don't have to override ejbActivate( ) , ejbPassivate( ) , ejbSetEntityContext( ) , ejbUnsetEntityContext( ) , or any business methods in the CMP class because these do not deal with the persistence directly.

Example 2-10 shows a pair of CMP and BMP classes.

Example 2-10. CMP and BMP classes
 public class TheCMPBean implements EntityBean {    protected EntityContext ctx;        public abstract String getName(  );    public abstract void setName (String name);    public abstract String getAddress(  );    public abstract void setAddress (String address);        public String ejbCreate (String name, String address)    throws CreateException    {           setName (name);           setAddress(address);           return name;    }        // Skipping other EJB methods . . .         // The business methods . . .     public String getMailingAddress(  )    {           return name + "\n" + address;    } }     public class TheBMPBean extends TheCMPBean {    protected String name;    protected String address;        // Overriding accessor methods        public String getName(  ) {           return name;    }    public abstract void setName (String name) {           this.name = name;    }    public abstract String getAddress(  ) {           return address;    }    public abstract void setAddress (String address) {           this.name = name;    }        // Overriding persistence methods    public String ejbCreate (String name, String address)    throws CreateException    {           // Insert it into the database with SQL statements.           //  . . .                setName (name);           setAddress(address);           return name;    }        // Override other persistence methods:    // ejbRemove, ejbLoad, ejbStore. } 

2.2.11 Create Domain Object Factories

If you need to support different persistence media such as the filesystem or database, there are alternatives to writing different BMP implementations for each persistence type. The problem with writing different BMP implementations is that you have to replicate code that deals with EJB semantics in each different implementation. Also, switching implementations involves either changing deployment descriptors for many components, or switching JAR files.

A good solution is to separate the details of persisting data from entity bean implementations. This is especially useful when you have to deal with persisting domain objects because a natural solution is to create abstract factories for domain objects. With or without domain objects, it's easy to see that this additional implementation layer would have to allow entity beans to load, save, and find data. To illustrate this implementation model, consider a User domain object that is handled by an entity bean. In Example 2-11, the entity bean will expose fine-grained get / set methods, but will use a UserFactory to persist the User domain object.

Example 2-11. Using domain object factories
 public class UserBean implements EntityBean {    private EntityContext ctx;    private transient UserFactory userFactory;    private UserDomainObject user;        public void setEntityContext (EntityContext ctx) {           this.ctx = ctx;               // Get the factory object for:            userFactory = UserFactory.getInstance(  );    }        public void unsetEntityContext(  ) {           ctx = null;           userFactory = null;    }        public void ejbActivate(  ) {           userFactory = UserFactory.getInstance(  );    }        public void ejbPassivate(  ) {    }        public Integer ejbCreate (String name, String password)    throws CreateException {           // Get the factory to create the user.           try {                  user = userFactory.createUser (name, password);                  return user.getId(  );               } catch (PersistenceException e) {                  throw new EJBException (e);           }    }        public Integer ejbFindByPrimaryKey (Integer id)    throws FinderException {           // Use the factory to find the user.           if (userFactory.existsUser(id)) return id;               throw FinderException ("User " + id + " not found");    }        public void ejbLoad(  ) {           try {                  user = userFactory.loadUser (user.getId(  ));               } catch (PersistenceException e) {                  throw new EJBException (e);           }    }        public void ejbStore(  ) {           try {                  userFactory.saveUser (user);           } catch (PersistenceException e) {                  throw new EJBException (e);           }    }        // Exposed business methods    public String getName(  ) {           return user.getName(  );    }        public void setName (String name) {           user.setName (name);    } }     public interface UserFactory {    public static synchronized UserFactory getInstance(  ) {           // In this example, you'll hardcode a particular factory.           return new DBUserFactory(  );    }        public User create (String name, String password) throws PersistenceException;    public boolean existsUser (int id);    public User loadUser (int id) throws PersistenceException;    public void saveUser (User user) throws PersistenceException; } 

I'll leave the implementation of the DBUserFactory to you because the factory contains standard JDBC code familiar to all EJB developers. The important detail to notice is the getInstance( ) method, which returns the appropriate factory. Instead of hardcoding the concrete factory, you can implement a flexible lookup. Some simple lookup methods include reading the factory class from JNDI, or using the ClassLoader to find the concrete class.

I l @ ve RuBoard


The OReilly Java Authors - JavaT Enterprise Best Practices
The OReilly Java Authors - JavaT Enterprise Best Practices
ISBN: N/A
EAN: N/A
Year: 2002
Pages: 96

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net