13.1 Implementing the Storefront Service Using EJB

Even though this chapter is specific to EJB, the intent is still to keep the focus on Struts. With that in mind, the discussion of EJB implementation details will be kept to a minimum. EJB is a complex topic, but the nature of several design patterns geared toward the interaction between EJBs and their clients makes this an easier task than you might first think. After all, an overriding goal of this chapter is to demonstrate how to design an application so that your Struts classes aren't impacted by the choice to use an EJB implementation of the model. You already have a head start on some of the central issues here, having seen how the model component of a web application can be hidden behind a service interface. In particular, you've seen through the Storefront example how easy it is to swap a debug model with a full implementation that accesses a database when this design approach is followed.

Throughout this chapter, the Storefront example will be used to illustrate how an EJB application tier can be used with a Struts application. If it weren't for the remote nature of accessing an EJB from a client application, this implementation choice wouldn't make any difference to you. However, the distributed aspects of EJB must be taken into account when your web-tier classes access this type of model. What you'll see in the remainder of this chapter are some recommendations on how best to implement the code needed to interface with the application tier. Key to this discussion is an approach for isolating the code that handles what's unique about EJB so that your action classes aren't affected.

13.1.1 A Quick EJB Overview

The EJB specification defines three types of beans: entity, session, and message-driven. Each type of bean has a different purpose within an EJB application.

Entity beans provide transactional access to persistent data and are most often used to represent rows stored in one or more related tables in a database. For example, you might implement CustomerBean, ItemBean, and OrderBean entity classes, among others, to support the Storefront application. These entity beans would incorporate the functionality provided by the corresponding business object classes in the example application. When entity beans are used, they take on the primary role of supplying the application model. They are expected to provide both the required data persistence operations and the business logic that governs the data they represent. Because the model should be reusable across applications in an enterprise, entity beans need to be independent of the types of clients that ultimately access the application tier.

Session beans are often described as extensions of the client applications they serve. They can implement business logic themselves, but more often their role is to coordinate the work of other classes (entity beans, in particular). They are more closely tied to their clients, so the precaution of keeping entity beans reusable doesn't apply to session beans to the same extent. The application tier is primarily considered to be the application model, but session beans are also referred to as "controllers" because of the coordination they do. This is especially true when session-bean methods are used to implement transactions that touch multiple business objects.

Session beans can be implemented as either stateless or stateful. A stateless session bean maintains no state specific to its client, so a single instance can efficiently be shared among many clients by the EJB container. A stateful bean instance, on the other hand, is assigned to a specific client so that state can be maintained across multiple calls. Holding state in the application tier can simplify the client application logic, but it makes it more difficult to scale to a large number of clients. Typical web applications maintain their client state in the user session, and possibly the database, instead of making use of stateful session beans. For this reason, we'll focus on stateless beans for our examples here.

The EJB 2.0 specification added message-driven beans as the third bean type so that EJB could be integrated with the Java Message Service (JMS). Message-driven beans differ from the other two types in that they respond asynchronously to requests instead of being called directly by a client application. The container invokes a message-driven bean whenever a JMS message that meets the selection criteria of the bean is received. For example, in a more complex version of the Storefront application, a message-driven bean could be used to respond to a notification that an item is being backordered. The bean could be responsible for emailing any customers that had orders already in place for the item. Message-driven beans have no direct interaction with client applications, so they are even less dependent on the client than entity beans are.

13.1.2 The Session Façade

The first step in designing an interface to the application tier is to identify the entry points exposed to a client application. Message-driven beans aren't called directly, so they don't come into play here. However, a typical EJB application can include a number of session and entity beans. As already pointed out, standard practice is to insulate entity beans from the details of the client so that they can be reused in other applications. If your Struts actions were to interact with entity beans directly, the web tier would quickly become coupled to the object model implemented by the application tier. This tight coupling, combined with the distributed nature of EJB, would lead to a number of issues:

·         Changes in the EJB object model would require corresponding changes in the web tier.

·         Action classes often would be required to execute multiple remote calls to the application server to satisfy the business logic needs of a request.

·         The business logic and transaction-management code needed to orchestrate multiple calls would have to exist in the web tier.

