Proxying a JMX Connection: Adapting To Another Protocol

We have found some interoperation problems in the JMX RMI/IIOP connector, or more exactly, in the way some JMX classes from the javax.management.modelmbean package are deserialized by the JMX RMI/IIOP stack. The bug is strictly linked to the IIOP stack and doesn't show up with the regurar RMI/JRMP protocol connector.

Nevertheless, there are some JDK 5 applications out there which use the JMX RMI/IIOP connector to expose Model MBeans through JMX. When connecting to such a JDK 5 application with JDK 6 jconsole, through the IIOP connector, the tool will spit out ominous stack traces on the terminal console and the MBean Tab will not display any of the exposed Model MBeans (other types of MBeans are correctly displayed).

Until such a time where the bug gets fixed, I can suggest two work around:

  1. Use jconsole from JDK 5 on the client side, or
  2. Creates a JDK 5 proxy that will use RMI/IIOP on the south side to connect to the JDK 5 application, and will open a JMX RMI/JRMP connector on the north side to allow JDK 6 clients to use RMI/JRMP instead of RMI/IIOP.
JRMP/IIOP Proxy

You can run the proxy on the client side, so there's no need to modify the server application. The proxy will simply take any JMX RMI/JRMP requests it receives from the north side and translate them into corresponding JMX RMI/IIOP requests to the south side.

A connector server is usually associated with an MBean Server obtained using the methods ManagementFactory.getPlatformMBeanServer() or MBeanServerFactory.createMBeanServer(). But in fact it can be associated with any object that implements the MBeanServer interface. Here, we create such an object (an instance of MBeanServerProxyHandler in the code below) where every method in the MBeanServer interface is forwarded to the remote MBeanServer using the RMI/IIOP connector. There are a few MBeanServer methods that are not available remotely, but that will still be called by the JMX Connector Server when deserialiazing attribute values and parameters sent by the client, so we need to fake those methods with code that does something reasonable.

Below is the implementation of such a proxy - which uses a java.reflect.Proxy to achieve this trick. Simply compile the class and run it with JDK 5 (do not run it with JDK 6):

   $JDK5_HOME/java com.sun.jmx.example.proxy.RMIIIOPProxy \\
          <rmi-port (north)> <rmi-lookup-name (north)> \\
          <iiop-host (south)> <iiop-port (south)> <iiop-lookup-name (south)>
      

You can then simply point your JDK client to

   service:jmx:rmi:///jndi/rmi://<host>:<rmi-port>/<rmi-lookup-name>
      

and the proxy will translate and forward all your JDK 6 client requests through IIOP to the south side...

This is not an ideal situation, but you might be able to use that as a temporary work around.

/\*
 \* Copyright 2007 Sun Microsystems, Inc.  All Rights Reserved.
 \*
 \* Redistribution and use in source and binary forms, with or without
 \* modification, are permitted provided that the following conditions
 \* are met:
 \*
 \*   - Redistributions of source code must retain the above copyright
 \*     notice, this list of conditions and the following disclaimer.
 \*
 \*   - Redistributions in binary form must reproduce the above copyright
 \*     notice, this list of conditions and the following disclaimer in the
 \*     documentation and/or other materials provided with the distribution.
 \*
 \*   - Neither the name of Sun Microsystems nor the names of its
 \*     contributors may be used to endorse or promote products derived
 \*     from this software without specific prior written permission.
 \*
 \* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 \* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 \* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 \* PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 \* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 \* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 \* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 \* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 \* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 \* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 \* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 \*/

package com.sun.jmx.example.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.rmi.registry.LocateRegistry;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import javax.management.MBeanServer;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.loading.ClassLoaderRepository;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;

/\*\*
 \* Class RMIIIOPProxy - Creates a JMX RMI/JRMP proxy for a JMX RMI/IIOP server
 \* @author Daniel Fuchs, 2007.
 \*/
public class RMIIIOPProxy {

    /\*\*
     \* A logger for this class.
     \*\*/
    private static final Logger LOG =
            Logger.getLogger(RMIIIOPProxy.class.getName());

