Observer Pattern - An Enterprise JavaBean Implementation (转)
Observer Pattern - An Enterprise JavaBean Implementation
Mailto:gcomeau@webb.NET">Greg Comeau
DOWNLOAD: ide.com/patterns/ObserverPattern/EJBObserverPattern.zip">EJBObserverPattern.zip
Motivation
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.
Implementation
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.
The EJBObserver Interface
A new interface is required that I called EJBObserver:
package EJBObserverPattern; public interface EJBObserver { 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.
The EJBObservable Class
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; public class EJBObservable implements java.io.Serializable { protected Set mObservers = new HashSet(); protected boolean mChanged = false; 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; } public void notifyObservers () throws RemoteException, EJBException { this.notifyObservers (null); } 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(); } } }
Foobar -- A Subclass of EJBObservable
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); } public String toString () { return mSomething; } }
The Test Harness
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.
Remote Interface of EJBObserver Implementation
First I created the remote interface, which I subclassed from the business interface EJBObserver:
package EJBObserverPattern; public interface RemoteObserver extends javax.ejb.EJBObject, EJBObserver { }
Home Interface of EJBObserver Implementation
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.
RemoteObserverBean
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 { 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?
Instantiate the Remote EJBObserver
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.
Reflection of the Home Interface
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.
Why Not Subclass EJBHome?
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.
One More Abstraction
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.
EJBObserverProxy
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())) ); } public void update (EJBObservable object, Object arg) throws RemoteException, EJBException { try { String beanHomeName = this.getName(); Properties p = this.getContextValues(); InitialContext namingContext = new InitialContext(p); EJBHome obsHome = (EJBHome) PortableRemoteObject.narrow ( namingContext.lookup (beanHomeName), EJBHome.class ); Method createMethod = obsHome.getClass().getDeclaredMethod ( "create", new Class[0] ); Object remoteObj = createMethod.invoke (obsHome, new Object[0]); EJBObserver obs = (EJBObserver) PortableRemoteObject.narrow ( remoteObj, EJBObserver.class); 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:
- It encapsulates member variables that store the settings required to lookup a home interface.
- It defines hashCode and equals methods so that EJBObserverProxy will be a well-behaved member of a Set. Recall that instances of EJBObserver are stored in a HashSet within EJBObservable. The semantics defined by Observable dictate that any given Observer must be unique within an instance of Observable. A Set implementation uses the hashCode and equals method to enforce uniqueness.
- It implements java.io.Serializable since instances of this class will be stored inside EJBObservable, which will be serialized as an argument of a remote method call.
- The update method encapsulates all of the nasty JNDI and reflection that we need to remotely invoke the associated EJBObserver instance.
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.
New Tester using EJBObserverProxy
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.
Motivation Redux
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.
About the Author
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.
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341