It's now time to turn our attention back to the client side of our session façade. In this section, we'll first cover how to satisfy the requirements of our service interface with our session-bean implementation. We'll then look at how to better manage the JNDI lookups and home and remote interface management inherent in being a remote client to an EJB.
As you saw when we defined the business interface for the Storefront session bean, we still have some work to do to match it up to the Storefront service interface. Our business interface doesn't include all the methods of IStorefrontService, and the methods that are declared include RemoteException in their throws clauses. We'll address these differences by going back to the Business Delegate pattern introduced in Chapter 6. Recall that the purpose of this pattern is to hide the business-service implementation from the client application.
We'll start out with a fairly straightforward Business Delegate implementation and then cover some specific ways to improve it. An initial implementation is shown in Example 13-7.
package com.oreilly.struts.storefront.service;
import java.rmi.RemoteException;
import java.util.Hashtable;
import java.util.List;
import javax.ejb.CreateException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.rmi.PortableRemoteObject;
import com.oreilly.struts.storefront.catalog.view.ItemDetailView;
import com.oreilly.struts.storefront.customer.view.UserView;
import com.oreilly.struts.storefront.framework.exceptions.*;
/**
* This class is a business delegate that supports the implementation of the
* IStorefrontService interface using the Storefront session bean.
*/
public class StorefrontEJBDelegate implements IStorefrontService {
private IStorefront storefront;
public StorefrontEJBDelegate( ) {
init( );
}
private void init( ) {
try {
Hashtable props = new Hashtable( );
props.put(Context.INITIAL_CONTEXT_FACTORY,
"org.jnp.interfaces.NamingContextFactory");
props.put(Context.PROVIDER_URL, "localhost");
InitialContext ic = new InitialContext(props);
Object home = ic.lookup("com.oreilly.struts.storefront.service.Storefront");
StorefrontHome sfHome = (StorefrontHome)
PortableRemoteObject.narrow(home, StorefrontHome.class);
storefront = sfHome.create( );
}
catch (NamingException e) {
throw new RuntimeException(e.getMessage( ));
}
catch (CreateException e) {
throw new RuntimeException(e.getMessage( ));
}
catch (RemoteException e) {
throw new RuntimeException(e.getMessage( ));
}
}
public UserView authenticate( String email, String password )
throws InvalidLoginException, ExpiredPasswordException,
AccountLockedException, DatastoreException {
try {
return storefront.authenticate(email, password);
}
catch (RemoteException e) {
throw DatastoreException.datastoreError(e);
}
}
public List getFeaturedItems( ) throws DatastoreException {
try {
return storefront.getFeaturedItems( );
}
catch (RemoteException e) {
throw DatastoreException.datastoreError(e);
}
}
public ItemDetailView getItemDetailView( String itemId )
throws DatastoreException {
try {
return storefront.getItemDetailView(itemId);
}
catch (RemoteException e) {
throw DatastoreException.datastoreError(e);
}
}
public void logout( String email ) {
// Do nothing for this example
}
public void destroy( ) {
// Do nothing for this example
}
}
When an instance of the StorefrontEJBDelegate class is created, its init( ) method is called to obtain a remote reference to the Storefront session bean. This method performs the required JNDI lookup using the naming service implementation provided by JBoss. As written, the delegate assumes that the naming service is running on the local machine. Later, we'll look at how to externalize the details of the JNDI lookup that must be performed by a delegate. Once a remote reference is obtained, the delegate holds it as part of its state. This field is declared to be of the business interface type because we need it only for accessing business methods. Even though the storefront field isn't declared to be of the session bean's remote interface type, the required handling of RemoteException makes it clear that our delegate is accessing a remote object.
Other than what is required to obtain a remote reference, most of the code in our delegate does nothing more than relay business method calls to the session-bean implementation. The logout( ) and destroy( ) methods have no counterparts in the application tier, so those implementations don't include session-bean calls. If we needed to do something in these methods, that code could either be implemented directly in the StorefrontEJBDelegate methods or in another web-tier component that could be called by the delegate.
The exception handling found in this implementation of the StorefrontEJBDelegate class is worth noting. In addition to hiding the details of JNDI lookups, a business delegate used with a session bean should also hide the EJB-specific exceptions that come with being a remote client. In the business methods of the delegate, any RemoteException that gets thrown from a session-bean call is caught and reported to the client using a DatastoreException. Hiding the remote nature of the model implementation addresses the mismatch in declared exceptions between our business interface and the IStorefrontService declarations.
The only reason our delegate uses a DatastoreException to respond to a RemoteException is to leave the service interface unaffected by the implementation approach. If this self-imposed constraint were relaxed so that changes to IStorefrontService were acceptable, a better approach would be to declare an exception class whose sole purpose is to report exceptions from a delegate in a generic fashion. For example, if we were to declare an application exception named ServiceDelegateException, we could throw that when a RemoteException occurred. Instead of throwing a RuntimeException to report a failure in obtaining a remote reference, the init( ) method could also be updated to make use of ServiceDelegateException. This new exception would be a more accurate indication of the type of error that occurred than using a DatastoreException. Furthermore, adding this new exception to our IStorefrontService declarations still wouldn't expose the fact that the implementation is based on EJB.
All that's left to do is to swap the current Storefront service implementation with the delegate we have created. The framework put into place with the StorefrontServiceFactory in Chapter 6 makes this easy to do. We simply need to change the class specified for our service implementation in the web.xml file to the following:
<init-param>
<param-name>storefront-service-class</param-name>
<param-value>
com.oreilly.struts.storefront.service.StorefrontEJBDelegate
</param-value>
</init-param>
With this change made, an action will be creating a delegate instance whenever it calls the getStorefrontService( ) method implemented in the StorefrontBaseAction. This method should be called only once during a request, to avoid the unnecessary overhead of creating additional remote references. However, even taking care to use the same delegate throughout a request leaves us with an implementation that isn't very efficient. The next section covers some ways to improve our use of JNDI and home interfaces.
|
Implementing a business delegate clearly isolates and minimizes the dependencies between the web and application tiers. We were able to implement our Storefront session bean using a business interface that isn't tied to any particular client type. We also were able to leave our Struts action classes untouched when switching to this implementation of our model. We do have a couple of problems to address, though, to turn this into a solution you would want to use in a real application. Most importantly, we need to improve how we're obtaining our home interface references. We also should get rid of the hardcoded parameters used by our JNDI lookup.
Performing a JNDI lookup to obtain a home interface reference is an expensive (slow) operation. We couldn't do much about this overhead if we actually needed a new home reference for each request, but that's not the case. An EJB home is a factory object that is valid throughout the lifetime of the client application. There is no state in this object that prevents it from being used across requests or client threads. Our delegate would be significantly improved if the home reference it needed were cached within the web tier after being requested the first time.
As with any design problem, there is more than one technique we should consider for caching our home reference. We're basically talking about application-scope data in the web tier, so modifying the delegate to store the reference in the ServletContext after doing the required JNDI lookup is a potential solution. This would prevent any additional lookups, but it would require us to make the ServletContext available to our delegate through its constructor. This one change would ripple out to our service factory, because it currently instantiates an IStorefrontService implementation using its no-argument constructor. It would be preferable to choose a solution without such a strong tie to HTTP constructs. A more flexible approach is to apply the EJBHomeFactory pattern as a way to cache the references we need.
The EJBHomeFactory pattern is defined in EJBDesignPatterns by Floyd Marinescu (Wiley & Sons). Implementing this pattern allows you to create and cache any EJB home reference needed by your application. Because it's not dependent on the ServletContext, you can reuse this technique in non-web applications. Example 13-8 shows the implementation of this pattern that we'll use for the Storefront application.
package com.oreilly.struts.storefront.framework.ejb;
import java.io.InputStream;
import java.io.IOException;
import java.util.*;
import javax.ejb.*;
import javax.naming.*;
import javax.rmi.PortableRemoteObject;
/**
* This class implements the EJBHomeFactory pattern. It performs JNDI
* lookups to locate EJB homes and caches the results for subsequent calls.
*/
public class EJBHomeFactory {
private Map homes;
private static EJBHomeFactory singleton;
private Context ctx;
private EJBHomeFactory( ) throws NamingException {
homes = Collections.synchronizedMap(new HashMap( ));
try {
// Load the properties file from the classpath root
InputStream inputStream = getClass( ).getResourceAsStream(
"/jndi.properties" );
if ( inputStream != null) {
Properties jndiParams = new Properties( );
jndiParams.load( inputStream );
Hashtable props = new Hashtable( );
props.put(Context.INITIAL_CONTEXT_FACTORY,
jndiParams.get(Context.INITIAL_CONTEXT_FACTORY));
props.put(Context.PROVIDER_URL, jndiParams.get(Context.PROVIDER_URL));
ctx = new InitialContext(props);
}
else {
// Use default provider
ctx = new InitialContext( );
}
} catch( IOException ex ){
// Use default provider
ctx = new InitialContext( );
}
}
/**
* Get the Singleton instance of the class.
*/
public static EJBHomeFactory getInstance( ) throws NamingException {
if (singleton == null) {
singleton = new EJBHomeFactory( );
}
return singleton;
}
/**
* Specify the JNDI name and class for the desired home interface.
*/
public EJBHome lookupHome(String jndiName, Class homeClass)
throws NamingException {
EJBHome home = (EJBHome)homes.get(homeClass);
if (home == null) {
home = (EJBHome)PortableRemoteObject.narrow(ctx.lookup(
jndiName), homeClass);
// Cache the home for repeated use
homes.put(homeClass, home);
}
return home;
}
}
|
Notice that our EJBHomeFactory class accepts the JNDI name and class for the home interface it is requested to locate. If we needed to access more than one session bean in the application tier, we would simply implement a delegate class for each session bean and use our factory to locate the corresponding home interface. Besides the performance improvements the caching of homes gives us, all the ugly narrowing and exception handling that goes along with looking up these references is kept in one place.
The EJBHomeFactory constructor also takes care of externalizing the provider and factory parameters we need to access the naming service. A standard approach for doing this is to use a jndi.properties file that includes entries like the following:
java.naming.factory.initial=org.jnp.interfaces.NamingContextFactory
java.naming.provider.url=localhost
If you call the no-argument constructor for the InitialContext class, the classpath is searched for a jndi.properties file. If this file is found, its entries are used to initialize the naming context. In our example, the factory class explicitly loads this file from the classpath. Otherwise, classloader priorities within the web server could prevent these settings from being picked up before the default values defined for the server.
|
Our business delegate can be simplified now that we have a standard approach for locating the home interface. We can change the implementation of the init( ) method to the following:
private void init( ) {
try {
StorefrontHome sfHome = (StorefrontHome)EJBHomeFactory.getInstance( ).
lookupHome("com.oreilly.struts.storefront.service.Storefront",
StorefrontHome.class);
storefront = sfHome.create( );
}
catch (NamingException e) {
throw new RuntimeException(e.getMessage( ));
}
catch (CreateException e) {
throw new RuntimeException(e.getMessage( ));
}
catch (RemoteException e) {
throw new RuntimeException(e.getMessage( ));
}
}
In our implementation, a remote reference to the session bean is created for each request. This isn't a problem from a performance standpoint, because the overhead attached to creating a remote reference pales in comparison with that associated with the home. However, this doesn't mean that you can't cache remote references if you want. In fact, if you're interfacing with a stateful session bean, you can't keep creating new remote references across requests, because you won't be calling the same bean instance each time.
You can avoid creating a new remote reference for each request by caching a business delegate instance in the session. Unlike with homes, you can't cache a remote reference as application-scope data. Even for stateless session beans, a remote reference holds information tied to the client thread that created it, so you can't share it across user sessions. If you cache the business delegate, there are some important changes to make—the user session could be serialized and restored by the web container, and a remote reference isn't required to be serializable. For a stateless session bean, you need to be able to create a new remote reference in the event of an error or a restart of the EJB container being accessed. For a stateful session bean, you need to hold an EJB handle in your delegate instead of a remote reference, so that you can always maintain a way to access the same bean.
|
In this section, we'll look at one last example of something you might want to implement within your business delegate. Our current implementation works well for the Storefront application, but delegates tend to become cluttered with redundant-looking methods if they have to support an interface with more than a few methods. We declared only three methods for our overly simple Storefront model, but even they follow a somewhat monotonous pattern. With the exception of the logout( ) and destroy( ) methods, each business method in the delegate is implemented by calling the method with the same name on the session bean and catching a RemoteException to replace it with a DatastoreException. A dynamic proxy offers a way to get rid of this redundancy.
If you ever find yourself performing the same additional steps as part of delegating a set of method calls to another object, you should consider introducing a dynamic proxy. This concept is a little difficult to grasp if you've never worked with one before, but its use can do away with a lot of repetitive code. Basically, a dynamic proxy is an object created at runtime using reflection that implements one or more interfaces you specify. The implementation of the interface methods consists of calling the invoke( ) method of an object you also specify. This invoke( ) method is declared by the java.lang.reflect.InvocationHandler interface, which must be explicitly implemented by the object used to construct the proxy. The proxy passes parameters to the invoke( ) method that identify the interface method that was called and the arguments that were passed to it. This idea is always easiest to explain by example, so Example 13-9 shows a replacement for our business delegate that can be used with a dynamic proxy.
package com.oreilly.struts.storefront.service;
import java.lang.reflect.*;
import java.rmi.RemoteException;
import java.util.*;
import javax.ejb.CreateException;
import javax.naming.*;
import javax.rmi.PortableRemoteObject;
import com.oreilly.struts.storefront.catalog.view.ItemDetailView;
import com.oreilly.struts.storefront.customer.view.UserView;
import com.oreilly.struts.storefront.framework.ejb.EJBHomeFactory;
import com.oreilly.struts.storefront.framework.exceptions.*;
/**
* This class is a dynamic proxy implementation of the IStorefrontService
* interface. It implements two of the IStorefrontService methods itself and
* delegates the others to the methods declared by the IStorefront business
* interface with the same name.
*/
public class DynamicStorefrontEJBDelegate implements InvocationHandler {
private IStorefront storefront;
private Map storefrontMethodMap;
public DynamicStorefrontEJBDelegate( ) {
init( );
}
private void init( ) {
try {
// Get the remote reference to the session bean
StorefrontHome sfHome = (StorefrontHome)EJBHomeFactory.getInstance( ).
lookupHome("com.oreilly.struts.storefront.service.Storefront",
StorefrontHome.class);
storefront = sfHome.create( );
// Store the business interface methods for later lookups
storefrontMethodMap = new HashMap( );
Method[] storefrontMethods = IStorefront.class.getMethods( );
for (int i=0; i<storefrontMethods.length; i++) {
storefrontMethodMap.put(storefrontMethods[i].getName( ),
storefrontMethods[i]);
}
}
catch (NamingException e) {
throw new RuntimeException(e.getMessage( ));
}
catch (CreateException e) {
throw new RuntimeException(e.getMessage( ));
}
catch (RemoteException e) {
throw new RuntimeException(e.getMessage( ));
}
}
public void logout(String email) {
// Do nothing for this example
}
public void destroy( ) {
// Do nothing for this example
}
public Object invoke(Object proxy, Method method, Object[] args )
throws Throwable{
try {
// Check for the two methods implemented by this class
if (method.getName( ).equals("logout")) {
logout((String)args[0]);
return null;
}
else if (method.getName( ).equals("destroy")) {
destroy( );
return null;
}
else {
// This method should match a method implemented by the
// session bean that has the same name and argument list
Method storefrontMethod = (Method)storefrontMethodMap.get(
method.getName( ));
if (storefrontMethod != null) {
// Call the method on the remote interface
return storefrontMethod.invoke( storefront, args );
}
else {
throw new NoSuchMethodException("The Storefront does not implement "
+ method.getName( ));
}
}
} catch( InvocationTargetException ex ) {
if (ex.getTargetException( ) instanceof RemoteException) {
// RemoteException isn't declared by the IStorefront method that was
// called, so we have to catch it and throw something that is
throw DatastoreException.datastoreError(ex.getTargetException( ));
}
else {
throw ex.getTargetException( );
}
}
}
}
The intent behind DynamicStorefrontEJBDelegate is for a dynamic proxy that is created as an implementation of the IStorefrontService interface to delegate all those calls to the invoke( ) method declared here. Notice that this delegate class is not declared to implement IStorefrontService. In fact, the only business methods from IStorefrontService that appear in this class are the logout( ) and destroy( ) methods that aren't implemented by our session bean.
To use the dynamic proxy-based delegate, we need to modify our approach for obtaining an implementation of the service interface from the factory. Rather than devising something elegant for a small part of this example, we'll just hardcode the new approach we need. This is shown in the following version of the createService( ) method of StorefrontServiceFactory:
public IStorefrontService createService( ){
Class[] serviceInterface = new Class[] { IStorefrontService.class };
IStorefrontService proxy = (IStorefrontService)Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader( ), serviceInterface,
new DynamicStorefrontEJBDelegate( ) );
return proxy;
}
When an action class asks for an implementation of the service interface, the factory now creates a dynamic proxy that implements this interface using an instance of DynamicStorefrontEJBDelegate. When the action makes a call on the service interface, the call goes to the proxy and is transformed into a call on the delegate's invoke( ) method. The invoke( ) method checks the name of the method that was called and either calls the logout( ) or destroy( ) method implemented in the class or delegates it to the session-bean method with the same name. This sequence of calls is illustrated in Figure 13-1. The trapping and replacement of RemoteException when our business delegate calls a session-bean method are now handled in a single place. This single invoke( ) method can handle all of the methods exposed by the session-bean business interface without modification.
A dynamic proxy is an appealing technique for minimizing method clutter in a business delegate, but there's a price to pay. When considering an approach such as this that relies heavily on reflection, you need to weigh the slower runtime performance that will result against the improved maintainability of your code.