Distributed Services and Connectors

Table of Contents

RMI Connector Details
JMXMP Connector Details
A Flexible JMXConnectorServer Agent

The following phrases all describe the same thing.

I recommend that you use Java 1.4 or later if you will be running a JMXConnector. If you run into problems running connectors with 1.3, I don't consider that my problem.

A Remote JMX Client is just like a Local JMX Client except that it must instantiate a Connector client and connect it up to a Connector server to get a MBeanServerConnection. It then uses the MBeanServerConnection in the same way as a Local JMX Client. Remote JMX applications can not work with an MBeanServer object, only an MBeanServerConnection.

The Agent must run a Connector Server using the same protocol that you will use on the client side. If your server serves tranport "rmi", then your client(s) need to use protocol "rmi".

RMI and JMXMP URLs

Sample JMXMP Server URL: service:jmx:jmxmp://0.0.0.0:2004

This simply specifies to have the JMXConnectorServer listen to port 2004 using the JMXMP protocol. 0.0.0.0 is the IP address IPADDR_ANY, which means answer connections to the specified port on any IP addresses for this host.

[Note]Note

With Sun's JMX Remote JMX RI, the hostname/ip-addr segment is not used to limit the target IP address/name, as it should. It must just match any valid IP address/name for this host (including localhost or 127.0.0.1), and it will then listen to IPADDR_ANY. You will have to do custom coding (or use an IP filtering or firewall product) to limit the listen addresses.

Sample RMI Server URL: service:jmx:rmi:///jndi/rmi://localhost:2003/jmxdemo

The rmi:// part specifies that the JMX server will use the RMI protocol, and instead of running its own listener, it will advertize on the RMI server at the specified location. The given host/ip address and port identify the end-point at which to reach an RMI server which is already running. The jmxdemo part specifies that the this program will register this service with the key jmxdemo on the RMI server.

Sample JMXMP Client URL: service:jmx:jmxmp://localhost:2004

This is as straight-forward as you can get. The client will attempt to connect to a JMXConnectorServer on port 2004 on localhost and will speak JMXMP protocol.

Sample RMI Client URL: service:jmx:rmi:///jndi/rmi://localhost:2003/jmxdemo

The rmi:// part specifies to connect to the RMI server at the specificied host and port, and look for the service with the given JNDI name, jmxdemo in this case. It will then use the JMX RMI protocol to speak to the JMXConnectorServer which registered that service.

I definitley do not recommend that you use an RMI connector (for reasons discussed below). Nevertheless, most of our examples will use RMI because any self-respecting JMX developer should be converstant with RMI connectors. If you're not going to use an RMI connector, you should at least understand why. The only real-word reason to use RMI, in my opinion, is that you have so many JMX servers which are so volatile that you need dynamic service lookup; or that you are prohibited from using any class outside of the base J2SE (even if it is made by Sun). If you use any protocol other than RMI, you will need to add some jar to your classpath, since J2SE does not contain classes implementing any other protocol than RMI. For JMXMP, this simply means adding jmxremote_optional.jar from Sun's JMX Remote RI (available here).

Here's a simple remote client (also available at ConnectorClient.java). It justs displays information about the specified MBean, but you can copy and paste the relevant portions to make any application of yours a JMX client; or use this as a starting point. (I'll present a Connector Server shortly). As you can see, there is very little code other than argument parsing and TLS setup (ignore the TLS part for now, if you are just learning about Distributed Services and Connectors right now).

Example 8.1. ConnectorClient.java

/*
 * @(#)$Id: ConnectorClient.java 2011 2009-01-09 17:07:43Z blaine $
 *
 * Copyright 2004-2009 Axis Data Management Corp.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


import javax.management.remote.JMXConnector;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.management.JMException;
import java.util.Map;
import java.util.HashMap;
import java.io.IOException;
import javax.management.remote.JMXAuthenticator;

/**
 * Dumps info about the specified MBean from the JMXConnectorServer at
 * the specified URL.
 */