    // Just a quick access Map to convert MBeanServer methods to their
    // corresponding MBeanServerConnection equivalent....
    //
    private static class ProxyMethodMap {
        static final Map<Method,Method> methodMap;
        static {
            try {
                final Method[] mm =
                        MBeanServerConnection.class.getMethods();
                methodMap = new HashMap<Method,Method>(mm.length);
                for (Method m : mm) {
                    try {
                        final Method ms =
                                MBeanServer.class.getMethod(m.getName(),
                                m.getParameterTypes());
                        methodMap.put(ms,m);
                    } catch (Exception x) {
                        // OK - no such method...
                    }
                }
            } catch (Exception x) {
                throw new ExceptionInInitializerError(x);
            }
        }
    }

    // This class makes it possible to create a Proxy implementing
    // the MBeanServer interface by wrapping an MBeanServerConnection
    //
    private static class MBeanServerProxyHandler
            implements InvocationHandler {
        final MBeanServerConnection conn;
        final ClassLoaderRepository classLoaderRepository;

        // Creates an MBeanServerProxyHandler.
        public MBeanServerProxyHandler(MBeanServerConnection conn) {
            this.conn=conn;
            classLoaderRepository = new ClassLoaderRepository() {
                public Class loadClass(String className)
                    throws ClassNotFoundException {
                    // trivial impl: we're
                    // loading everything through the System Class Loader.
                    return Class.forName(className,false,
                            ClassLoader.getSystemClassLoader());
                }
                public Class loadClassBefore(ClassLoader stop, String className)
                    throws ClassNotFoundException {
                    // trivial impl: ignore 'stop' and 'exclude' - since we're
                    // loading everything through the System Class Loader.
                    return loadClass(className);
                }
                public Class loadClassWithout(ClassLoader exclude,
                        String className) throws ClassNotFoundException {
                    // trivial impl: ignore 'stop' and 'exclude' - since we're
                    // loading everything through the System Class Loader.
                    return loadClass(className);
                }
            };
        }

        // Takes a method from MBeanServer.class and forwards it to
        // a wrapped MBeanServerConnection...
        //
        public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {

            // Methods also defined in MBeanServerConnection
            final Method toInvoke = ProxyMethodMap.methodMap.get(method);
            if (toInvoke != null) {
                try {
                    return toInvoke.invoke(conn,args);
                } catch (InvocationTargetException x) {
                    throw x.getCause();
                }
            }

            final String name = method.getName();
            final Class[] sig = method.getParameterTypes();

            // equals & hashcode
            if ("equals".equals(name) && sig.length == 1
                    && Object.class.equals(sig[0])) {
                return super.equals(args[0]);
            }
            if ("hashCode".equals(name) && sig.length == 0) {
                return super.hashCode();
            }
            if ("toString".equals(name) && sig.length == 0) {
                return super.toString();
            }

            // There might be better ways - but we will impose that
            // all necessary classes are given in the classpath.
            //
            if ("getClassLoaderFor".equals(name) && sig.length == 1
                    && ObjectName.class.equals(sig[0])) {
                return ClassLoader.getSystemClassLoader();
            }
            if ("getClassLoader".equals(name) && sig.length == 1
                    && ObjectName.class.equals(sig[0])) {
                return ClassLoader.getSystemClassLoader();
            }
            if ("getClassLoaderRepository".equals(name) && sig.length == 0) {
                return classLoaderRepository;
            }

            // The connector server shouldn't need to call any other methods....
            //
            throw new UnsupportedOperationException(name+Arrays.asList(sig));
        }

    }


    /\*\*
     \* Starts a JMX RMI/JRMP proxy for a JMX RMI/IIOP Server.
     \* Run this using JDK 1.5.
     \* @param args the command line arguments:
     \* {@code <rmiport> <rmi-server-name> <iiophost> <iiopport> <iiop-server-name>}
     \*/
    public static void main(String[] args) throws Exception {

        // check args
        if (args.length < 5) {
            System.err.println("syntax: java "+RMIIIOPProxy.class.getName()+
                    " <rmiport> <rmi-server-name> <iiophost> <iiopport> <iiop-server-name>");
            System.exit(1);
        }

        final int rmiport  = Integer.parseInt(args[0]);
        final String rmiServerName = args[1];
        final String iiophost = args[2];
        final int iiopport = Integer.parseInt(args[3]);
        final String iiopServerName = args[4];

        if (rmiport < 1) throw new IllegalArgumentException(args[0]);
        if (iiopport < 1) throw new IllegalArgumentException(args[3]);

        // creates north side URL & conf (tweak this if you want to add
        // authentication, authorization, etc....)
        //
        final JMXServiceURL rmi =
                new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:"
                +rmiport+"/"+rmiServerName);
        final Map<String,Object> northEnv = Collections.emptyMap();

