Programming Jakarta Struts, 2nd Edition [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Programming Jakarta Struts, 2nd Edition [Electronic resources] - نسخه متنی

Chuck Cavaness

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید








13.2 Interfacing Struts to EJB


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.


13.2.1 Using a Business Delegate


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.


Example 13-7. A business delegate for the Storefront session bean

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.


13.2.1.1 Exception handling

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.


If the inclusion of RemoteException
in our business interface had been the only difference between this
and the service interface, it might have been tempting to simply add
this exception to IStorefrontService and continue
forward. However, this would have unnecessarily cluttered the
contract for whatever service implementation might be used with
implementation details.

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.


13.2.1.2 Swapping the implementation

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.


Don't forget that you'll need to
copy the JBoss client JARs to the lib directory
for your web application before using your delegate.
You'll also need the home and remote interface class
files for the Storefront session bean in the
classes directory.


13.2.2 Managing EJB Home and Remote References


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.


13.2.2.1 Implementing an EJBHomeFactory

The EJBHomeFactory pattern is defined in
EJB Design Patterns 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.


Example 13-8. An EJBHomeFactory implementation

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;
}
}


The
getInstance method of
EJBHomeFactory differs from most Singleton
implementations in that it isn't declared to be
synchronized. As discussed in EJB Design
Patterns
, using a synchronized method here would degrade
performance without providing any significant benefit in return. If
multiple instances of EJBHomeFactory are
instantiated due to simultaneous calls to
getInstance during initialization, some redundant
home references will likely be created, but no harm will be done.

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.


The JNDIConnectorPlugin example in Chapter 9 demonstrated how the naming service
parameters could be read from the web.xml file
and stored in the ServletContext.
We're not using that approach here because we want
to keep our factory independent of the web tier.


13.2.2.2 Using an EJBHomeFactory in a business delegate

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( ));
}
}


13.2.2.3 What about the remote references?

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 makethe 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 addition to the coverage in EJB Design
Patterns
, you can find more information on the patterns
discussed in this chapter in Core J2EE Patterns
by Deepak Alur, John Crupi, and Dan Malks (Prentice Hall).


13.2.3 Using Dynamic Proxies


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.


Example 13-9. Dynamic proxy implementation of the Storefront service

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.


Figure 13-1. Sequence diagram for retrieving item detail through a dynamic proxy

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.


    / 181