Skip to main content

Creating type-safe MBean proxies

Posted by emcmanus on July 6, 2006 at 10:04 AM PDT

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 href="http://download.java.net/jdk6/doc/api/javax/management/ObjectName.html">ObjectName,
say somedomain:type=Cache. Then you might make a
proxy like this:

CacheMBean proxy = href="http://download.java.net/jdk6/doc/api/javax/management/JMX.html#newMBeanProxy(javax.management.MBeanServerConnection,%20javax.management.ObjectName,%20java.lang.Class)">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 UndeclaredThrowableException about?
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 href="http://download.java.net/jdk6/doc/api/java/lang/reflect/UndeclaredThrowableException.html">UndeclaredThrowableException comes about
because the method you called, CacheMBean.getSize(),
doesn't declare the exception
href="http://download.java.net/jdk6/doc/api/javax/management/InstanceNotFoundException.html">InstanceNotFoundException in its
throws
clause. In other words, it is "not declared as throwable", which
is mangled into the barely comprehensible name
UndeclaredThrowableException 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 written
CacheMBean 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 an
IOException so that wouldn't have fixed the problem
we saw above. You could reasonably add href="http://download.java.net/jdk6/doc/api/javax/management/JMException.html">JMException
to the throws of every method too, and then you
would see InstanceNotFoundException directly.
JMException is the parent class of
InstanceNotFoundException.

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 href="http://download.java.net/jdk6/doc/api/javax/management/AttributeNotFoundException.html">AttributeNotFoundException
from proxy.getSize() if the MBean
somedomain: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 InstanceNotFoundException
right there and then if the MBean called
objectName doesn't exist.

We make a call to href="http://download.java.net/jdk6/doc/api/javax/management/MBeanServerConnection.html#getObjectInstance(javax.management.ObjectName)">getObjectInstance
because it's about the simplest MBeanServer operation you can do
that will throw InstanceNotFoundException if the
MBean doesn't exist and do nothing if it does.

The magic with <T> basically means "if the
intfClass parameter is
CacheMBean.class then the return type is
CacheMBean". This is because the type of
CacheMBean.class is
Class<CacheMBean>. But it's black
magic, as usual with Java generics, tainted by the demon href="http://gafter.blogspot.com/2004/09/puzzling-through-erasure-answer.html">Erasure.
That's why we write intfClass.cast(blah)
rather than just (T) blah in the last
line. Casting to T 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 the right one. Nothing is stopping you from
creating a CacheMBean proxy for an MBean that is
actually a NoddyInToylandMBean. 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 href="http://download.java.net/jdk6/doc/api/javax/management/MBeanServer.html#isInstanceOf(javax.management.ObjectName,%20java.lang.String)">MBeanServer.isInstanceOf
to check that the MBean does indeed implement the
CacheMBean interface. isInstanceOf
throws InstanceNotFoundException, so we could
simply replace the mbsc.getObjectInstance(name) in
ExistentJMXProxy.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
CacheMBean interface. 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 class javax.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 href="http://download.java.net/jdk6/doc/api/javax/management/MBeanInfo.html">MBeanInfo
corresponding to the proxy interface (CacheMBean) and
compare it against the MBeanInfo from the MBean we
want to proxy. Every readable attribute in the proxy's
MBeanInfo must have a corresponding readable
attribute in the MBean's MBeanInfo. Every
writeable attribute must have a corresponding writeable
attribute. Every operation must have a corresponding
operation.

We don't have to require the two
MBeanInfos 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 class
TypeSafeJMXProxy with a method
newMBeanProxy 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()
and proxy.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 href="http://download.java.net/jdk6/doc/api/javax/management/StandardMBean.html">javax.management.StandardMBean.
StandardMBean implements the href="http://download.java.net/jdk6/doc/api/javax/management/DynamicMBean.html">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 href="http://www-128.ibm.com/developerworks/java/library/j-jtp08305.html">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 href="http://download.java.net/jdk6/doc/api/java/lang/reflect/Proxy.html#newProxyInstance(java.lang.ClassLoader,%20java.lang.Class[],%20java.lang.reflect.InvocationHandler)">Proxy.newProxyInstance
(see above) is where this
actually gets used.

OK, we've got the MBeanInfo corresponding to
our CacheMBean 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 what
checkMBeanInfos 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 in
proxyInfo against every attribute in
mbeanInfo, 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 an
MBeanAttributeInfo or
MBeanOperationInfo to 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 a ClassLoader. The
best we can do is to use the ClassLoader 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 of
JMX.newMBeanProxy.

Acknowledgement

The idea for this article came from an e-mail exchange with
href="http://java.sun.com/developer/community/chat/JavaLive/2004/jl0520.html">Sanjay
Radia, now at href="http://www.cassatt.com/">Cassatt.

Related Topics >>