Using a proxy & reflection to access a JMX Standard MBean

A current project uses JMX to monitor the application. Using JMX's Standard MBeans makes publishing an application's health, for example, very easy. This is the "server" side of the story. The "client" side of the story is not so satisfying. The JMX API is designed to support a highly dynamic management environment. To this end, the API uses an indirect access to the data. The client uses textual, untyped descriptors to get, set, and invoke methods on mbeans. Unless you are building a general purpose client this leads to lots of code and mental overhead. For example, if this is your Standard MBean

public interface HugsMBean {
   boolean isHappy();
   int getHugs();
   void addHugs( int hugCount );
}

To use the isHappy() method requires the code

MBeanServerConnection mbeanServerConnection  = ...

ObjectName happyMBeanName = new ObjectName( "com.andrewgilmartin.hugs:name=hugs");
Boolean isHappy = (Boolean) mbeanServerConnection.invoke( happyMBeanName, "isHappy", null, null );
if ( isHappy ) { 
   ...
}

If you are using Standard MBeans to publish data it would be great for your client to also use the Standard MBean to access the data. For example,
MBeanServerConnection mbeanServerConnection  = ...

HugsMBean hugs = ... // i.e. associate with com.andrewgilmartin.hugs:name=hugs
if ( hugs.isHappy() ) {
   ...
}

To this end, below is a little set of helper classes that use Java's reflection and proxy facilities to do just this. The first code we need is an invocation handler that will send attribute and invocation mbean requests between the proxy and the mbean server:

package com.andrewgilmartin.common.management;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import javax.management.Attribute;
import javax.management.JMException;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;

public class MBeanClient implements InvocationHandler {

    private MBeanServerConnection mbeanServerConnection;
    private ObjectName mbeanName;
    
    public MBeanClient( MBeanServerConnection mbeanServerConnection, String mbeanName ) throws JMException {
        this.mbeanServerConnection = mbeanServerConnection;
        this.mbeanName = new ObjectName( mbeanName );
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ( method.getName().startsWith("get") && method.getParameterTypes().length == 0) {
            String attributeName = method.getName().substring(3);
            return mbeanServerConnection.getAttribute( mbeanName, attributeName);
        }
        else if ( method.getName().startsWith("set") && method.getParameterTypes().length == 1) {
            String attributeName = method.getName().substring(3);
            Attribute attribute = new Attribute( attributeName, args[0] );
            mbeanServerConnection.setAttribute(mbeanName, attribute);
            return null;
        }
        else {
            return mbeanServerConnection.invoke(mbeanName, method.getName(), args, null);
        }
    }
}

And now we need a factory (or perhaps just a static creator method somewhere) to tie together the Standard MBean interface, the MBeanClient helper, and the proxy:
package com.andrewgilmartin.common.management;

import java.lang.reflect.Proxy;
import javax.management.JMException;
import javax.management.MBeanServerConnection;

public class MBeanClientFactory {

    private MBeanServerConnection mbeanServerConnection;

    public MBeanClientFactory( MBeanServerConnection mbeanServerConnection ) {
        this.mbeanServerConnection = mbeanServerConnection;
    }

    public  T create( String objectName, Class... mbeanInterfaces ) throws JMException {
        T mbeanClient = (T) Proxy.newProxyInstance(
         this.getClass().getClassLoader(),
         mbeanInterfaces,
         new MBeanClient( mbeanServerConnection, objectName ) );
        return mbeanClient;
    }
}

Now, connect the client and create the proxy

// connect to the mbean server
MBeanServerConnection mbeanServerConnection = ...

// create the mbean client factory
MBeanClientFactory clientFactory = new MBeanClientFactory(mbeanServerConnection);

// create the mbean client for the server's mbean
HugsMBean hugs = clientFactory.create("com.andrewgilmartin.hugs:name=hugs",HugsMBean.class);

// use the mbean client
while ( ! hugs.isHappy() ) {
  hugs.addHugs( 27 );
}
System.out.println( "happy with " + hugs.getHugs() + " hugs");

Post script: Here is how to connect to a JMX server running on localhost at port 9999 using RMI:

JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi");
JMXConnector jmxc = JMXConnectorFactory.connect(url, null);
MBeanServerConnection mbeanServerConnection = jmxc.getMBeanServerConnection();