Mailto:gcomeau@webb.NET">Greg Comeau
DOWNLOAD: ide.com/patterns/ObserverPattern/EJBObserverPattern.zip">EJBObserverPattern.zip
I recently needed an infrastructure that would allow an arbitrary number of Enterprise JavaBeans to observe changes to a collection of central business entities. The application environment in which I am developing consists of a number of EJB applications running on more than one host. All of the applications are designed to work together as a single, integrated suite. The database environment is distributed. A central store of data is shared by the entire suite while each application maintains a separate store of data that is specific to that application.
One part of the suite is responsible for accepting transactions from the outside world. Each transaction arrives in the foRM of an XML document. A typical transaction might require changes to the central data store as well as one or more application data stores. All changes must be done within the SCOpe of a single transaction, i.e. if one application aborts the transaction then all participants must abort the transaction.
Also, there are some data constraints that cross application boundaries. It is possible for one application to abort a transaction because a change made to the central repository is unacceptable within the context of some application specific data. e.g. Application X may require that a customer has a fax number. If a transaction attempts to grant Customer A peRmission to use Application X and Customer A does not have a fax number then Application X must abort the transaction.
It was the enforcement of these distributed data constraints that motivated my use of the observer pattern. Each application needs to observe changes in the shared repository as they occur -- and within the transaction in which they occur. If a proposed change to the shared data is unacceptable to any given application then the transaction must be aborted before the change is made permanent.
What follows is essentially the process that I went through to implement this pattern. I have omitted some of the more glaring mistakes to protect the stupid guilty.
Note: All of the source files are available for download. The java classes are contained within a package called EJBObserverPattern.
The java.util.Observable class and the java.util.Observer interface work great within the scope of a single Java VM. But I discovered that they aren't much use in implementing the observer pattern across VMs with EJBs as implementations of Observer. The first step in creating an EJB observer might be to extend java.util.Observer to create a remote interface:
public interface RemoteObserver extends javax.ejb.EJBobject, java.util.Observer { }
If you did this you'd quickly find out that java.util.Observer.update(...) does not declare java.rmi.RemoteException, something required of all methods of a remote interface. At this point I found it necessary to create a new observer interface and observable class that would work in the EJB universe. I created my own observer and observable classes which parallel the respective classes in java.util.
A new interface is required that I called EJBObserver:
package EJBObserverPattern; public interface EJBObserver { /** * @param observable a reference to the object being observed * @param arg the arguments being passed to the observer */ public void update (EJBObservable observable, Object arg) throws java.rmi.RemoteException, javax.ejb.EJBException; }
Note: A common pattern in EJB implementations is to define a superinterface to be extended by both the remote interface and the bean itself. This is commonly called a business interface. EJBObserver is a business interface.
Yet Another Note: My development environment adheres to the EJB 1.0 specification. Thus, I have to explicitly declare javax.ejb.EJBException. EJB 1.1 and later redefines EJBException as a subclass of RuntimeException making it unnecessary to explicitly declare it in a method signature.
My new class EJBObservable defines all the same methods as java.util.Observable (why change a paradigm that works.) Thus, my EJBObserver update method takes an EJBObservable object; this mirrors java.util.Observer, which takes a java.util.Observable object. I marked EJBObservable as serializable by implementing java.io.Serializable. This is necessary since instances of this class will be sent to remote EJB observers:
package EJBObserverPattern; import java.rmi.RemoteException; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import javax.ejb.EJBException; /** * A class to be subclassed by any entity which may be observed * by instances of EJBObserver. * * @see java.util.Observable */ public class EJBObservable implements java.io.Serializable { /** * The collection of observers. Using a Set will guarantee * no duplicate observers. */ protected Set mObservers = new HashSet(); protected boolean mChanged = false; /* The methods for adding, deleting, and counting observers are all trivial, as are the methods for checking and setting the 'changed' indicator. Their semantics are identical to the respective methods in java.util.Observable. */ public void addObserver (EJBObserver observer) { mObservers.add (observer); } public void deleteObserver (EJBObserver observer) { mObservers.remove (observer); } public void deleteObservers () { mObservers.clear(); } public int countObservers () { return mObservers.size(); } public boolean hasChanged () { return mChanged; } public void clearChanged () { this.setChanged (false); } protected void setChanged (boolean changed) { mChanged = changed; } /** * An overloaded form of notifyObservers that takes no argument. The semantics * are identical to calling notifyObservers (..., null). * @exception RemoteException thrown by a remote implementation of EJBObserver * @exception EJBException thrown by a remote implementation of EJBObserver */ public void notifyObservers () throws RemoteException, EJBException { this.notifyObservers (null); } /** * If this object has changed as indicated by the hasChanged method, * notify all observers and call clearChanged to reset the hasChanged indicator. * @param arg any object * @exception RemoteException thrown by a remote implementation of EJBObserver * @exception EJBException thrown by a remote implementation of EJBObserver */ public void notifyObservers (Object arg) throws RemoteException, EJBException { if (this.hasChanged()) { for (Iterator i = mObservers.iterator(); i.hasNext(); ) { EJBObserver obs = (EJBObserver) i.next(); obs.update (this, arg); } this.clearChanged(); } } }
For testing purposes I created a subclass of EJBObservable called Foobar. This is the thing that will be observed. Foobar has a single member variable with a corresponding setter method. Setting the member variable also sets the changed attribute inherited from the superclass EJBObservable. The semantics of the changed attribute are identical to the respective attribute of java.util.Observable:
package EJBObserverPattern; public class Foobar extends EJBObservable { String mSomething = null; public void setSomething (String s) { mSomething = s; setChanged (true); /* We could call notifyObservers right here, but this may or may not be a good thing to do. Each call to notifyObservers will result in a remote method call for every registered observer. If there is more than one attribute to 'set' then it might be better for the caller to set them all and then call notifyObservers explicitly. */ } public String toString () { return mSomething; } }
Again for the purposes of testing, I created a test harness called Tester. This class simply creates an instance of Foobar, adds an observer, invokes the setter method of Foobar and calls notifyObservers:
package EJBObserverPattern; public class Tester { public static void main (String[] args) { try { Foobar f = new Foobar(); f.addObserver (???); f.setSomething (args[0]); f.notifyObservers (args[1]); } catch (Exception e) { e.printStackTrace(); } } }
Note: Tester will go through several revisions later in the document. My intent is to show you the abstraction process as well as the final result.
As you can see I've put the cart before the horse after the barn door closed. I do not yet have an implementation of EJBObserver. This turned out to be the interesting part.
Note that updating a remote observer via EJBObserver.update(...) will involve a remote method invocation. The observer is located at some arbitrary location in the EJB universe. The local manifestation of a remote observer will be an instance of a remote interface obtained by the usual means -- by invoking the create method of a home interface obtained via JNDI.
So now I needed to implement the usual components of an EJB, the home and remote interfaces and the bean itself. I used a stateless session bean. Note that there can be an arbitrary number of EJBObserver implementations in use at one time but only one is required to demonstrate the pattern.
First I created the remote interface, which I subclassed from the business interface EJBObserver:
package EJBObserverPattern; public interface RemoteObserver extends javax.ejb.EJBObject, EJBObserver { }
Next I created the home interface, which is almost as trivial as the remote interface. There's nothing special about it:
package EJBObserverPattern; public interface RemoteObserverHome extends javax.ejb.EJBHome { public RemoteObserver create() throws java.rmi.RemoteException, javax.ejb.CreateException; }
The astute reader may notice that I'm going to have a problem with this home interface. Rest assured that I was not astute. (What is a stute?) I didn't recognize the problem until I ran into it. I'll describe the exact problem and my solution later.
A stute is the adult form of a ware.
Now for the bean itself. Again, I implemented the superinterface EJBObserver. The implementation of the update method simply writes to the system output stream so I know that it worked:
package EJBObserverPattern; import javax.ejb.*; public class RemoteObserverBean implements EJBObserver, SessionBean { /** * @param observable a reference to the object being observed * @param arg the arguments being passed to the observer */ public void update (EJBObservable observable, Object arg) { System.out.println ("Hey! I've been updated! " + "observable = " + observable.toString() + " arg = " + arg.toString()); } public void ejbCreate () { } public void ejbActivate() { } public void ejbPassivate() { } public void ejbRemove() { } public void setSessionContext(SessionContext ctx) { } }
There is nothing special about the deployment descriptor for this bean. I've included it in the download file. The only detail worth noting is that the EJB must be a well-behaved transactional component, preferably using Component Managed Transactions. Otherwise any persistence operations performed by an instance of EJBObserver may not be rolled back if some other EJBObserver aborts the transaction.
I used WEBLOGIC to build and deploy it. It all worked perfectly the first time. Really. ... Why do you look skeptical?
At this point I cannibalized the appropriate code to get the new home and remote interfaces. Plugging this code into Tester I ended up with the following:
package EJBObserverPattern; import java.util.Properties; import javax.naming.Context; import javax.naming.InitialContext; import javax.rmi.PortableRemoteObject; public class Tester { public static void main (String[] args) { try { Foobar f = new Foobar(); Properties p = new Properties(); p.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory"); p.put(Context.PROVIDER_URL, "t3://localhost:7003"); InitialContext namingContext = new InitialContext(p); RemoteObserverHome obsHome = (RemoteObserverHome) PortableRemoteObject.narrow ( namingContext.lookup (RemoteObserver.class.getName()), RemoteObserverHome.class ); RemoteObserver obs = (RemoteObserver) PortableRemoteObject.narrow ( obsHome.create(), RemoteObserver.class); f.addObserver (obs); f.setSomething (args[0]); f.notifyObservers (args[1]); } catch (Exception e) { e.printStackTrace(); } } }
When I ran the above it worked as expected. The update method of the remote observer was called and I got the expected output on the console. But this isn't exactly what I wanted. Notice that I coupled myself directly to a specific implementation of EJBObserver, namely RemoteObserver. My intention from the beginning was to query some semi-static registry to obtain the JNDI parameters that identify one or more observers. The parameters stored in each entry of the registry would be the initial context factory, the provider url, and the JNDI name of the home interface.
In other words, I wanted to invoke an arbitrary EJBObserver implementation via remote polymorphism.
Decoupling from RemoteObserver was easy enough. I simply changed every reference to EJBObserver -- which I should have done in the first place. Then I ran into a problem with the home interface.
The aforementioned astute reader has by now realized that my home interface RemoteObserverHome does not have some convenient superinterface to use. Any such interface would have to define the create method in order for polymorphism to work. EJBHome doesn't define the create method. Casting the home interface to EJBHome and invoking the create method will generate a compile error.
When you know a class implements a method with a given signature but you can't use polymorphism, reflection works great. Here's how Tester looked after I used reflection to find and invoke the create method of an arbitrary home interface:
package EJBObserverPattern; import java.lang.reflect.Method; import java.util.Properties; import javax.ejb.EJBHome; import javax.naming.Context; import javax.naming.InitialContext; import javax.rmi.PortableRemoteObject; public class Tester { public static void main (String[] args) { try { Foobar f = new Foobar(); String initialContextFactory = "weblogic.jndi.WLInitialContextFactory"; String providerUrl = "t3://localhost:7003"; String homeName = RemoteObserver.class.getName(); Properties p = new Properties(); p.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory); p.put(Context.PROVIDER_URL, providerUrl); InitialContext namingContext = new InitialContext(p); // Get a home interface and narrow it to EJBHome EJBHome obsHome = (EJBHome) PortableRemoteObject.narrow ( namingContext.lookup (homeName), EJBHome.class ); // Find the create method via reflection Method createMethod = obsHome.getClass().getDeclaredMethod ( "create", new Class[0] ); // Invoke the reflected create method to get a remote interface Object remoteRef = createMethod.invoke (obsHome, new Class[0]); // Narrow the remote reference to EJBObserver EJBObserver obs = (EJBObserver) PortableRemoteObject.narrow ( remoteRef, EJBObserver.class); f.addObserver (obs); f.setSomething (args[0]); f.notifyObservers (args[1]); } catch (Exception e) { e.printStackTrace(); } } }
The initial context factory, provider url, and EJB home name can easily be retrieved from some external repository via JdbC. I won't show that implementation. One could also imagine an additional mechanism by which a remote application could register itself in the above repository. I won't show that either. Such details aren't really part of this pattern.
You might be asking yourself, "Self, why resort to reflection when I can subclass EJBHome to define the create method that I need to make polymorphism work?"
That's a reasonable question, Self. Let's try it. I did.
First let's create a new interface EJBObserverHome that extends EJBHome. This will be the superinterface to be extended by any home interface of a remote EJBObserver:
package EJBObserverPattern; public interface EJBObserverHome extends javax.ejb.EJBHome { public EJBObserver create() throws java.rmi.RemoteException, javax.ejb.CreateException; }
Now let's redefine RemoteObserverHome to extend this interface:
package EJBObserverPattern; public interface RemoteObserverHome extends EJBObserverHome { }
Looking good. Now let's rebuild RemoteObserverBean.
(build, build, build ... oops.)
DDCreator RemoteObserverBeanDD.ser Error: Bean EJBObserverPattern.RemoteObserverBean does not comply with the EJB 1.0 specification Bean class: EJBObserverPattern.RemoteObserverBean ----------------------------------------------- home.create() must always return the remote interface type
(Sound of hand slapping forehead.) Of course, a create method of a home interface can't return some abstract class; it must return the exact remote interface type of the EJB that lives there. And if you try to override the create method in RemoteObserverHome to return the right interface type then the compiler will remind you that you can't change the return type of an overridden method. Catch-22. And you can't reuse the same home interface for every remote EJBObserver because there must be a one-to-one correspondence between a home interface and a specific EJB implementation. Otherwise how would the home implementation know which bean to create? Catch-23. (That last bit may have seemed obvious to you. I actually tried it.)
If anybody has another solution to this quandry -- besides reflection -- please enlighten me.
There was one detail of this that bothered me. Looking up the home interface and creating a remote interface is a lot of work to go through every time I want to add an observer. What if the observable instance never changes? That's a lot of work for nothing. When I add an observer I simply want to save the minimum amount of information that will be necessary to instantiate and invoke the remote observer when and if the time comes to do so. There's one more abstraction to make.
Patterns can be addictive. You can't eat just one. A proxy pattern was a perfect fit. I created a new class called EJBObserverProxy that implemented EJBObserver. EJBObserverProxy was designed to save all the information needed to instantiate and invoke a remote observer. It also encapsulated the reflective mechanism by which this was accomplished:
package EJBObserverPattern; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.rmi.RemoteException; import java.util.Properties; import java.util.Set; import java.util.Iterator; import java.util.HashSet; import javax.ejb.EJBException; import javax.ejb.EJBHome; import javax.rmi.PortableRemoteObject; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; public class EJBObserverProxy implements java.io.Serializable, EJBObserver { private Properties mContextValues = null; private String mJNDIName = null; public EJBObserverProxy ( String initialContextFactory, String providerUrl, String jndiName) { Properties p = new Properties(); p.put(Context.INITIAL_CONTEXT_FACTORY, initialContextFactory); p.put(Context.PROVIDER_URL, providerUrl); init (p, jndiName); } public EJBObserverProxy ( Properties contextValues, String jndiName ) { init (contextValues, jndiName); } protected void init ( Properties contextValues, String jndiName ) { mContextValues = contextValues; mJNDIName = jndiName; } public String getName () { return mJNDIName; } public Properties getContextValues () { return mContextValues; } public int hashCode () { return (mContextValues.toString() + mJNDIName).hashCode(); } public boolean equals (Object o) { EJBObserverProxy loc = (EJBObserverProxy) o; return ( (mContextValues.equals (loc.getContextValues())) && (mJNDIName.equals (loc.getName())) ); } /** * Instantiate the remote observer referenced by this object and update it. */ public void update (EJBObservable object, Object arg) throws RemoteException, EJBException { try { String beanHomeName = this.getName(); Properties p = this.getContextValues(); InitialContext namingContext = new InitialContext(p); /* Get a reference to the named object and narrow it to EJBHome to make sure it is a home interface. */ EJBHome obsHome = (EJBHome) PortableRemoteObject.narrow ( namingContext.lookup (beanHomeName), EJBHome.class ); /* We can't directly cast this reference to a home interface because each EJB implementation will have its own home interface in some arbitrary class hierarchy. EJBHome doesn't define a create() method so we can't invoke it via polymorphism. Let's use reflection to find and invoke the create() method. Look for a method named 'create' which takes no parameters. */ Method createMethod = obsHome.getClass().getDeclaredMethod ( "create", new Class[0] ); /* Invoke the 'create' method of the home interface via our reflected Method. */ Object remoteObj = createMethod.invoke (obsHome, new Object[0]); /* Narrow the resulting reference to the EJBObserver interface, which each remote observer is obligated to implement. */ EJBObserver obs = (EJBObserver) PortableRemoteObject.narrow ( remoteObj, EJBObserver.class); /* Update this observer with an EJBObservable instance and some argument. Foobar is an instance of EJBObservable. */ obs.update (object, arg); } catch (NoSuchMethodException e) { throw new EJBException (e); } catch (NamingException e) { throw new EJBException (e); } catch (InvocationTargetException e) { throw new EJBException (e); } catch (IllegalAccessException e) { throw new RuntimeException (e.toString()); } } }
There are a number of things to note about EJBObserverProxy:
A reader suggested the use of javax.ejb.Handle to maintain a persistent relationship to a remote observer. You can get a Handle to a remote object and serialize it to persistent storage. The Handle can be materialized from storage at any time to recreate the remote object. I considered encapsulating a Handle object inside EJBObserverProxy but I couldn't figure out how to implement hashCode such that two Handle objects that pointed to the same remote interface would evaluate to the same hash code. Remember that duplicate observers cannot be added to a single observable. The HashSet implementation first compares hash codes to determine if two objects might be equal. If the hash codes are different then it doesn't bother calling the equals method. So there was no way (that I found) to eliminate duplicate observers using Handle objects.
Of course, the act of writing the above paragraph guarantees that it can be done. Such is the nature of the universe.
Now I could use EJBObserverProxy to greatly simplify Tester, thus simplifying the eventual client code that will use this pattern:
package EJBObserverPattern; public class Tester { public static void main (String[] args) { try { Foobar f = new Foobar(); EJBObserverProxy obs = new EJBObserverProxy ( "weblogic.jndi.WLInitialContextFactory", "t3://localhost:7003", RemoteObserver.class.getName()); f.addObserver (obs); f.setSomething (args[0]); f.notifyObservers (args[1]); } catch (Exception e) { e.printStackTrace(); } } }
The term client in this context means the party that instantiates the EJBObservable object. The client could easily be another EJB.
Recall that my motivation was to allow an arbitrary collection of EJBs to observe changes in a central data repository. Any such EJB can now simply extend EJBObserver to define its remote interface and register its JNDI information with the manager of the central repository via some simple database table. If a business entity within the central repository is changed then the central repository manager, after making the change, instantiates a subclass of EJBObservable which encapsulates the updated business entity. The EJBObserver registry is then used to add the appropriate observers and notifyObservers is called.
If you look again real close you might recognize a mediator pattern in the above paragraph.
Any remote EJBObserver may abort the transaction by simply throwing a system exception or calling setRollbackOnly on its EJBContext object. Recall that a system exception is a java.lang.RuntimeException or java.rmi.RemoteException, or any subclass directly or indirectly thereof. Note that Tester doesn't start a new transaction before it calls notifyObservers. This is an academic example. If I really wanted a single transaction context I'd have to start a new client demarcated transaction. In reality, the role of Tester will usually be filled by an EJB and container managed transactions would be used.
This implementation can be used in any distributed application where an Observer pattern makes sense -- not just in the distributed transaction scenario that I've outlined.
Greg Comeau is a Senior Software Engineer at Webb Interactive Services, Inc. with Offices in Denver and Boulder, Colorado. The author prefers the latter but harbors no ill will against the former.
Comments on this article are welcome. Flames will go unanswered. I'd rather be skiing. Have a nice day.
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。