MBean proxies allow you to access an MBean through a Java interface, writing proxy.getFoo() instead of mbeanServer.getAttribute(name, "Foo"). But when you create a proxy, there is no check that the MBean actually matches the interface you specify, or even that the MBean exists. Why is that, and what can you do about it?

Here's a more concrete example. Suppose you have an MBean interface like this:

public interface CacheMBean {
    public int getSize();
    public void setSize(int x);
    public void dropOldest(int nEntries);
}

Also suppose that you have registered an MBean answering to that interface with a certain ObjectName, saysomedomain:type=Cache. Then you might make a proxy like this:

CacheMBean proxy = JMX.newMBeanProxy(mbeanServer, objectName, CacheMBean.class);

Well, that's the neato Mustang (Java SE 6) version. If you're using an earlier version, like Tiger (J2SE 5.0), then it's a bit more verbose:

CacheMBean proxy = (CacheMBean)
    MBeanServerInvocationHandler.newProxyInstance(mbeanServer, objectName,
                                                  CacheMBean.class, false);

(Of course that will continue to work on Mustang, but the first version is so much nicer that you'll want to use it if you can.)

Either way, this allows you to write things like:

proxy.setSize(proxy.getSize() * 2);
proxy.dropOldest(25);

instead of the code you would have to write without the proxy:

int size = mbeanServer.getAttribute(objectName, "Size");
mbeanServer.setAttribute(objectName, new Attribute("Size", size * 2));
mbeanServer.invoke(objectName, "dropOldest", new Object[] {25},
                   new String[] {"int"});

It's clear that the version with the proxy is much simpler to read and write, and much safer too since the compiler will check that the methods you are invoking are indeed in the interface and that you are using the right types. That's why we recommend using proxies like this when at all possible.

Poxy proxy

But all is not completely rosy. What happens for example if the MBean doesn't exist? You might expect that creating the proxy would fail in that case. But it doesn't. You can go right ahead and make your proxy, and then when you call proxy.getSize()you'll get an exception like this:

Exception in thread "main" java.lang.reflect.UndeclaredThrowableException
        at $Proxy0.getSize(Unknown Source)
        at typesafeproxy.Test.main(Test.java:37)
Caused by: javax.management.InstanceNotFoundException: somedomain:type=Cache
        at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.getMBean(DefaultMBeanServerInterceptor.java:1094)
        at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.getAttribute(DefaultMBeanServerInterceptor.java:662)
        at com.sun.jmx.mbeanserver.JmxMBeanServer.getAttribute(JmxMBeanServer.java:638)
        at javax.management.MBeanServerInvocationHandler.invoke(MBeanServerInvocationHandler.java:262)
        ... 2 more

You'll immediately wonder

  • why is it only telling me now?
  • what's that UndeclaredThrowableExceptionabout?

JMX.newMBeanProxy doesn't check whether the MBean you name exists for the simple reason that even if it does exist at that point, there's no guarantee that it will still exist when you come to make a call through your proxy. So your code needs to be prepared to deal with that eventuality anyway.

The UndeclaredThrowableException comes about because the method you called, CacheMBean.getSize(), doesn't declare the exception InstanceNotFoundException in itsthrows clause. In other words, it is "not declared as throwable", which is mangled into the barely comprehensible nameUndeclaredThrowableException you see here.

We generally recommend that you declare throws IOException on every method in an MBean interface so that you will be forced to catch any network problems that arise when you make a remote call through a proxy. So we should have writtenCacheMBean like this:

public interface CacheMBean {
    public int getSize() throws IOException;
    public void setSize(int x) throws IOException;
    public void dropOldest(int nEntries) throws IOException;
}

However, InstanceNotFoundException is not anIOException so that wouldn't have fixed the problem we saw above. You could reasonably add JMException to the throws of every method too, and then you would seeInstanceNotFoundException directly.JMException is the parent class ofInstanceNotFoundException.

JMException is also the parent class of a number of other exceptions that you might see when you are accessing an MBean, whether through a proxy or not. For example, you might get an AttributeNotFoundException fromproxy.getSize() if the MBeansomedomain:type=Cache does exist but it doesn't contain an attribute called Size. Again, we could have checked that every attribute and operation in the proxy existed at the time we were creating the proxy, but that wouldn't guarantee that the MBean wouldn't later be replaced by another one where some attributes have been removed.

Pesky proxy

Nevertheless, usually you do know that your MBeans aren't suddenly going to disappear or delete their attributes, and in that case you might be happier if the checks were made upfront when the proxy is created. Can we achieve that somehow?

The answer is that we can. To start with, here's a simple class that defines a method that creates an MBean proxy, but throws an exception if the proxied MBean doesn't exist.

public class ExistentJMXProxy {
    private ExistentJMXProxy() {}  // no instances of this class

    public static <T> T newMBeanProxy(
        MBeanServerConnection mbsc,
        ObjectName name,
        Class<T> intfClass)
        throws IOException, InstanceNotFoundException {

        // Provoke IOException or InstanceNotFoundException now
        // rather than later
        mbsc.getObjectInstance(name);

        return intfClass.cast(
            MBeanServerInvocationHandler.newProxyInstance(
                mbsc, name, intfClass, false));
    }
}

Now you can write
CacheMBean proxy = ExistentJMXProxy.newMBeanProxy(
    mbeanServer, objectName, CacheMBean.class);
and you will get an InstanceNotFoundExceptionright there and then if the MBean called objectNamedoesn't exist.

We make a call to getObjectInstance because it's about the simplest MBeanServer operation you can do that will throwInstanceNotFoundException if the MBean doesn't exist and do nothing if it does.

The magic with <T> basically means "if theintfClass parameter is CacheMBean.classthen the return type is CacheMBean". This is because the type of CacheMBean.class isClass<CacheMBean>. But it's blackmagic, as usual with Java generics, tainted by the demon Erasure. That's why we writeintfClass.cast(blah) rather than just(T) blah in the last line. Casting toT would produce the dreaded warning: [unchecked] unchecked cast from the compiler.

Prickly proxy

We might be satisfied with our existence check, but in fact just because an MBean exists with the name we gave doesn't mean it's theright one. Nothing is stopping you from creating aCacheMBean proxy for an MBean that is actually aNoddyInToylandMBean. Once again, you'll only find out about it when you actually try to use the proxy. The MBean might even have some of the right attributes but not others (it's an older version, say), so the nasty surprise might be significantly delayed. What we really want is a check that every method we might call on the proxy will be valid on the target MBean.