        // creates south side URL & conf (tweak this if south side requires
        // authentication, authorization, etc....)
        //
        final JMXServiceURL iiop =
                new JMXServiceURL("service:jmx:iiop:///jndi/iiop://"+iiophost+":"
                +iiopport+"/"+iiopServerName);
        final Map<String,Object> southEnv = Collections.emptyMap();

        // Connect to south side...
        final JMXConnector south = JMXConnectorFactory.connect(iiop,southEnv);
        try {

            // Make an MBeanServer from south side MBeanServerConnection
            final MBeanServerProxyHandler proxyHandler =
                    new MBeanServerProxyHandler(south.getMBeanServerConnection());
            final MBeanServer remote = (MBeanServer) Proxy.newProxyInstance(
                    ClassLoader.getSystemClassLoader(),
                    new Class[] {MBeanServer.class}, proxyHandler);
            System.out.println("Connection established with south server at: "+
                    iiop);

            // Start a JMX RMI/JRMP connector server for north side...
            //
            // You may need to tweak these lines if you want to add SSL etc...
            LocateRegistry.createRegistry(rmiport);
            final JMXConnectorServer cs =
                    JMXConnectorServerFactory.newJMXConnectorServer(rmi,
                    northEnv,remote);
            cs.start();

            System.out.println("Proxy server listening at: "+rmi);
            System.out.println("Strike <Enter> to exit");
            System.in.read();
            cs.stop();

        } finally {
            south.close();
        }
    }

}

Cheers, and my thanks to Eamonn for his editorial review!
-- daniel

Comments:

Hello Daniel, first of all thanks you for providing this article, My question is will it be possible to proxy multiple JMX Instances through single JMX Proxy Server if so what and how would you recommend ?

Posted by Arron on April 17, 2008 at 09:59 AM CEST #

Hi Arron,

It depends on what you mean by "proxying".
If it's the kind of proxying described above then you can start as many proxies as you want in the same JVM.

You would only need to rework a bit the main() method above in order to allow to pass several proxy configurations, and then create one proxy per such configuration.

If what you have in mind is rather federating several servers views into a single MBeanServer, then what you need is something like cascading/MBeanServer federation.

There's an MBeanServer federation feature planned for JMX 2.0 which will allow you to do that, but we haven't released any prototype yet. You could also use OpenDMK cascading - but it might not be scalable enough - depending on how many MBeans and servers you federate.

Hope this helps,

-- daniel

Posted by daniel on April 17, 2008 at 10:58 AM CEST #

Hello Daniel,

Thanks for rapid reply,

I am actually trying to Act as a Proxy Server infront of over 100 JMX Servers, each having their own security models and protocols, i.e some use JMXMP and others use RMI.

you can say I am trying to have a fedaration feature, What I am trying to do is to enable clients(GUI) connect to a JMX Server without knowing where the server is located (i.e IP and port )but just let them select the server they wish to connect to by an ID and query methods by providing Username and Password This way I will not have to open JMX Servers to public and be able to use private IP addresses and still be able to connect to them etc as well as having single control panel managing multiple Servers.

If I were to develop my own way of accomplishing this, what would you recommend the best solution be as I have very limited amount of time and waiting for JMX2.0 will not be reasonable for me.

i.e I could create an HTTP Server that lets you query the JMX Servers or to Create an JMX Server in proxy itself and register couple of MBeans that keeps the state of Sessions and forward the requests to required Server? What would you say best option will be? will the latter option be able to handle quite few amount of connections lets say 100 or maybe 1000 at a given time?.

Your ideas are greatly appreciated.

Regards
Arron.

Posted by Arron on April 17, 2008 at 11:43 AM CEST #

Post a Comment:
  • HTML Syntax: NOT allowed
About

Daniel Fuchs blogs on Scene Builder, JMX, SNMP, Java, etc...

The views expressed on this blog are those of the author and do not necessarily reflect the views of Oracle.

Search

Categories
Archives
« July 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  
       
Today