public class ConnectorClient {
    static MBeanServerConnection mbsc = null;
    static public void main(String[] sa) throws IOException, JMException {
        boolean tlsMode = false;
        String urlString = null;
        String beanId = null;

        /* Command-line argument parsing */
        if (sa.length == 3 && sa[0].equals("--tls")) {
            tlsMode = true;
            urlString = sa[1];
            beanId = sa[2];
        } else if (sa.length == 2) {
            urlString = sa[0];
            beanId = sa[1];
        }
        if (urlString == null) {
            System.err.println(
                "SYNTAX:  java [--tls] ConnectorClient JMXServiceURL beanspec\n"
                + "Sample invocations:\n    "
                + "service:jmx:rmi:///jndi/rmi://saturn.acme.com:9999/jndi_id\n"
                + "    service:jmx:jmxmp://neptune.acme.com:9999\n"
                + "(A JMXMP impl. required in classpath for jmxmp service.\n"
                + "Try Sun's free jmxremote_optional.jar).");
            System.exit(2);
        }

        Map env = new HashMap();
        if (tlsMode) {
            // TLS Mode setup.

            // Note that the settings below are conditional, so you can
            // override then with "java -Djavax...=Y... ConnectorClient..."
            // It's definitely not safe to use -D to set passwords, though,
            // but it's useful for prototyping.
            if (System.getProperty("javax.net.ssl.trustStore") == null)
                System.setProperty("javax.net.ssl.trustStore",
                        "server-cert.store");
            if (System.getProperty("javax.net.ssl.keyStorePassword") == null)
                System.setProperty("javax.net.ssl.keyStorePassword",
                        "pwdC1store");
            // Comment out the lines above if the server isn't requiring
            // a cert for your client.
            if (System.getProperty("javax.net.ssl.keyStore") == null)
                System.setProperty("javax.net.ssl.keyStore", "client1.store");

            /* The method above is the simplest (IMO therefore the best)
             * method if your application doesn't use certs for any other
             * purpose.  You should use the instance-based TLS configuration
             * method if your app uses certs for any other purpose in a
             * single instantiation (i.e., it could fetch web pages over
             * https, or be a TLS Soap client, etc., or it could run connect
             * to multiple TLS JMXConnectorServers).
             *
             * The instance-based method allocates a SSLSocketFactory
             * based on the SSLContext instance which you instantiate and
             * configure, so you can configure multiple SSLSocketFactories
             * with different SSLContext instances.  This all applies to
             * any standard JSSE TLS application, but for JMX, you 
             * associate the allocated SSLSocketFactory to the Connector
             * with:
             * 
             *  env.put("jmx.remote.tls.socket.factory", yourFactory);
             */

            env.put("jmx.remote.profiles", "TLS");
            //env.put("jmx.remote.tls.enabled.protocols", "TLSv1");
            //env.put("jmx.remote.tls.enabled.cipher.suites",
                //"SSL_RSA_WITH_NULL_MD5");
            // Most users will probably want to use the default TLS 
            // protocols and suites.
        }

        JMXConnector c =
                JMXConnectorFactory.connect(new JMXServiceURL(urlString), env);
        // If you aren't setting a profile or any other options, you can use
        // null for the second connect() parameter, instead of an empty list.
        try {
            mbsc = c.getMBeanServerConnection();

            // For this example, I chose to not expose the Adaptor as an
            // MBean, which is sometimes a good thing to do for security.
            // Therefore, I use it as a normal Java Object.
            System.err.println("Info on '" + beanId + "' is:");
            javax.management.MBeanAttributeInfo[] aa =
                    mbsc.getMBeanInfo(new ObjectName(beanId)).getAttributes();
            for (int i = 0; i < aa.length; i++)
                System.err.println(aa[i].getName());
        } finally {
            if (c != null) c.close();
        }
        System.exit(0);
    }
}

RMI Connector Details

The RMI protocol is required by all JMX Remote implementations, but it requires use of a running rmi registry server, and indirection (most simply by JNDI).

If you start an JMXConnectorServer with an rmi URL, you will need to start an rmi registry server if one is not already running on the host and port specified at the end of your URL. This is easily done by running

    rmiregistry 2003

Where 2003 is the port (by default it'll serve all the host addresses on the server). The program rmiregistry comes with Java (Sun's JDKs, at least).