If you're very familiar with the JMX API, you might think that a good and simple way to make this check would be to use MBeanServer.isInstanceOf to check that the MBean does indeed implement the CacheMBean interface.isInstanceOf throwsInstanceNotFoundException, so we could simply replace the mbsc.getObjectInstance(name) inExistentJMXProxy.newMBeanProxy with this:

if (!mbsc.isInstanceOf(name, intfClass.getName()))
    throw new InstanceNotFoundException("Wrong type MBean: " + name);

But actually that isn't a great idea. It will work if the MBean is a Standard MBean that implements the CacheMBeaninterface. But it won't work if the MBean is a Dynamic MBean, even if it exports the exact same attributes and operations that a Standard MBean would. And it won't work if you use the classjavax.management.StandardMBean to create a customized Standard MBean; if you've read other entries in this blog you'll know I'm very fond of doing that.

The problem is that isInstanceOf is making a test on the Java class providing the implementation of the MBean. We don't really care about that. What we really want to know is whether all of the methods we can call on the proxy will work.

Prolix proxy

So what we want to do is to check, when creating the proxy, that the named MBean exists, and that it has all the attributes and operations that the proxy can access. How might we go about doing that?

The simplest way is to generate the MBeanInfo corresponding to the proxy interface (CacheMBean) and compare it against theMBeanInfo from the MBean we want to proxy. Every readable attribute in the proxy's MBeanInfo must have a corresponding readable attribute in the MBean'sMBeanInfo. Every writeable attribute must have a corresponding writeable attribute. Every operation must have a corresponding operation.

We don't have to require the twoMBeanInfos to be identical. The MBean might have additional attributes and operations that we won't be able to access through the proxy, and there's no problem with that. Also, the attribute and operation types don't have to match exactly: the real type of an attribute might be a subclass of the type that the proxy expects, and that's OK provided the attribute is a read-only one. Likewise, the return type of an operation might be a subclass of the type that the proxy expects.

So let's look at some code. We're going to make a classTypeSafeJMXProxy with a methodnewMBeanProxy that will only create a proxy if the target MBean exists and exports the right attributes and operations. If you write
CacheMBean proxy = TypeSafeJMXProxy.newMBeanProxy(
   mbeanServer, objectName, CacheMBean.class);
then you can be sure that proxy.getSize() andproxy.setSize(n) and proxy.dropOldest(n)will all work, assuming the MBean doesn't disappear or mutate in the meantime.

