The Source for Java Technology Collaboration
User: Password:



Eamonn McManus

Eamonn McManus's Blog

Adding Descriptors to MBeans in Tiger

Posted by emcmanus on June 08, 2006 at 09:34 AM | Comments (0)

Mustang (Java SE 6) includes the ability to give additional information about MBeans to management clients via Descriptors, as I described previously. But what if you are not yet able to migrate to the Mustang platform? As I hinted in that previous entry, all is not lost. You can still use Descriptors, though it's more work.

We often hear from developers that they'd dearly love to move to the latest and greatest Java SE platform, but they can't. Their app server isn't yet supported on that platform; or their giant mission-critical financial application would require an enormous test campaign on the new platform, that the QA department doesn't have the resources to do; or their pointy-haired boss fears the unknown or doesn't see what the fuss is all about. So if there's some way to make the shiny new features available on older platforms, these developers are all for it!

Descriptors

Descriptors allow you to give additional information about MBeans to management clients. For example, a Descriptor on an MBean attribute might say what units it is measured in, or what its minimum and maximum possible values are. As of Mustang, Descriptors are a basic part of the JMX API and are available in all types of MBeans. In particular, the Mustang version of the jconsole tool will display an MBean's descriptors, as the following screenshot illustrates:

JConsole screenshot showing MBean with its Descriptor information

The descriptor here includes some standard descriptor fields, namely immutableInfo, interfaceClassName, and mxbean; as well as a custom field author.

You can perfectly well use Mustang jconsole to connect to a JMX agent running on Tiger (J2SE 5.0). (You can even connect to an earlier J2SE version such as 1.4, though in that case you will need to have the JMX and JMX Remote API classes in the classpath of your application.) So even if you can't migrate your application to Mustang, it's perfectly feasible to install a second JDK, the Mustang one, and just use its jconsole to connect to your application running on Tiger. Likewise you could write your own management client running on Mustang, even though your application is still running on Tiger.

So if you could somehow arrange for your MBean to contain descriptors in Tiger, then you could show them with your Mustang jconsole or access them in your Mustang management client. And this is in fact possible.

Descriptors on Tiger

The trick is that descriptors have always existed in the JMX API, but only in conjunction with Model MBeans. Model MBeans are an advanced (not to say obscure) part of the API, so you probably haven't paid much attention to them. But the key point here is that the Model MBean package defines a set of subclasses of the standard metadata classes such as MBeanInfo and MBeanAttributeInfo. For example, ModelMBeanAttributeInfo is just like MBeanAttributeInfo except that it adds a descriptor. Prior to Mustang, MBeanAttributeInfo did not have a descriptor, but ModelMBeanAttributeInfo did. Since ModelMBeanAttributeInfo is a subclass of MBeanAttributeInfo, you can use a ModelMBeanAttributeInfo wherever an MBeanAttributeInfo is expected. So in particular if you have a plain MBean, for example a Standard MBean, you can change its MBeanInfo so that it contains an array of ModelMBeanAttributeInfo rather than just MBeanAttributeInfo.

Suppose we want to create a Standard MBean, but with descriptor fields as in the snapshot above. Here's how we might go about it.

First, we create a subclass of StandardMBean called DescriptorStandardMBean that allows us to supply the extra descriptor fields when the MBean is constructed. So the MBean registration might look like this:

MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName name = new ObjectName("com.example.MyApp:type=Test");
Map<String, Object> descriptorMap = new HashMap<String, Object>();
descriptorMap.put("author", "Éamonn McManus");
descriptorMap.put("immutableInfo", "true");
descriptorMap.put("interfaceClassName", TestMBean.class.getName());
descriptorMap.put("mxbean", "false");
Test test = new Test();
Object mbean = new DescriptorStandardMBean(test, TestMBean.class, descriptorMap);
mbs.registerMBean(mbean, name);

Here's what the DescriptorStandardMBean class might look like. It will have two constructors, corresponding to the two constructors of the parent class StandardMBean, but adding an extra Map parameter that will contain the extra descriptor fields. So it starts off like this:

public class DescriptorStandardMBean extends StandardMBean {

    private final Map<String, ?> mbeanDescriptorMap;

    public <T> DescriptorStandardMBean(
            T impl, Class<T> intf,
            Map<String, ?> mbeanDescriptorMap)
            throws NotCompliantMBeanException {
        super(impl, intf);
    	this.mbeanDescriptorMap = mbeanDescriptorMap;
    }
    