To avoid these issues, the interface exposed to the clients of the application tier is almost always limited to session beans. This approach is commonly referred to as either "session wraps entity" or a " session façade." There are quite a few advantages to this design. When a client application makes a single call to a session-bean method to perform a required operation, the session bean can easily execute the request as a single transaction, and the implementation details can be hidden. This session-bean method might need to access a number of entity beans, or even other session beans, to satisfy the request. No matter what the flow of control is on the application server, the complexity is hidden from the client. Because session beans become the only clients of the entity beans when using a session façade, there's little chance of the entities becoming tied to any particular type of external client.

Even though the discussion here assumes that the business objects are implemented as entity beans, this doesn't have to be the case in an EJB application. The same concerns and advantages that support using a session façade apply when other implementations are used. Just as some Java developers don't like EJB, not all EJB developers like entity beans. Because the session façade hides the object model from the client, entity beans can be replaced with another approach, such as Java data objects (JDOs) or regular data access objects (DAOs), without impacting the interface exposed to the client.

13.1.3 The Business Interface

When using a session façade, you must first define the details of the interface between the web and application tiers. You might have some questions about which side should be the primary driver of this contract between the two tiers. Early in the development of the Storefront example, the IStoreFrontService interface was introduced to define the application-tier functionality required to support the presentation needs of the application. In particular, the presentation layer relies on the supporting service to authenticate users and to provide product descriptions needed to build the online catalog. Taking a user-centered view of the application, it's easy to see how the service-layer requirements can be driven by the web tier. After all, if a certain view is to be built, the model functionality to support it has to exist. However, one of the main reasons to implement an enterprise's business logic within EJBs is to allow those business rules and the supporting data to be used to across multiple applications. This includes providing support for clients other than those associated with web applications. This isn't a problem, because the division of responsibility between session and entity beans helps to protect the reusability of business logic.

Because session beans are treated as extensions of the clients they serve, there's nothing wrong with defining a session façade that's specific to a particular client application. As long as your entity and message-driven beans remain independent of the client, it's reasonable to implement a session-bean interface in response to the requirements set forth by a specific client. If multiple client views of the model are required, multiple façades can be implemented to support them.

The façade presented by a session bean can support either local or remote clients. Local clients of a session or entity bean can only be other EJBs deployed within the same container. These clients are tightly coupled to the beans they access, but they offer performance advantages when calls need to be made between EJBs. This is because these method calls use pass-by-reference semantics instead of being treated as remote calls between distributed components. Session beans are often local clients of entity beans, but it's less common for them to have local clients of their own to support. Our web-tier components obviously aren't EJBs running within the application tier, so we care only about remote clients for our purposes here. What we need to do, then, is define the remote interface for our session façade.

Every session bean that supports remote clients must have a corresponding remote interface that extends the javax.ejb.EJBObject interface. This interface determines the business methods that are exposed by the session bean. It might seem strange, but you'll almost never see a method explicitly declared in a remote interface. This is because of an EJB design pattern known as the business interface.

Besides its remote interface, a session bean supporting remote clients must have a home interface that extends javax.ejb.EJBHome, an implementation class, and one or more XML deployment descriptors. Unlike the remote interface, which declares the bean's business methods, the home interface defines factory-like methods for creating session-bean references. By definition, the bean's methods are implemented by the implementation class. The deployment descriptors identify and configure a bean for use within a particular EJB container. We'll define each piece for our example as we go along.

When you think of a class and an interface with which it's associated, you would normally expect that the class would explicitly implement that interface. This isn't true with remote (or local) interfaces in EJB. Instead, the container creates an intermediate object (often referred to as an EJBObject) to implement the remote interface. This object intercepts calls made by clients of the bean, then delegates them to the implementation class after performing any operations (such as security or transaction management) that might be required. Instead of the Java compiler verifying that the bean class implements each of the business methods declared by the remote interface, that responsibility falls to the deployment tools provided by the container. Even a bean class that compiles without any errors will fail at deployment if there's a mismatch between it and its remote interface.

