How do I add a NotificationListener to a remote MBeanServer?
You implement the org.jboss.jmx.adaptor.rmi.RMINotificationListener and register it with the RMIAdaptor. Here is an example listener from the testsuite:
package org.jboss.test.jmx.invoker; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; import javax.management.Notification; import org.jboss.jmx.adaptor.rmi.RMINotificationListener; /** An RMI callback implementation used to receive remote JMX notifications * @author Scott.Stark@jboss.org * @version $Revision: 1.2.6.2 $ */ public class Listener implements RMINotificationListener { private int expectedCount; private int count; public Listener(int expectedCount) { this.expectedCount = expectedCount; } public int getCount() { return count; } public void export() throws RemoteException { UnicastRemoteObject.exportObject(this); } public void unexport() throws RemoteException { UnicastRemoteObject.unexportObject(this, true); } public void handleNotification(Notification event, Object handback) { System.out.println("handleNotification, event: "+event+", count="+count); count ++; synchronized( this ) { if( count == expectedCount ) notifyAll(); } } }
Here is how the Listener is registered:
/** Test the remoting of JMX Notifications * @throws Exception */ public void testNotification() throws Exception { log.info("+++ testNotification"); Listener listener = new Listener(10); listener.export(); RMIAdaptor server = (RMIAdaptor) getInitialContext().lookup("jmx/invoker/RMIAdaptor"); server.addNotificationListener(getObjectName(), listener, null, "runTimer"); synchronized( listener ) { listener.wait(15000); } server.removeNotificationListener(getObjectName(), listener); listener.unexport(); int count = listener.getCount(); assertTrue("Received 10 notifications, count="+count, count == 10); }
The Listener stub would be created with rmic and deployed to the server, typically by dropping a jar into the server/xxx/lib directory.
Another approach to use a NotificationListener directly
In 3.2.3 we did this:
RMIAdaptor adaptor = ic.lookup("jmx/rmi/RMIAdaptor"); MBeanServer server = new RMIConnectorImpl(adaptor);
Then added a NotificationListener to the server. Our listener (on the client) would respond to notifications emitted by the remote MBeanServer.
In 4.0 RMIConnectorImpl has been removed. If you add a NotificationListener to RMIAdaptor directly you will not get the same behaviour as the old RMIConnectorImpl.
This is because RMIAdaptor just copies your listener to the remote server and adds the copy to the remote MBeanServer. Your original isn't connected to anything. The copy will respond to notifications, but will be running on the remote machine, not your local one.
The solution that I used is to create a remote proxy for my local listener that can be copied and added to the remote MBeanServer - and that forwards notifications via RMI to my listener from the remote machine.
The NotificationListener interface is not remote (the method does not throw RemoteException) so we need a remote version of this interface. 4.0 has one of these, org.jboss.jmx.adaptor.rmi.RMINotificationListener, so we can reuse that.
You need an implementation:
package jmx; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; import javax.management.Notification; import javax.management.NotificationListener; import org.jboss.jmx.adaptor.rmi.RMINotificationListener; /** * bridges the gap between an RMINotificationListener, which must declare * <code>RemoteException</code> and a NotificationListener which does not. * This class is exported via JRMP - a proxy for RMINotificationListener * is created and sent to the remote machine, that calls the * <code>handleNotification</code> method of this class - which in turn * calls the non-remote <code>handleNotification</code> method of the local * listener. * * @author Adrian Bigland */ public class RmiJmxNotificationBridge extends UnicastRemoteObject implements RMINotificationListener { /** pass on notifications to this listener */ private NotificationListener listener; /** * creates a new bridge between the remote notification listener interface * and the local non-remote listener. * @param listener expose this as a remote listener * @throws RemoteException if this cannot be exported */ public RmiJmxNotificationBridge(NotificationListener listener) throws RemoteException { super(); this.listener = listener; } /* (non-Javadoc) * @see org.jboss.jmx.adaptor.rmi.RMINotificationListener#handleNotification(javax.management.Notification, java.lang.Object) */ public void handleNotification(Notification notification, Object handback) throws RemoteException { try { listener.handleNotification(notification, handback); } catch (Exception e) { throw new RemoteException("client failed to handle the notfication", e); } }
That can be serialized from your local machine, and will call your listener back via RMI - I am using JDK 1.5, which doesn't need stubs created using rmic. If you are not using 1.5 you will need to create a stub and put this in your JBoss server's classpath. The magic happens in the constructor of UnicastRemoteObject - this sets up the remoting. The object we get from the constructor can be sent off to a remote machine. It will call back another object that has our local listener... we didn't get this object back from the constructor.
This class doesn't implement NotificationListener though, it implements a similar interface that is remotable. Although you can serialize this to the remote MBeanServer, because it does not implement NotificationListener you cannot add it as a listener to the server. You need to wrap it in a real NotificationListener that implements the NON remote interface, and just passes calls on the the remote version. Which in turn passes these calls on to our real listener via RMI:
package jmx; import java.io.Serializable; import java.rmi.RemoteException; import javax.management.Notification; import javax.management.NotificationListener; import org.apache.log4j.Logger; import org.jboss.jmx.adaptor.rmi.RMINotificationListener; import org.jboss.util.id.GUID; /** * A listener that forwards calls to a remote listener. The remote listener * cannot be used directly because it implements a remote version of the * <code>NotificationListener</code> interface that throws * <code>RemoteException</code>s from its methods - and is therefore * incompatible with <code>NotificationListener</code> * <p> * We can't combine both remote and non-remote interface in the proxy for the * remote listener because we are using <code>UnicastRemoteObject</code> to * create it for us - it only knows how to create a proxy that can forward * remote method calls using RMI/JRMP. We need to wrap this proxy with another * class that knows how to link the non-remote and remote listener interfaces. * </p> * * @author Adrian Bigland */ public class JmxRmiNotificationBridge implements NotificationListener, Serializable { /** log errors and progress with this */ private transient Logger logger = Logger.getLogger(getClass()); /** * globally unique ID * allows us to remove a listener after adding it. We can't refer to the * listener that the server adds in response to our addNotificationListener * call - because although we know which object we serialized over there, * the copy that the server creates from the serialization stream is * unknown to us - how do we tell the server to remove the listener it * created when we know it is not needed any more? Passing a GUID and * making equals use it solves the problem. */ private GUID id = new GUID(); /** * proxy for a remote listener - forwards calls to the remote * RMINotificationListener interface to another implementation running on * a remote machine using RMI/JRMP */ private RMINotificationListener remoteListener; /** * creates a new bridge between the JMX non-remote handle method and * the remote RMI handle method. * @param remoteListener forward calls to * <code>NotifcationListener</code>'s non-remote * <code>handleNotification</code> method to * <code>RMINotificationListener<code>'s remote * <code>handleNotification</code> method implemented by this proxy */ public JmxRmiNotificationBridge(RMINotificationListener remoteListener) { this.remoteListener = remoteListener; } /** * checks if this is the same as another object - which is true only if * the other object is also a <code>JmxRmiNotificationBridge</code> with * the same GUID. * @param o is this the same as this object? * @return true if this is essentially the same thing as another object */ public boolean equals(Object o) { if (o == null) return false; if (o == this) return true; if (!(getClass().equals(o.getClass()))) return false; JmxRmiNotificationBridge bridge = (JmxRmiNotificationBridge)o; return id.equals(bridge.id); } /** * gets a hash code consistent with equals. * @return a hash code for this object */ public int hashCode() { return id.hashCode(); } /** * acts just prior to the end of deserialization and restores a Log4j * logger - there wasn't any point in serializing this. Since * deserialization creates objects without calling their constructors, we * needed to do this constructor step here. * @return the new object */ Object readResolve() { logger = Logger.getLogger(getClass()); return this; } /* (non-Javadoc) * @see javax.management.NotificationListener#handleNotification(javax.management.Notification, java.lang.Object) */ public void handleNotification(Notification notification, Object handback) { try { remoteListener.handleNotification(notification, handback); } catch (RemoteException e) { logger.error("a remote listener failed to handle a notification", e); } } }
Note that I used a GUID and implementations of hashCode and equals that are consistent with it - so that later calls to remove your listener can be honoured, but this was a mistake on my part. RemoteObject implements equals and hashCode meaningfully for remote objects, so all my non-remote object had to do was pass calls to equals and hashCode through to the remote object. You only have to pay attention to the remote object method implementation if you are using custom socket factories.
You also need a way of tying these classes together:
package jmx; import java.rmi.RemoteException; import javax.management.NotificationListener; import org.jboss.jmx.adaptor.rmi.RMINotificationListener; /** * tools for connecting to remote MBeanServers. * * @author Adrian Bigland */ public class JRMPRemotingTools { /** * prevents instantiation - use the static methods instead. */ private JRMPRemotingTools() {} /** * exports the given listener using JRMP. This method returns a proxy * for your listener, that you can send to a remote machine. When the * remote machine calls <code>handleNotification</code> the proxy uses * JRMP to contact your listener and passes on the call to it. You can * send this stub to the remote machine via any method that serializes * it. For example, you could bind this stub to JNDI - any remote machine * looking this up will be sent a serialized copy. * @param listener export this listener using JRMP * @return a serializable stub that you can send to a remote machine * @throws RemoteException if the proxy cannot be created */ public static NotificationListener exportListener( NotificationListener listener) throws RemoteException { RMINotificationListener exportable = null; exportable = new RmiJmxNotificationBridge(listener); // listener that forwards non-remote calls to the remote stub return new JmxRmiNotificationBridge(exportable); } }
Call the exportListener method, and the add the resulting listener to RMIAdaptor directly.
You will need to make the first two classes available to JBoss on its classpath (I currently add them to my server's lib directory in a jar, but you may find a better solution). You will also need to do the same for any rmic stubs that you need. I haven't had to do this, so I haven't got details of how to do this.
Comments