    public DescriptorStandardMBean(
            Class<?> intf,
            Map<String, ?> mbeanDescriptorMap)
            throws NotCompliantMBeanException {
        super(intf);
    	this.mbeanDescriptorMap = mbeanDescriptorMap;
    }

The <T> voodoo incantation on the first constructor says that if the second parameter is TestMBean.class, say, then the first parameter must be an object of that type or a subtype. That is, the declared type of the parameter must be TestMBean or a subtype (typically a class that implements that interface). You can see that this is true of our registration code above, assuming the Test class implements the TestMBean interface following the usual Standard MBean conventions. (The corresponding StandardMBean constructor uses the same generics voodoo in Mustang, though not in Tiger.)

The mbeanDescriptorMap is declared as a Map<String, ?> rather than a Map<String, Object> because the caller might want to give it a Map<String, String>, say. In fact, we could have used a Map<String, String> in the registration code, because all the descriptor entries were in fact strings. One of the subtleties of generics is that you can't assign a List<String> to a List<Object>, and likewise you can't assign a Map<String, String> to a Map<String, Object>. But you can assign if Object is replaced by ?.

Now let's look at how we attach our descriptor to the MBean's MBeanInfo:

    @Override
    public MBeanInfo getMBeanInfo() {
        MBeanInfo mbi = super.getMBeanInfo();
        if (mbi instanceof ModelMBeanInfoSupport)  // we already rewrote it
            return mbi;

        Descriptor d = new DescriptorSupport();
        for (Map.Entry<String, ?> entry : mbeanDescriptorMap.entrySet())
            d.setField(entry.getKey(), entry.getValue());
        d.setField("descriptorType", "mbean");
        if (d.getFieldValue("name") == null)
            d.setField("name", mbi.getClassName());
        mbi = new ModelMBeanInfoSupport(
                mbi.getClassName(),
                mbi.getDescription(),
                makeModelMBeanAttributeInfos(mbi.getAttributes()),
                makeModelMBeanConstructorInfos(mbi.getConstructors()),
                makeModelMBeanOperationInfos(mbi.getOperations()),
                null,  // no ModelMBeanNotificationInfo[]
                d);
        cacheMBeanInfo(mbi);
        return mbi;
    }

We override getMBeanInfo so that it calls the original getMBeanInfo from StandardMBean, and then augments it. Because StandardMBean.getMBeanInfo() contains logic to cache the computed MBeanInfo, we need to be careful. super.getMBeanInfo() may return the augmented MBeanInfo that was cached last time. The class we're using to include the descriptor is ModelMBeanInfoSupport, a subclass of MBeanInfo, so if super.getMBeanInfo() returns one of these we know it's one we already created.

To make the descriptor, we use the DescriptorSupport class, which is a concrete implementation of the Descriptor interface. We create an empty descriptor, then add all the entries in the Map to it.

Model MBeans impose a gratuitously annoying rule, which is that every descriptor must have a name and descriptorType field. We add a somewhat random value for the name field, if it wasn't already provided by the Map. The descriptorType field must be equal to "mbean" so we set that.

Next we make the ModelMBeanInfoSupport. It's going to contain the same fields as the original MBeanInfo, but with an extra descriptor. Unfortunately this requires us to take apart the MBeanInfo that we started from, because the MBeanAttributeInfo[] and other arrays it contains are not good enough for ModelMBeanInfoSupport. It wants ModelMBeanAttributeInfo[] etc. You might guess seeing those makeModelMBeanAttributeInfos and other calls that we are in for some tedious code, and you would be right, as we'll see shortly.

Finally, we call StandardMBean.cacheMBeanInfo, so that the next time somebody calls getMBeanInfo() on this MBean it will return the value we just constructed.

Now let's look at that tedious code I mentioned:

    private static ModelMBeanAttributeInfo[]
            makeModelMBeanAttributeInfos(MBeanAttributeInfo[] mbais) {
        ModelMBeanAttributeInfo[] mmbais = new ModelMBeanAttributeInfo[mbais.length];
        for (int i = 0; i < mbais.length; i++) {
            MBeanAttributeInfo mbai = mbais[i];
            ModelMBeanAttributeInfo mmbai = new ModelMBeanAttributeInfo(
                    mbai.getName(),
                    mbai.getType(),
                    mbai.getDescription(),
                    mbai.isReadable(),
                    mbai.isWritable(),
                    mbai.isIs());
            mmbais[i] = mmbai;
        }
        return mmbais;
    }
    