public class TypeSafeJMXProxy {

    /** There are no instances of this class. */
    private TypeSafeJMXProxy() {
    }

    /**
     * Create an MBean proxy, checking that the target MBean exists
     * and implements the attributes and operations defined by the
     * given interface.
     *
     * @param mbsc the MBean Server in which the proxied MBean is registered.
     * @param name the ObjectName under which the proxied MBean is registered.
     * @param intfClass the MBean interface that the proxy will
     * implement by forwarding its methods to the proxied MBean.
     *
     * @return The newly-created proxy.
     *
     * @throws IOException if there is a communication problem when
     * connecting to the {@code MBeanServerConnection}.
     * @throws InstanceNotFoundException if there is no MBean
     * registered under the given {@code name}.
     * @throws NotCompliantMBeanException if {@code intfClass} is
     * not a valid MBean interface.
     * @throws NoSuchMethodException if a method in
     * {@code intfClass} does not correspond to an attribute or
     * operation in the proxied MBean.
     */
    public static <T> T newMBeanProxy(
            MBeanServerConnection mbsc,
            ObjectName name,
            Class<T> intfClass)
            throws IOException, InstanceNotFoundException,
                   NotCompliantMBeanException, NoSuchMethodException {

        // Get the MBeanInfo, or throw InstanceNotFoundException
        final MBeanInfo mbeanInfo;
        try {
            mbeanInfo = mbsc.getMBeanInfo(name);
        } catch (InstanceNotFoundException e) {
            throw e;
        } catch (JMException e) {
            // IntrospectionException or ReflectionException:
            // very improbable in practice so just pretend the MBean wasn't there
            // but keep the real exception in the exception chain
            final String msg = "Exception getting MBeanInfo for " + name;
            InstanceNotFoundException infe = new InstanceNotFoundException(msg);
            infe.initCause(e);
            throw infe;
        }

        // Construct the MBeanInfo that we would expect from a Standard MBean
        // implementing intfClass.  We need a non-null implementation of intfClass
        // so we create a proxy that will never be invoked.
        final T impl = intfClass.cast(Proxy.newProxyInstance(
                intfClass.getClassLoader(), new Class<?>[] {intfClass}, nullIH));
        final StandardMBean mbean = new StandardMBean(impl, intfClass);
        final MBeanInfo proxyInfo = mbean.getMBeanInfo();

        checkMBeanInfos(intfClass.getClassLoader(), proxyInfo, mbeanInfo);
        return intfClass.cast(MBeanServerInvocationHandler.newProxyInstance(
                mbsc, name, intfClass, false));
    }

The trick we use to get the MBeanInfo for our MBean interface (CacheMBean) is to create an MBean for it locally using the ever-useful javax.management.StandardMBean.StandardMBean implements the DynamicMBean interface (confusingly enough), so we can just call getMBeanInfo() on that interface and then throw the MBean away.

But to create the MBean we need an object that implements the MBean interface. We can use a dynamic proxy to get that object. No method in the object will ever actually be called, since we throw away the MBean as soon as we've extracted its MBeanInfo. So we can just make a proxy that implements the interface by returning null from all of its methods:

    private static class NullInvocationHandler implements InvocationHandler {
        public Object invoke(Object proxy, Method method, Object[] args) {
            return null;
        }
    }
    private static final NullInvocationHandler nullIH =
        new NullInvocationHandler();

The call to Proxy.newProxyInstance (see above) is where this actually gets used.

OK, we've got the MBeanInfo corresponding to ourCacheMBean interface (proxyInfo), and we've got the MBeanInfo of the target MBean. Now we need to check that they are compatible as we described above. That's whatcheckMBeanInfos will do.

    private static void checkMBeanInfos(
            ClassLoader loader, MBeanInfo proxyInfo, MBeanInfo mbeanInfo)
            throws NoSuchMethodException {

        // Check that every attribute accessible through the proxy is present
        // in the MBean.
        MBeanAttributeInfo[] mais = mbeanInfo.getAttributes();
    attrcheck:
        for (MBeanAttributeInfo pai : proxyInfo.getAttributes()) {
            for (MBeanAttributeInfo mai : mais) {
                if (compatibleAttributes(loader, pai, mai))
                    continue attrcheck;
            }
            final String msg =
                    "Accessing attribute " + pai.getName() + " would fail";
            throw new NoSuchMethodException(msg);
        }

        // Check that every operation accessible through the proxy is present
        // in the MBean.
        MBeanOperationInfo[] mois = mbeanInfo.getOperations();
    opcheck:
        for (MBeanOperationInfo poi : proxyInfo.getOperations()) {
            for (MBeanOperationInfo moi : mois) {
                if (compatibleOperations(loader, poi, moi))
                    continue opcheck;
            }
            final String msg =
                    "Accessing operation " + poi.getName() + " would fail";
            throw new NoSuchMethodException(msg);
        }
    }

Notice that we're comparing every attribute inproxyInfo against every attribute inmbeanInfo, so the execution time is quadratic in the number of attributes, and likewise for operations. We could improve this, but it is not as simple as it might seem (consider overloaded operations, for example), and the number of attributes or operations is rarely big enough to justify a more complicated algorithm.

For every attribute in proxyInfo, there must be a compatible attribute somewhere in mbeanInfo, determined as follows:

  • the name of the attribute must be the same;
  • if the proxy attribute is readable then the MBean attribute must be readable too;
  • if the proxy attribute is writeable then the MBean attribute must be writeable too;
  • if the proxy attribute is writeable then the MBean attribute must have exactly the same type;
  • if the proxy attribute is not writeable then the MBean attribute's type can also be a subclass of the proxy attribute's type.

These rules allow the MBean to have an attribute that is read/write even though the proxy is read-only or write-only. They are a bit too strict, in that we could allow the type of a write-only attribute in the proxy to be a subclass of the type in the MBean; but write-only attributes hardly ever occur so we ignore that case.

    private static boolean compatibleAttributes(
            ClassLoader loader,
            MBeanAttributeInfo proxyAttrInfo, MBeanAttributeInfo mbeanAttrInfo) {
        if (!proxyAttrInfo.getName().equals(mbeanAttrInfo.getName()))
            return false;
        if (!proxyAttrInfo.getType().equals(mbeanAttrInfo.getType())) {
            if (proxyAttrInfo.isWritable())
                return false; // type must be identical
            if (!isAssignable(loader,
                              proxyAttrInfo.getType(), mbeanAttrInfo.getType()))
                return false;
        }
        if (proxyAttrInfo.isReadable() && !mbeanAttrInfo.isReadable())
            return false;
        if (proxyAttrInfo.isWritable() && !mbeanAttrInfo.isWritable())
            return false;
        return true;
    }

Similar logic applies for operations. The return type of an operation in the MBean can be a subclass of the return type in the proxy but otherwise everything must match exactly.

    private static boolean compatibleOperations(
            ClassLoader loader,
            MBeanOperationInfo proxyOpInfo, MBeanOperationInfo mbeanOpInfo) {
        if (!proxyOpInfo.getName().equals(mbeanOpInfo.getName()) ||
                !isAssignable(loader,
                              proxyOpInfo.getReturnType(),
                              mbeanOpInfo.getReturnType()))
            return false;
        MBeanParameterInfo[] proxyParams = proxyOpInfo.getSignature();
        MBeanParameterInfo[] mbeanParams = mbeanOpInfo.getSignature();
        if (proxyParams.length != mbeanParams.length)
            return false;
        for (int i = 0; i < proxyParams.length; i++) {
            if (!proxyParams[i].getType().equals(mbeanParams[i].getType()))
                return false;
        }
        return true;
    }

Finally, we have to define what it means for a type in anMBeanAttributeInfo or MBeanOperationInfoto be a subclass of another such type, given that types are expressed as strings. Two type strings are obviously compatible if they are equal, but otherwise we must convert those strings into classes that we can compare. To do that we need aClassLoader. The best we can do is to use theClassLoader of the proxy interface. So we allow subclasses if they are known to that ClassLoader.

    private static boolean isAssignable(
            ClassLoader loader, String toClassName, String fromClassName) {
        if (toClassName.equals(fromClassName))
            return true;
        try {
            Class<?> toClass = Class.forName(toClassName, false, loader);
            Class<?> fromClass = Class.forName(fromClassName, false, loader);
            return toClass.isAssignableFrom(fromClass);
        } catch (ClassNotFoundException e) {
            // Could not load one of the two classes so consider not assignable
            // In real code we might like to log the exception
            return false;
        }
    }
}

Perplexing proxy

With that considerable wodge of code, we can now make proxies and be sure they will work. So long as the MBean they're connected to doesn't budge, anyway. In a future version of the JMX API, we may add this functionality to an alternative form ofJMX.newMBeanProxy.

Acknowledgement

The idea for this article came from an e-mail exchange with Sanjay Radia, now at Cassatt.