Adding Descriptors to MBeans in Tiger
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:
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:
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.
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.
- Login or register to post comments
- Printer-friendly version
- emcmanus's blog
- 1230 reads