    private static ModelMBeanOperationInfo[]
            makeModelMBeanOperationInfos(MBeanOperationInfo[] mbois) {
        ModelMBeanOperationInfo[] mmbois = new ModelMBeanOperationInfo[mbois.length];
        for (int i = 0; i < mbois.length; i++) {
            MBeanOperationInfo mboi = mbois[i];
            ModelMBeanOperationInfo mmboi = new ModelMBeanOperationInfo(
                    mboi.getName(),
                    mboi.getDescription(),
                    mboi.getSignature(),
                    mboi.getReturnType(),
                    mboi.getImpact());
            mmbois[i] = mmboi;
        }
        return mmbois;
    }
    
    private static ModelMBeanConstructorInfo[]
            makeModelMBeanConstructorInfos(MBeanConstructorInfo[] mbcis) {
        ModelMBeanConstructorInfo[] mmbcis = new ModelMBeanConstructorInfo[mbcis.length];
        for (int i = 0; i < mmbcis.length; i++) {
            MBeanConstructorInfo mbci = mbcis[i];
            ModelMBeanConstructorInfo mmbci = new ModelMBeanConstructorInfo(
                        mbci.getName(),
                        mbci.getDescription(),
                        mbci.getSignature());
            mmbcis[i] = mmbci;
        }
        return mmbcis;
    }

Each of these methods has the same outline. We start with the original MBeanFooInfo array, and construct a parallel ModelMBeanFooInfo array, where each MBeanFooInfo from the original array has been replaced by a ModelMBeanFooInfo with the same information. In practice you might need to write a fourth method, makeModelMBeanNotificationInfos. I haven't done that here because StandardMBean always defines an empty MBeanNotificationInfo[] array. (I said it would be possible to add descriptors to your Tiger MBeans; I didn't say it would be elegant.)

If you're like me, you'll be very annoyed at the fact that these three methods look exactly the same (four if you add makeModelMBeanNotificationInfos). I tried writing the code this way instead:

    private static <I extends MBeanFeatureInfo, M extends I> M[]
            makeModelMBeanFeatureInfos(I[] mbfis, Class<M> modelClass) {
        M[] mmbfis = newArray(modelClass, mbfis.length);
        Converter<M> converter = getConverter(modelClass);
        for (int i = 0; i < mbfis.length; i++) {
            I mbfi = mbfis[i];
            M mmbfi = modelClass.cast(converter.convert(mbfi));
            mmbfis[i] = mmbfi;
        }
        return mmbfis;
    }

A static initializer then initializes a Map<Class<?>, Converter<?>> with one entry for each of the four ModelMBeanFooInfo classes, that is an anonymous class implementing the Converter<ModelMBeanFooInfo> interface. Well, it works, and it avoids the boilerplate in the makeModelMBeanFooInfos methods, but as explanatory code goes it leaves something to be desired. So let's forget makeModelMBeanFeatureInfos.

So, OK, now we've defined DescriptorStandardMBean, right? So does it work?

Wait a minute! We're missing something important:

}

Right. So now let's see what jconsole looks like connecting to our test program on Tiger:

JConsole screenshot showing Tiger MBean with its Descriptor information

Success! Well, mostly. You can see that the descriptor fields we defined are indeed present: author, immutableInfo, interfaceClassName, and mxbean. Unfortunately, ModelMBeanInfoSupport has seen fit to add its own junk fields for us. We'll just have to ignore those.

You can use similar tricks to add descriptors to individual attributes. In the makeModelMBeanAttributeInfos method, for example, you could append a Descriptor parameter to the ModelMBeanAttributeInfo constructor call, with whatever you want in it. As before, you have to remember to add the pointless but necessary name and descriptorType fields; the name must be the same as the name of the attribute, and the descriptorType must be the string "attribute". Also as before, ModelMBeanAttributeInfo will want to add its own crud to your descriptor.

JConsole screenshot showing Tiger MBean attribute with its Descriptor information

Finally, note that there is no ModelMBeanParameterInfo class, so there is no way to specify a descriptor for a parameter (to indicate its units, for example). The best you can do is to put a Descriptor[] in the enclosing ModelMBeanOperationInfo or ModelMBeanConstructorInfo descriptor, with something like operationDescriptor.setField("parameterDescriptors", parameterDescriptors).

Everything here will work on J2SE 1.4 as well, except you'll have to add the JMX and JMX Remote APIs to the classpath of your application, and create your own MBean Server and RMI Connector instead of the ones built-in to Tiger.


Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment



Only logged in users may post comments. Login Here.


Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds