Skip to main content

Applying MXBean mappings to your own types

Posted by emcmanus on November 21, 2008 at 5:50 AM PST

The MXBean
framework
gives you a way to define MBeans with custom
types, and map those types to standard
("open")
types so generic clients can deal with them. Sometimes you want
to do this mapping outside an MBean. For example, we recommend
that the custom payload
("userData")
of
a Notification
should use open types, but open values can be a bit painful to
construct. It would be nice to use the MXBean framework to map
from a custom payload type to an open type. Is there a way to
do that?

Let's take an example. Suppose you are tracking coordinates read
from some device, and you have a notification that indicates that
an (x,y) value has been read that was outside the expected bounds.
You'd like the notification to include the value that was
read.

In the JMX 2.0 API, you can do this easily, using interfaces I
described before:

    public class Coordinate {
        @ConstructorProperties({"x", "y"})
        public Coordinate(int x, int y) {...}
        public int getX() {...}
        public int getY() {...}
    }

    MXBeanMapping coordinateMapping =
        MXBeanMappingFactory.        -->DEFAULT.        -->mappingForType(Coordinate.class, MXBeanMappingFactory.DEFAULT);

    Coordinate outOfBounds = ...;
    Notification notif =         -->new Notification(Coordinate.class.getName(), source, seqNo++);
    notif.setUserData(coordinateMapping.toOpenValue(outOfBounds));
 

Though you don't have to, you can also make the corresponding
OpenType available to clients, by putting it in
the Descriptor
of the
corresponding MBeanNotificationInfo.
The Descriptor
field openType
is defined with this meaning, so the code would look like this:

    OpenType<?> coordinateOpenType = coordinateMapping.getOpenType();
    Descriptor notifDescr =         -->        -->new ImmutableDescriptor(
            new String[] {
                "openType",
            }, new Object[] {
                coordinateOpenType
            });
    MBeanNotificationInfo mbni = new MBeanNotificationInfo(
        new String[] {"com.example.bounds.exceeded"},
        Notification.class.getName(),
        "Read a coordinate value outside the expected bounds",
        notifDescr);
 

That's all very well, but the JMX 2.0 API will only be available
in the JDK 7 platform. What if you want to do this today?

The good news is that it is possible, although it requires
writing some throwaway classes (or generating them at runtime, as I
discussed
previously).
I'll show some general-purpose
code later, but for now here's a simple
way you could do it. Create an MXBean with an attribute X of type
Coordinate, register it in an MBeanServer (perhaps one created
specially for the purpose), then arrange for the original value of
X to be the Coordinate you want to convert. Calling
getAttribute(objectName, "X") should then give you
the converted value, and you can get the OpenType by looking at
getMBeanInfo(objectName).getAttributes()[0].

But actually, that's overkill. You don't need to register an MXBean at all. You can simply use the StandardMBean class to create a private MXBean, and do the same trick. For example, you could write this:

    public interface CoordinateWrapperMXBean {
        public Coordinate getX();
    }
    public class CoordinateWrapper implements CoordinateWrapperMXBean {
        public Coordinate x;
        public Coordinate getX() {
            return x;
        }
    }
    CoordinateWrapper coordinateWrapper = new CoordinateWrapper();
    StandardMBean coordinateMBean = new StandardMBean(
        coordinateWrapper, CoordinateWrapperMXBean.class, true);

    Coordinate outOfBounds = ...;
    coordinateWrapper.x = outOfBounds;
    Object userData = coordinateMBean.getAttribute("X");
 

That's kind of unwieldy, and it's not thread-safe as written, but
you can see the general idea.

A general-purpose MXBean conversion class

To reduce the unwieldiness, we can generalize this for any type,
in such a way that you only need to write this...

    public interface CoordinateCaller extends Callable<Coordinate> {
        public Coordinate call();
    }
 

...and you can feed it to a simple conversion framework that I'll
show below, like this:

    MXBeanConverter coordinateConverter =
        MXBeanConverter.getInstance(CoordinateCaller.class);
    Coordinate outOfBounds = ...;
    Object userData = coordinateConverter.toOpenValue(outOfBounds);
 

You can also get the OpenType like this:

    OpenType<?> coordinateOpenType = coordinateConverter.getOpenType();
 

And you can convert in the opposite direction (for example, when
you receive the Notification and you want to reconstruct the
original Coordinate object) like this:

    Coordinate outOfBounds =
        coordinateConverter.fromOpenValue(notification.getUserData());
 

If you only have a handful of types that you need to convert,
having to create a FooCaller interface for each one shouldn't be
too big a burden. If you have a huge number of types, then it
would be worth looking at generating each FooCaller interface at
runtime, for example
using ASM. (Actually, I'd
recommend generating an interface with getX and setX methods
instead.)

By the way, if you look closely at the CoordinateCaller
interface...

    public interface CoordinateCaller extends Callable<Coordinate> {
        public Coordinate call();
    }
 

...you might reasonably wonder why you need to define
the call() method when
a Callable<Coordinate> already has one. Ideally
you would not have to, but this is a shortcoming of the current
MXBean framework.

The approach used here is a variant on
the getAttribute("X") approach I explained above.
Since we're using a call() method and not
a getX() method, we need to use invoke
and not getAttribute on the StandardMBean.

To do the reverse conversion, we make
an MXBean
proxy
for the CoordinateCaller, and arrange for its call
to MBeanServerConnection.invoke
to return the open-type value, so that the proxy will convert it
back into a Coordinate that it returns from
its call() method. (Don't worry if you didn't
understand that; you don't need to in order to use the
converter.)

OK, so here's the code:

package mxbeanconverter;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.concurrent.Callable;
import javax.management.JMX;
import javax.management.MBeanServer;
import javax.management.MBeanServerDelegate;
import javax.management.StandardMBean;
import javax.management.openmbean.OpenType;

public class MXBeanConverter<T> {
    private final StandardMBean mbean;
    private final Callable<T> proxy;
    private final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>();

    public OpenType getOpenType() {
        return (OpenType) mbean.getMBeanInfo().getOperations()[0]
                .getDescriptor().getFieldValue("openType");
    }

    public Object toOpenValue(T javaValue) {
        threadLocal.set(javaValue);
        try {
            return mbean.invoke("call", null, null);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
            // Should not happen
        }
    }

    public T fromOpenValue(Object openValue) {
        threadLocal.set(openValue);
        try {
            return proxy.call();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
            // Should not happen
        }
    }

    private MXBeanConverter(Class<? extends Callable<T>> intf) {
        mbean = makeMBean(intf);
        proxy = makeProxy(intf);
    }

    public static <T> MXBeanConverter<T> getInstance(Class<? extends Callable<T>> intf) {
        return new MXBeanConverter(intf);
    }

    private <I extends Callable<T>> StandardMBean makeMBean(Class<I> intf) {
        I impl = intf.cast(Proxy.newProxyInstance(
                intf.getClassLoader(), new Class<?>[] {intf}, threadLocalIH));
        return new StandardMBean(impl, intf, true);
    }

    private <I extends Callable<T>> I makeProxy(Class<I> intf) {
        MBeanServer threadLocalMBS = (MBeanServer) Proxy.newProxyInstance(
                MBeanServer.class.getClassLoader(),
                new Class<?>[] {MBeanServer.class},
                threadLocalIH);
        return intf.cast(JMX.newMXBeanProxy(
                threadLocalMBS, MBeanServerDelegate.DELEGATE_NAME, intf));
    }

    // An InvocationHandler that returns threadLocal.get() from every method.
    // This means that we can make an implementation of FooCallable where
    // the call() method returns threadLocal.get(), and it also means that
    // we can make an implementation of MBeanServer where
    // invoke(...any parameters...) returns threadLocal.get().
    private class GetThreadLocalIH implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) {
            return threadLocal.get();
        }
    }
    private final InvocationHandler threadLocalIH = new GetThreadLocalIH();
}

[Tags:


]

Comments

Nice article! If the only reason you want to do these conversions is for creating the payload for AttributeChangeNotifications, with the old and new attribute values in open-data format, then there's an more specific helper class for this, that converts the values too, described in my blog: http://blogs.sun.com/nickstephen/entry/a_helper_class_for_sending