For some reason, Sun put a lot of effort into adding an extra level of indirection into the RMI method. Besides the extra configuration necessary, you actually have to run extra processes for clients to locate your RMI servers. Instead of specifying a URL that points right to your JMX service, your clients must use a lookup service like JNDI, SLP, etc. and JMX access must be made through the lookup service. Most people will never work with a JMX infrastructure large enough to justify the complexity needed for this indirection, but you will need to understand it nonetheless. (We call this phenomenon over-engineering).

JMXMP Connector Details

In contrast to RMI, Sun's generic JMXMP Connector is simple and intuitive. Just compare the rmi URL and the jmxmp URL above. The JMXMP Connector doesn't come with J2SE, but all you have to do is download the latest JMX Remote RI from http://java.sun.com/products/JavaManagement/download.html, pull the file jmxremote_optional.jar from the distribution, and put it in the classpath of your server and client classes. This JMXMP Connector just routes serialized Java objects over a TCP connection. There are two great things about jmxmp. Both server and client just specify the server address and port-- no extra server (like JNDI) is needed. Another great benefit is, JMXMP supports TLS in an intuitive and powerful way, as covered in the TLS chapter.

A Flexible JMXConnectorServer Agent

Now for the server side of the Connector. Just like most JMXConnectorServer programs, ours controls access to an MBeanServer and is therefore a JMX Agent, hence I've named this program ConnectorServerAgent. This is the agent that is used by the demo site web app at http://admc.com/jmxhtml/jmx (where it runs under the Java security manager). You can download this source code from ConnectorServerAgent.java. If you run this class as-is, it will require the classs SampleStd.class and SampleStdMBean.class to be in your runtime classpath. As the JavaDocs in the code explain, you can subclass ConnectorServerAgent to serve any MBeans that you want to. If you want to play with the rest of the sample MBeans from this Howto, or any of your own MBeans which use AbstractDynamicMBean, you'll also need the AbstractDynamicMBean classes in your classpath. (Probably easiest to just put http://admc.com/dist/admcjmx-adaptors.jar into the classpath). The length of this program is only due to TLS support and to make it scalable (so you can easily use it for your own purposes).

Example 8.2. ConnectorServerAgent.java

/*
 * @(#)$Id: ConnectorServerAgent.java 2011 2009-01-09 17:07:43Z blaine $
 *
 * Copyright 2004-2009 Axis Data Management Corp.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import javax.management.MBeanServerFactory;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import javax.management.JMException;
import java.util.HashMap;
import java.util.Map;
import javax.management.ReflectionException;
import java.io.File;

/**
 * Sample Agent that runs a JMXConnectorServer using the specified URL.
 *
 * Subclass and override method loadBeans() to intialize with your own
 * MBeans (or none); and override isRequireClientAuth() if you want to
 * run with TLS but without Client certs.
 *
 * It's useful for debugging (and learning) to be able to run this program
 * in the foreground.  If you don't ever want it to run in the foreground,
 * then override serve() to run this.serve() in a Thread (or similar).
 */
public class ConnectorServerAgent {
    private boolean tlsMode = false;
    private String urlString = null;
    private Map env = new HashMap();

    static public void main(String[] sa)
            throws IOException, JMException {
        boolean tlsMode = false;
        String urlString = null;

        /* Command-line argument parsing */
        if (sa.length == 2 && sa[0].equals("--tls")) {
            tlsMode = true;
            urlString = sa[1];
        } else if (sa.length == 1) {
            urlString = sa[0];
        }
        if (urlString == null) {
            System.err.println(
                "SYNTAX:  java [--tls] ConnectorServerAgent JMXServiceURL\n"
                + "JMXServiceURL examples:\n    "
                + "service:jmx:rmi:///jndi/rmi://localhost:9999/jndi_id\n"
                + "    service:jmx:jmxmp://0.0.0.0:9999\n"
                + "(A JMXMP impl. required in classpath for jmxmp service.\n"
                + "Try Sun's free jmxremote_optional.jar).\n"
                + "(RMI URLs require an RMI registry to be running at the "
                + "specified address/port).");
            System.exit(2);
        }
        new ConnectorServerAgent(urlString, tlsMode).serve();
    }

    protected ConnectorServerAgent(String urlString, boolean tlsMode) {
        this.urlString = urlString;
        this.tlsMode = tlsMode;
    }

    /**
     * Override this class to load your own Beans upon startup, or override
     * with a no-op method to load no beans upon startup (in which case
     * clients will need to add Beans).
     */
    protected void loadBeans(MBeanServer beanServer) throws JMException {
        try {
            beanServer.createMBean("tst.SampleStd", new ObjectName("a:b=c"));
        } catch (ReflectionException re) {
            throw new JMException(
            "The sample MBean class 'tst.SampleStd' is not in your classpath.\n"
                + "Either fix your classpath, or subclass "
                + getClass().getName() + '.');
        }
    }

    /**
     * Initializes the Agent and runs the JMXServerConnector.
     */
    protected void serve() throws JMException, IOException {
        if (tlsMode) setupTls();
        
        MBeanServer mBeanServer = MBeanServerFactory.createMBeanServer();
        loadBeans(mBeanServer);

        (JMXConnectorServerFactory.newJMXConnectorServer(
            new JMXServiceURL(urlString), env, mBeanServer)).start();

        System.out.println(
                "If you're running this server in the foreground, you "
                + "can stop it with Ctrl-C.");
    }

    /**
     * TLS Mode setup.
     */
    protected void setupTls() {
        // Note that the settings below are conditional, so you can
        // override then with "java -Djavax...=Y... ConnectorServerAgent..."
        // It's definitely not safe to use -D to set passwords, though,
        // but it's useful for prototyping.
        if (System.getProperty("javax.net.ssl.trustStore") == null)
            System.setProperty("javax.net.ssl.trustStore",
                    "client1-cert.store");
        if (System.getProperty("javax.net.ssl.keyStorePassword") == null)
            System.setProperty("javax.net.ssl.keyStorePassword",
                    "pwdSstore");
        if (System.getProperty("javax.net.ssl.keyStore") == null)
            System.setProperty("javax.net.ssl.keyStore", "server.store");

        /* The method above is the simplest (IMO therefore the best)
         * method if your application doesn't use certs for any other
         * purpose.  You should use the instance-based TLS configuration
         * method if your app uses certs for any other purpose
         * (i.e., it could fetch web pages over https, or be a TLS
         * Soap client, etc., or it could run multiple TLS
         * JMXConnectorServers).
         *
         * The instance-based method allocates a SSLSocketFactory
         * based on the SSLContext instance which you instantiate and
         * configure, so you can configure multiple SSLSocketFactories
         * with different SSLContext instances.  This all applies to
         * any standard JSSE TLS application, but for JMX, you 
         * associate the allocated SSLSocketFactory to the Connector
         * with:
         * 
         *  env.put("jmx.remote.tls.socket.factory", yourFactory);
         */

        env.put("jmx.remote.profiles", "TLS");
        //env.put("jmx.remote.tls.enabled.protocols", "TLSv1");
        //env.put("jmx.remote.tls.enabled.cipher.suites",
            //"SSL_RSA_WITH_NULL_MD5");
        // Most users will probably want to use the default TLS 
        // protocols and suites.
        env.put("jmx.remote.tls.need.client.authentication",
                Boolean.toString(isRequireClientAuth()));
        // Comment out the line above if you don't want to require
        // clients to have their own certs.

        if ((new File("access.properties")).isFile())
            env.put("jmx.remote.x.access.file", "access.properties");
        // IF file "access.properties" is present in $PWD (from where server
        // is started), it must have keys of permitted client cert subjects,
        // and values of "readwrite" or "readonly".
        // N.b. You MUST ESCAPE all spaces, colons, and equal signes in the
        // subject with backslashes!
        // Example record:
        // CN\=proto\ client\ 1,OU\=RND,O\=Fake\ Corp.,C\=US  readwrite
    }

    /**
     * Only used if running with TLS mode.
     *
     * If you want to run TLS mode without Client certs, just override
     * this class and override this method to return false.
     *
     * @returns true  (unless this method is overridden).
     */
    protected boolean isRequireClientAuth() {
        return true;
    }
}

For the convenience of UNIX users, you can download my startup script from runagent.ksh or script from runagent.bash. You will have to fix the file paths for your environment. You can also modify it to run other programs as daemons. Invoke it like nohup ./runagent.ksh or nohup ./runagent.ksh in order to disassociate it from your login shell.


$Revision: 2036 $