If you declared a session bean to implement its remote interface, you'd be guaranteed that the compiler would catch any problems with its business-method declarations. The problem is that you'd also have to provide dummy implementations of the non-business methods declared by javax.ejb.EJBObject. These methods would never be called (they're called only on the intermediate object created by the container), but they would have to be there to satisfy the compiler. Instead of taking this approach, most EJB developers create a second interface, known as the business interface, that declares the business methods that need to be exposed. Declaring the remote interface to extend this interface and the bean class to implement it exposes the required methods, so the compiler can verify that the bean implements them. This pattern provides a convenient starting point for defining our client access mechanism.

The use of a business interface also prevents programmers from accidentally passing or returning a this reference to an instance of a bean class that has been declared to implement its remote interface. This topic is beyond the scope of this book, but the short explanation is that the EJB container can manage bean instances properly only when they're referred to using only their remote (or local) interfaces. A bean reference can't be returned in place of its remote interface if the bean class implements only its business interface.

Returning to the IStorefrontService interface that eventually must be satisfied by our implementation, recall that it contains methods related to both user authentication and the product catalog. Even when using a session façade, you would likely separate these responsibilities into separate session beans. This would reduce the complexity of the session beans involved and simplify their maintenance. However, given that EJB design isn't our focus here, our first simplification will be to assume that our façade will consist of a single Storefront session bean. You probably wouldn't do this in a real application, but once you know how to interface with a single session bean, applying the same technique to multiple session beans is straightforward. A suitable business interface for the Storefront session bean is shown in Example 13-1.

Example 13-1. The business interface for the Storefront session bean
package com.oreilly.struts.storefront.service;
 
import java.rmi.RemoteException;
import java.util.List;
import com.oreilly.struts.storefront.catalog.view.ItemDetailView;
import com.oreilly.struts.storefront.customer.view.UserView;
import com.oreilly.struts.storefront.framework.exceptions.*;
 
/**
 * The business interface for the Storefront session bean
 */
public interface IStorefront {
 
  public UserView authenticate( String email, String password )
    throws InvalidLoginException, ExpiredPasswordException,
      AccountLockedException, DatastoreException, RemoteException;
 
  public List getFeaturedItems(  ) throws DatastoreException, RemoteException;
 
  public ItemDetailView getItemDetailView( String itemId )
    throws DatastoreException, RemoteException;
}

The first thing to notice about the IStorefront interface is that its methods don't exactly match those declared by IStorefrontService. First of all, our business interface doesn't include the logout( ) and destroy( ) methods found in the service interface. The reason for this is that those methods represent web-tier functionality, not true business logic that needs to move to the application tier. Also, every method in IStorefront is declared to throw RemoteException, which is not part of the declarations in IStorefrontService. All business methods exposed to a remote client of an EJB must be declared to throw RemoteException, which is used to report communication failures specific to remote method execution. This is the one aspect of a remote interface that can't be hidden by the business interface. Without this restriction, this interface could be made to look very much like our service interface. Once we cover how our example session bean will be implemented, we'll discuss how these mismatches between the interfaces can be handled.

It's also important to notice that our business interface is referencing the view classes already created to support the service interface. The same Data Transfer Object (DTO) pattern introduced in Chapter 7 applies to an EJB-based model. Instead of exposing the actual business object implementation classes or many fine-grained methods to access their properties, you can use simple JavaBean classes to communicate the state of the model to the client. We declared our BaseView superclass to be Serializable so that the view classes could be referenced in a remote interface. DTO classes tend to consist of simple data types, so this constraint should be a minor one.

With our business interface defined, Example 13-2 shows the trivial remote interface declaration we'll need to eventually deploy our session bean.

Example 13-2. The remote interface for the Storefront session bean
package com.oreilly.struts.storefront.service;
 
import javax.ejb.EJBObject;
 
public interface Storefront extends EJBObject, IStorefront {
 /**
  * The remote interface for the Storefront session bean. All methods are
  * declared in the IStorefront business interface.
  */
} 

13.1.4 Stateless Session Bean Implementation

Without getting into anything too elaborate, we next want to come up with an implementation of our session façade. We'll make a few decisions here to simplify things, but the result will be all we need to illustrate how to interface the web and application tiers. It will also be good enough for you to deploy in an EJB container and use to test your Struts interface.

If you were given the task of implementing the application tier of the Storefront application using EJB, you probably would produce a design consisting of both session and entity beans. You could represent the model components using entity beans, and you'd likely have a number of session beans to provide the functionality for security, catalog, and order operations. The session beans would provide the workflow functionality required from process business objects, and the entity beans would serve as the corresponding entity business objects.

We've already made the decision to use only a single session bean for the example. The session façade makes our next simplification easy as well. Because we've isolated the interface between our two tiers into a façade, any division of responsibilities between session and entity beans is of no concern to us as Struts developers. The web tier sees only session-bean methods and DTO classes, so nothing else about the implementation will affect the web components. Given that, we'll implement our façade using a single stateless session bean that does not require any other EJBs.

If you're starting with an EJB implementation that includes entity beans, you might want to use XDoclet (available from http://www.sourceforge.net/projects/xdoclet/) to automatically generate Struts ActionForms from these beans. For more complex EJB implementations than what we're looking at here, XDoclet also provides an automated means of generating the various interfaces and deployment descriptors required for a bean. This code generation is performed based on special JavaDoc tags that you include in your EJB implementation classes.

Because we're not using entity beans, we can make use of the same ORM approach and entity business object classes already used by the StorefrontServiceImpl class. In fact, our implementation will look very much like that class, with the exception of the callback methods required by the javax.ejb.SessionBean interface. This is shown in Example 13-3.

Example 13-3. The Storefront session bean
package com.oreilly.struts.storefront.service;
 
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.ejb.CreateException;
import javax.ejb.EJBException;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import org.odmg.*;
import ojb.odmg.*;
import com.oreilly.struts.storefront.businessobjects.CustomerBO;
import com.oreilly.struts.storefront.businessobjects.ItemBO;
import com.oreilly.struts.storefront.catalog.view.ItemDetailView;
import com.oreilly.struts.storefront.catalog.view.ItemSummaryView;
import com.oreilly.struts.storefront.customer.view.UserView;
import com.oreilly.struts.storefront.framework.exceptions.AccountLockedException;
import com.oreilly.struts.storefront.framework.exceptions.DatastoreException;
import com.oreilly.struts.storefront.framework.exceptions.ExpiredPasswordException;
import com.oreilly.struts.storefront.framework.exceptions.InvalidLoginException;
 
/**
 * This is a simple Session Bean implementation of the Storefront service
 */
public class StorefrontBean implements SessionBean, IStorefront {
  private SessionContext ctx;
  private Implementation odmg = null;
  private Database db = null;
 
  public UserView authenticate( String email, String password )
   throws InvalidLoginException, ExpiredPasswordException,
     AccountLockedException, DatastoreException {
 
      // Query the database for a user that matches the credentials
      List results = null;
      try{
        OQLQuery query = odmg.newOQLQuery(  );
        // Set the OQL select statement
        String queryStr = "select customer from " + CustomerBO.class.getName(  );
        queryStr += " where email = $1 and password = $2";
        query.create(queryStr);
 
        // Bind the input parameters
        query.bind( email );
        query.bind( password );
 
        // Retrieve the results
        results = (List)query.execute(  );
      }catch( Exception ex ){
        ex.printStackTrace(  );
        throw DatastoreException.datastoreError(ex);
      }
 
      // If no results were found, must be an invalid login attempt
      if ( results.isEmpty(  ) ){
        throw new InvalidLoginException(  );
      }
 
      // Should only be a single customer that matches the parameters
      CustomerBO customer  = (CustomerBO)results.get(0);
 
      // Make sure the account is not locked
      String accountStatusCode = customer.getAccountStatus(  );
      if ( accountStatusCode != null && accountStatusCode.equals( "L" ) ){
        throw new AccountLockedException(  );
      }
 
      // Populate the value object from the Customer business object
      UserView userView = new UserView(  );
      userView.setId( customer.getId().toString(  ) );
      userView.setFirstName( customer.getFirstName(  ) );
      userView.setLastName( customer.getLastName(  ) );
      userView.setEmailAddress( customer.getEmail(  ) );
      userView.setCreditStatus( customer.getCreditStatus(  ) );
 
      return userView;
  }
 
  public List getFeaturedItems(  ) throws DatastoreException {
    List results = null;
    try{
      OQLQuery query = odmg.newOQLQuery(  );
      // Set the OQL select statement
      query.create( "select featuredItems from " + ItemBO.class.getName(  ) );
      results = (List)query.execute(  );
    }catch( Exception ex ){
      ex.printStackTrace(  );
      throw DatastoreException.datastoreError(ex);
    }
    List items = new ArrayList(  );
    Iterator iter = results.iterator(  );
    while (iter.hasNext(  )){
      ItemBO itemBO = (ItemBO)iter.next(  );
      ItemSummaryView newView = new ItemSummaryView(  );
      newView.setId( itemBO.getId().toString(  ) );
      newView.setName( itemBO.getDisplayLabel(  ) );
      newView.setUnitPrice( itemBO.getBasePrice(  ) );
      newView.setSmallImageURL( itemBO.getSmallImageURL(  ) );
      newView.setProductFeature( itemBO.getFeature1(  ) );
      items.add( newView );
    }
    return items;
  }
 
  public ItemDetailView getItemDetailView( String itemId )
   throws DatastoreException {
      List results = null;
      try{
        OQLQuery query = odmg.newOQLQuery(  );
 
        // Set the OQL select statement
        String queryStr = "select item from " + ItemBO.class.getName(  );
        queryStr += " where id = $1";
        query.create(queryStr);
        query.bind(itemId);
 
        // Execute the query
        results = (List)query.execute(  );
      }catch( Exception ex ){
        ex.printStackTrace(  );
        throw DatastoreException.datastoreError(ex);
      }
 
      //
      if (results.isEmpty(  ) ){
        throw DatastoreException.objectNotFound(  );
      }
 
      ItemBO itemBO = (ItemBO)results.get(0);
 
      // Build a ValueObject for the Item
      ItemDetailView view = new ItemDetailView(  );
      view.setId( itemBO.getId().toString(  ) );
      view.setDescription( itemBO.getDescription(  ) );
      view.setLargeImageURL( itemBO.getLargeImageURL(  ) );
      view.setName( itemBO.getDisplayLabel(  ) );
      view.setProductFeature( itemBO.getFeature1(  ) );
      view.setUnitPrice( itemBO.getBasePrice(  ) );
      view.setTimeCreated( new Timestamp(System.currentTimeMillis(  ) ));
      view.setModelNumber( itemBO.getModelNumber(  ) );
      return view;
  }
 
  /**
   * Opens the database and prepares it for transactions.
   */
  private void init(  ) throws DatastoreException {
    // Get odmg facade instance
    odmg = OJB.getInstance(  );
    db = odmg.newDatabase(  );
    // Open database
    try{
      db.open("repository.xml", Database.OPEN_READ_WRITE);
    }catch( ODMGException ex ){
      throw DatastoreException.datastoreError(ex);
    }
  }
 
  public void ejbCreate(  ) throws CreateException {
    try {
      init(  );
    }catch ( DatastoreException e ) {
      throw new CreateException(e.getMessage(  ));
    }
  }
 
  public void ejbRemove(  ) {
    try {
      if (db != null) {
        db.close(  );
      }
    }catch ( ODMGException e ) {}
  }
 
  public void setSessionContext( SessionContext assignedContext ) {
    ctx = assignedContext;
  }
 
  public void ejbActivate(  ) {
    // Nothing to do for a stateless bean
  }
 
  public void ejbPassivate(  ) {
    // Nothing to do for a stateless bean
  }
 
} 
 
 

In our StorefrontBean class, the business method implementations are unchanged from the StorefrontServiceImpl versions. Only the management of the database connection needed to be modified. Whenever the EJB container creates a new instance of this bean, the ejbCreate( ) callback method is invoked and a database connection is established. This connection is closed in the corresponding ejbRemove( ) method that is called prior to the instance being destroyed. The container never passivates stateless session beans, so do-nothing implementations are supplied for the ejbPassivate( ) and ejbActivate( ) methods of the SessionBean interface. If we needed more than one session bean in our example, we'd move these two methods into an adapter class and extend all our concrete implementation classes from it.

A more correct EJB approach would be to open the database connection using a javax.sql.DataSource connection factory obtained from a JNDI lookup. This allows the container to manage connection pooling and transaction enlistment for you automatically. Again, this doesn't affect our interface, so we can continue to use this simple implementation.

Our session bean now has a remote interface and an implementation class. That leaves the home interface, which is always simple in the case of a stateless session bean. All we need is a single create( ) method, as shown in Example 13-4.

Example 13-4. The home interface for the Storefront session bean
package com.oreilly.struts.storefront.service;
 
import java.rmi.RemoteException;
import javax.ejb.CreateException;
import javax.ejb.EJBHome;
 
/**
 * The home interface for the Storefront session bean.
 */
public interface StorefrontHome extends EJBHome {
  public Storefront create(  ) throws CreateException, RemoteException;
} 
 

13.1.5 JBoss Deployment

We need to select an EJB container and create the required XML deployment descriptors before we can deploy and use our session bean. The open source JBoss application server fits our requirements perfectly. This full-featured J2EE implementation, complete with EJB 2.0 support, is a favorite among open source developers. You can download the software for free from http://www.jboss.org.

With our minimal implementation, we don't need anything complicated as far as deployment information for our session bean. Example 13-5 shows the standard ejb-jar.xml descriptor for our bean. For the most part, this file simply identifies the home and remote interfaces and the implementation class. It also declares that all of the business methods are nontransactional (because they're read-only methods).

Example 13-5. The ejb-jar.xml deployment descriptor for the Storefront session bean
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ejb-jar PUBLIC "-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 2.0//EN" 
 "http://java.sun.com/dtd/ejb-jar_2_0.dtd">
 
<ejb-jar >
 
   <description>
      Generic deployment information for the Storefront session bean
   </description>
   <display-name>Storefront Session Bean</display-name>
 
   <enterprise-beans>
      <session >
         <ejb-name>Storefront</ejb-name>
         <home>com.oreilly.struts.storefront.service.StorefrontHome</home>
         <remote>com.oreilly.struts.storefront.service.Storefront</remote>
         <ejb-class>com.oreilly.struts.storefront.service.StorefrontBean</ejb-class>
         <session-type>Stateless</session-type>
         <transaction-type>Container</transaction-type>
      </session>
   </enterprise-beans>
 
   <assembly-descriptor >
      <container-transaction >
         <method >
            <ejb-name>Storefront</ejb-name>
            <method-name>*</method-name>
         </method>
         <trans-attribute>NotSupported</trans-attribute>
      </container-transaction>
   </assembly-descriptor>
 
</ejb-jar>

In addition to the ejb-jar.xml file, most containers require one or more vendor-specific descriptors as part of a bean's deployment information. In this case, all we need to do is associate a JNDI name with our bean. Example 13-6 shows how this is done with JBoss. The fully qualified name of the bean's remote interface was chosen as the JNDI name. It's also common to use the home interface name.

Example 13-6. The J Boss deployment descriptor for the Storefront session bean
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jboss PUBLIC "-//JBoss//DTD JBOSS//EN" 
 "http://www.jboss.org/j2ee/dtd/jboss.dtd">
 
<jboss>
 
   <enterprise-beans>
      <session>
         <ejb-name>Storefront</ejb-name>
         <jndi-name>com.oreilly.struts.storefront.service.Storefront</jndi-name>
      </session>
   </enterprise-beans>
 
</jboss>

Deployment of an EJB requires packaging it into a Java archive (JAR) file. The deployment JAR file for our session bean needs to include the following files:

·         The home and remote interface class files

·         The bean implementation class file

·         The two deployment descriptors (these files must be placed in a META-INF directory)

·         The OJB.properties file and the various repository XML files used by the ORM framework

·         The business object and DTO class files referenced by the Storefront bean

Once you've created this JAR file, you can deploy the bean by copying the file to the server/default/deploy directory underneath your JBoss installation. You can place the JAR files for your JDBC driver and the OJB classes in the server/default/lib directory. At this point, you can start JBoss and verify that you have everything in place to execute the application tier.