Skip navigation

MXBeans include a way to handle inter-MBean references conveniently. You can use this to build an MBean hierarchy that is simple to navigate.

In a previous blog entry, I described MXBeans. User-defined MXBeans are a new feature in the Mustang (Java SE 6) platform. They define a type mapping, so you can use arbitrary types in the Java interface that defines your management interface, but have them mapped to a fixed set of predefined types. More details in that earlier entry.

One thing I didn't mention there is that MXBeans contain a facility for managing references between MXBeans very simply. (This facility was based on an idea by Charles Paclat of BEA.) This is probably easiest to explain by example. Suppose I have someproducts, and each product is composed of one or moremodules. I have one MBean per product and one MBean per module. Given the MBean for a product, I would like to be able to see the MBeans for its modules, and vice versa. It looks something like this:

Product MBean referencing two Module MBeans 

The corresponding MXBean interfaces might look like this:

public interface ProductMXBean {
    public Set<ModuleMXBean> getModules();
    public void addModule(ModuleMXBean module);
    public void removeModule(ModuleMXBean module);
    public String getName();
    // ...
}

public interface ModuleMXBean {
    public ProductMXBean getProduct();
    public String getName();
    // ...
}

The ModuleMXBean has an attribute calledProduct, defined by the getProduct()method. The declared type of that attribute isProductMXBean, but this type will be mapped by the MXBean framework. So the actual type that a client such as jconsole will see will in fact be ObjectName. You might see something like this if you connected jconsole to this app:

JConsole showing a Module MXBean with an ObjectName in its Product attribute 

The Product attribute is anObjectName, specifically the name of theProductMXBean.

Creating references

What might the code that created the ProductMXBeanand ModuleMXBeans look like? Here's one possibility:

        // Create and register the Product
        ObjectName productName = new ObjectName("com.example.myapp:type=Product");
        ProductMXBean product = new ProductImpl("wonderprod");
        mbeanServer.registerMBean(product, productName);
        
        // Create and register the Modules and add each one to the Product
        String[] moduleIds = {"fred", "jim", "sheila"};
        for (String moduleId : moduleIds) {
            ModuleMXBean module = new ModuleImpl(moduleId, product);
            ObjectName moduleName =
                new ObjectName("com.example.myapp:type=Module,name=" + moduleId);
            mbeanServer.registerMBean(module, moduleName);
            product.addModule(module);
        }

Here we are creating the Product and its Modules all at the same time, so we are able to give the Product a direct reference to each of the Module objects and vice versa. The code in bold shows this happening. This approach assumes an intimate relationship between all the objects in question, and is most suitable if the code for creating all the MXBeans is small and centralized.

Another possibility is illustrated by rewriting the code as follows. Only the code in bold below has changed.

        // Create and register the Product
        ObjectName productName = new ObjectName("com.example.myapp:type=Product");
        ProductMXBean product = new ProductImpl("wonderprod");
        mbeanServer.registerMBean(product, productName);
        ProductMXBean productProxy =
            JMX.newMXBeanProxy(mbeanServer, productName, ProductMXBean.class);
        
        // Create and register the Modules and add each one to the Product
        String[] moduleIds = {"fred", "jim", "sheila"};
        for (String moduleId : moduleIds) {
            ModuleMXBean module = new ModuleImpl(moduleId, productProxy);
            ObjectName moduleName =
                new ObjectName("com.example.myapp:type=Module,name=" + moduleId);
            mbeanServer.registerMBean(module, moduleName);
            ModuleMXBean moduleProxy =
                JMX.newMXBeanProxy(mbeanServer, moduleName, ModuleMXBean.class);
            product.addModule(moduleProxy);
        }

The coupling between the Product and itsModules is much looser because each only knows theObjectName of the other, and not the object itself as before. The Modules could have been created in one place in the code and the Product somewhere else entirely, and they would only have had to agree on theObjectNames.

Navigating the MXBean hierarchy

The real power of this approach comes when you are coding a client that interacts with this model.

You can construct a proxy for an MXBean using the new method JMX.newMXBeanProxy. If you have a proxy for aModuleMXBean, say, then it is an object of typeModuleMXBean. If you call its getName()method, that will result in a call to MBeanServerConnection.getAttribute(moduleObjectName, "Name"). Very handy.

But the ModuleMXBean interface also has this method:
ProductMXBean getProduct();
What does it return when you call it on aModuleMXBean proxy?

You might have guessed the answer. It returns another proxy, this time for the ProductMXBean.

Suppose we known the ObjectName of aModuleMXBean, and we want to find the names of all the modules in the same product. We can do this by starting with a proxy for the ModuleMXBean and using it to obtain a proxy for its ProductMXBean:

Navigating from the ModuleMXBean to its ProductMXBean 

Then we can navigate from this proxy to proxies for all of the product's modules:

Navigating from the ProductMXBean to its ModuleMXBeans 

In code, finding the names of all the modules given just theObjectName of one module looks like this:

        ModuleMXBean startModuleProxy =
            JMX.newMXBeanProxy(mbeanServerConnection, startModuleName,
                               ModuleMXBean.class);
        ProductMXBean containingProduct = startModuleProxy.getProduct();
        Set<ModuleMXBean> modules = containingProduct.getModules();
        for (ModuleMXBean module : modules)
            System.out.println(module.getName());

The ability to navigate through the MBean model using proxies is a very powerful one, and a strong incentive to use this approach to managing references between MBeans.

The Java compiler doesn't save parameter names in the class files it generates. This is a problem for Standard MBeans, because we'd like to show those names in management clients. I talked about this in an earlier blog entry, where I suggested using a@PName annotation on each parameter to specify its name redundantly. Here's another approach, using annotation processors, which will work without adding any annotations at all.

[Updated 4 August 2006 to incorporate recent API changes.]

Annotation processors were introduced in the Tiger JDK, via theapttool. apt is a command that you can use instead ofjavac. It does all the same things asjavac, but in addition you can give it one or moreannotation processors. An annotation processor is a sort of compiler plug-in that you can use to get the compiler to execute your own code during compilation.

Annotation processors cannot change the code that the compiler will generate for a given source file. (This is consistent with the idea that annotations cannot change Java language semantics.) However, they can generate new files based on what they find in the source files being compiled. In particular, they can generate new Java source files, and those new files will also be compiled in the current run. The annotation processors will be run on these new files too, potentially resulting in several "rounds" of source file generation.

Another interesting thing that annotation processors can do is to make checks. For example, the @Descriptionannotation that I defined in the earlier blog entry only makes sense in an MBean interface. If you put it anywhere else, it will have no effect, and you will have no indication that whatever you were trying to achieve with it isn't working. You could use an annotation processor to detect stray @Descriptionannotations and emit warnings or errors for them at compile time. Bruce Chapman gave an interesting BOF at the latest JavaOne on the subject of user-defined compile-time checking using annotation processors (slides here)

A processor for MBean operation parameter names

The name annotation processor is a bit of a misnomer. It's actually a general-purpose compiler plugin mechanism. You can arrange for it to analyze all source files that are being compiled, whether or not they contain annotations.

We can use this ability to define a compiler plugin that extracts the parameter names out of MBean operations defined in Standard MBeans. Here's the idea. Suppose we have the following Standard MBean interface (from the earlier blog entry):

package com.example.myapp;

public interface CacheControllerMBean {
    /** Drop the n oldest entries whose size matches the given constraints. */
    public int dropOldest(int n, int minSize, int maxSize);
}

We would like to generate another Java interface defined like this:

package com.example.myapp;

public interface CacheControllerMBeanPNames {
    public static final String[] dropOldestPNames = {
        "n", "minSize", "maxSize",
    };
}

Using this, we will be able to modify theAnnotatedStandardMBean class that we defined before, so that it can pick up the names for the parameters inCacheControllerMBean by using reflection onCacheControllerMBeanPNames.

Before looking at how that can be done, let's look at some of the other ways we could achieve the same thing. One of them is to use some sort of script that picks out the relevant information and generates the needed files. The problem with this sort of script is that it's very difficult to do the required pattern matching correctly. Will we be able to write a regular expression that recognizes a Java interface no matter how it is defined? Even if it is defined like this for example?

public // for now
interface
/* Name may change in later version */ CacheControllerMBean
    {

If so, then we'll basically be reinventing the parser from the Java compiler. Why not just use that parser, as annotation processors allow us to do?

Another possibility is to define a doclet that you can plug into the javadoctool. This can certainly work, but it is not as straightforward. You'll have to figure out how to invoke javadoc with your doclet, and how to arrange for the generated source files to be compiled and included in your jar file. All of this "just works" with annotation processors. Furthermore, doclets are not a standard Java feature, so you have to use a com.sun API to code them.

Assuming we do use an annotation processor, we can choose between generating Java source code and generating plain text (or binary) files. We could generate an XML file with the parameter names, for example. There are several advantages to generating a source file, however. One is that the compiler will do some basic sanity checking on what our processor generates. Another is that we don't have to do anything special to arrange for the generated class to be packaged up in our jar file; it just gets put there along with all the other compiled classes. A third advantage is that we can retrieve the information at runtime using the same Reflection API that we are already using to introspect the MBean interface.

Writing the annotation processor

Up until now I've been talking about the apt tool from JDK 5.0. That's a non-standard tool, as you can see from theinterfacesyou use to define a processor. These are all com.sun.*interfaces.

A standard annotation processor facility is being defined by JSR 269, "Pluggable Annotation Processing API". This will be part of the Mustang (Java SE 6) platform, which means that a Java compiler from any Mustang implementation can support it. Processing is no longer handled by a separate non-standard tool likeapt, but by specifying the processor(s) directly tojavac. The interfaces for defining a processor are all in the javax.* namespace.

The Mustang documentation for annotation processors is still a bit raw, and I found the slidesfrom the BOF on the subject at the latest JavaOne to be immensely helpful.

The Java classes that are of interest when writing a processor are in javax.annotation.processing and in the various javax.lang.model.* packages.

A processor is a Java class that implements the Processor interface, usually by subclassing AbstractProcessor. We're going to define a processor in the classcom.example.processors.MBeanPNameProcessor, and we'll be able to invoke it like this:

javac -processor com.example.processors.MBeanPNameProcessor source-files

To shorten the text, I'll use wildcard imports in what follows. In practice, you're always much better off getting your IDE to spell out the imports explicitly.

Here's the beginning of the processor:

package com.example.processors;

import java.io.*;
import java.util.*;
import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import javax.lang.model.util.*;
import javax.tools.*;

@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class MBeanPNameProcessor extends AbstractProcessor {
    public MBeanPNameProcessor() {
    }

Most processors will subclass AbstractProcessor as this one does. That means that they only have to implement the process method. You do have to supply some information with annotations on the processor, however.

The @SupportedSourceVersion annotation says what version of the Java programming language your processor understands. Here, we say we understand version 6, i.e. Mustang. Obviously that means that we understand all earlier versions too. But when the Dolphin (Java SE 7) platform arrives, and somebody compiles with javac -source 7, our processor won't run. We can't guarantee that our processor will be able to handle new language features like superpackages and XML literals that might show up in Dolphin. So this annotation guarantees that we won't make it try.

The @SupportedAnnotationTypes annotation is somewhat more complicated. Different processors can "claim" different annotations. Without getting into the details of this, let's just say that a processor such as ours that is not directly concerned with annotations should do the following:

  • specify @SupportedAnnotationTypes("*");
  • return false from its processmethod;
  • be specified in the list of processors before any processor that also specifies @SupportedAnnotationTypes("*") and that returns true from its processmethod.

Every processor must have a public no-arg constructor, as here.

The next thing is a handy method for debugging the processor:

    private static final boolean silent = false;

    private void note(String msg) {
        if (!silent)
            processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg);
    }

This is the equivalent of System.out.println, except its output shows up as a "note" from the compiler, something like this:
Note: Found com.example.myapp.CacheControllerMBean

We can set silent to true when we're confident the processor works.

Here, processingEnv is a protected field defined in the parent class AbstractProcessor and defined when the processor is initialized. Purists like me might have preferred a processingEnv() method, though you could seeprocessingEnv as being likeSystem.out.

Here's the process method, which defers the interesting work to another method we will define later:

    public boolean process(Set<? extends TypeElement> annotations,
                           RoundEnvironment roundEnv) {
        note("process: annotations=" + annotations + ", roundEnv=" + roundEnv);
        checkForMBeanInterfaces(roundEnv.getRootElements());
        return false;
    }

The annotations parameter is not interesting for us because we're not defining an actual annotation processor. The RoundEnvironment contains information about the current processing "round". Of this information, we're only interested here in the classes being compiled, as returned bygetRootElements.

On the first round, process will be invoked with the set of classes that were named on the command line or that need to be recompiled because they're referenced by those classes. Then, if we generate any new source files, there'll be a second round where the classes will be the ones contained in those source files. This continues until a round generates no new source files; then there'll be a final round with no input classes that can be used to do any final processing.

We're not concerned with the details of rounds for this processor. Whatever classes we see, we'll analyze. If there are no classes, we won't analyze anything.

This processor will handle MBean interfaces that are top-level members of a package, but also that are defined within other classes. Defining an MBean interface as a nested class isn't recommended in general but I often do it for tests so that the whole test fits into one source file. The processor could be simplified slightly if we only supported top-level MBean interfaces.

The following method will initially be given the list of classes being compiled. Then for each class in the list it will be invoked recursively with all of that class's members, namely constructors, fields, methods, and nested classes. Of these, only nested classes interest us, so we'll filter out all the others. Finally we'll call another method on every class we see, including nested classes, in order to pick out the MBean interfaces.

    private void checkForMBeanInterfaces(Collection<? extends Element> elements) {
        note("checkForMBeanInterfaces: " + elements);
        Collection<? extends TypeElement> typeElements =
                ElementFilter.typesIn(elements);

        for (TypeElement type : typeElements) {
            checkForMBeanInterfaces(type.getEnclosedElements());
            checkForMBeanInterface(type);
        }
    }

Here's the method that detects an MBean interface. An MBean interface must match the following criteria:

  • the name of the type ends with ...MBean
  • the type is an interface (not a class or annotation or enum)
  • the interface is public
  • the interface has no type parameters (likeMyMBean<T>)
    private void checkForMBeanInterface(TypeElement type) {
        // name must end in MBean
        if (!type.getQualifiedName().toString().endsWith("MBean"))
            return;

        // must be an interface
        if (type.getKind() != ElementKind.INTERFACE)
            return;

        // must be public
        if (!type.getModifiers().contains(Modifier.PUBLIC))
            return;

        // must not have type parameters
        if (!type.getTypeParameters().isEmpty())
            return;

        writePNamesInterface(type);
    }

If the type met all those conditions, then we're ready to analyze it and write out the generated ...MBeanPNamesinterface with the method parameter names:

    private void writePNamesInterface(TypeElement type) {
        note("Found " + type);
        String pnamesInterfaceName =
                processingEnv.getElementUtils().getBinaryName(type) + "PNames";
        try {
            writePNamesInterface(type, pnamesInterfaceName);
        } catch (IOException e) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
                    "Could not create source file for " + pnamesInterfaceName +
                    ": " + e);
        }
    }

If we get an exception while we're trying to write the new file, then we convert that into a compiler error as shown above. Generally speaking, processors should not throw exceptions.

We use getBinaryName here just because we're handling nested classes. The upshot is that if the interface iscom.example.Main.TestMBean then we will generatecom/example/Main$TestMBean.java rather thancom/example/Main/TestMBean.java which would fail.

Here's the code that actually generates the new interface.

    private void writePNamesInterface(TypeElement type, String pnamesInterfaceName)
    throws IOException {
        Filer filer = processingEnv.getFiler();
        OutputStream os =
            filer.createSourceFile(pnamesInterfaceName).openOutputStream();
        PrintWriter pw = new PrintWriter(os);
        int lastDot = pnamesInterfaceName.lastIndexOf('.');
        String baseName = pnamesInterfaceName.substring(lastDot + 1);
        pw.println("// " + baseName + ".java - generated by " +
                MBeanPNameProcessor.class.getName());
        pw.println();
        if (lastDot > 0) {
            pw.println("package " + pnamesInterfaceName.substring(0, lastDot) + ";");
            pw.println();
        }
        pw.println("public interface " + baseName + " {");
        for (ExecutableElement method :
             ElementFilter.methodsIn(type.getEnclosedElements())) {
                writeMethodPNames(pw, method);
        }
        pw.println("}");
        pw.close();
        os.close();
    }

    private void writeMethodPNames(PrintWriter pw, ExecutableElement method) {
        pw.println("    public static final String[] " + method.getSimpleName() +
                   "PNames = {");
        pw.print("        ");
        for (VariableElement param : method.getParameters())
            pw.print("\"" + param.getSimpleName() + "\", ");
        pw.println();
        pw.println("    };");
    }
}

That's it! This is a complete processor that can generate aCacheControllerMBeanPNames interface like this:

package com.example.myapp;

public interface CacheControllerMBeanPNames {
    public static final String[] dropOldestPNames = {
        "n", "minSize", "maxSize",
    };
}

(It's worth noting that the generated interface will be incorrect if the MBean interface contains overloaded methods, i.e. more than one method with the same name. We strongly recommend against including overloaded methods in MBeans so "this could be construed as a feature.")

Using the generated interface

All we need to do now is to modifyAnnotatedStandardMBean from the earlier blog entry so that it picks up the parameter names from the ...MBeanPNames interface if it exists. Compared to writing an annotation processor, that is a piece of cake. The new code is in bold below:

    @Override
    protected String getParameterName(MBeanOperationInfo op,
                                      MBeanParameterInfo param,
                                      int paramNo) {
        String name = param.getName();
        Method m = methodFor(getMBeanInterface(), op);
        if (m != null) {
            PName pname = getParameterAnnotation(m, paramNo, PName.class);
            if (pname != null)
                name = pname.value();
            else {
                String name1 = getNameFromPNames(op, name, paramNo);
                if (name1 != null)
                    name = name1;
            }
 
        }
        return name;
    }
And here's the new method getNameFromPNames
    private String getNameFromPNames(MBeanOperationInfo op, String name, int paramNo) {
        try {
            Class<?> pnamesClass =
                    Class.forName(getMBeanInterface().getName() + "PNames");
            Field namesField = pnamesClass.getField(op.getName() + "PNames");
            String[] names = (String[]) namesField.get(null);
            return names[paramNo];
        } catch (Exception e) {
            // no PNames class, or malformed
            return null;
        }
    }

Phew! I hope that's been of interest. My thanks to Joe Darcy, who took the trouble to give me some very detailed explanations.

Mustang (Java SE 6) includes the ability to give additional information about MBeans to management clients viaDescriptors, 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 jconsoletool 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 yourapplication 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 thatthe Model MBean package defines a set of subclasses of the standard metadata classes such as MBeanInfo and MBeanAttributeInfo. For example, ModelMBeanAttributeInfo is just likeMBeanAttributeInfo except that it adds a descriptor. Prior to Mustang, MBeanAttributeInfo did not have a descriptor, but ModelMBeanAttributeInfo did. SinceModelMBeanAttributeInfo is a subclass ofMBeanAttributeInfo, you can use aModelMBeanAttributeInfo wherever anMBeanAttributeInfo 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 ofModelMBeanAttributeInfo rather than justMBeanAttributeInfo.

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 calledDescriptorStandardMBean 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 isTestMBean.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 theTest class implements the TestMBeaninterface 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 aMap<String, ?> rather than aMap<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 aList<Object>, and likewise you can't assign aMap<String, String> to a Map<String, Object>. But you can assign ifObject is replaced by ?.

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

    @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 augmentedMBeanInfo that was cached last time. The class we're using to include the descriptor isModelMBeanInfoSupport, a subclass ofMBeanInfo, 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 theMap to it.

Model MBeans impose a gratuitously annoying rule, which is that every descriptor must have a name anddescriptorType 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 parallelModelMBeanFooInfo array, where eachMBeanFooInfo 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 emptyMBeanNotificationInfo[] array. (I said it would bepossible 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 addmakeModelMBeanNotificationInfos). 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 aMap<Class<?>, Converter<?>> with one entry for each of the four ModelMBeanFooInfo classes, that is an anonymous class implementing theConverter<ModelMBeanFooInfo> interface. Well, it works, and it avoids the boilerplate in themakeModelMBeanFooInfos methods, but as explanatory code goes it leaves something to be desired. So let's forgetmakeModelMBeanFeatureInfos.

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, andmxbean. 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 makeModelMBeanAttributeInfosmethod, for example, you could append a Descriptorparameter to the ModelMBeanAttributeInfo constructor call, with whatever you want in it. As before, you have to remember to add the pointless but necessary name anddescriptorType fields; the name must be the same as the name of the attribute, and thedescriptorType 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 noModelMBeanParameterInfo 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 orModelMBeanConstructorInfo 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.

JDK 5.0 allows you to make an application monitorable without writing any extra code, using command-line properties such as-Dcom.sun.management.jmxremote. But what if the behaviour obtained using these properties isn't exactly what you want?

Monitoring available out of the box

JDK 5.0 (codenamed Tiger) introduced support for management and monitoring through three important new features (among others):

  • addition of the JMX and JMX Remote APIs into the platform;
  • definition of a new API to monitor the JVM itself (java.lang.management)
  • support for "out-of-the-box" management and monitoring.

Of these, the first two are standard features of J2SE 5.0, while the last is a specific feature of the JDK from Sun (and any other implementations that may have copied it).

"Out-of-the-box" management and monitoring means that you can take an application as it is, "out of the box", without any code that specifically deals with monitoring, and add a JMX agent to it to make it monitorable. The full details are in the document Monitoring and Management Using The JMX API. Typically you run the application with a command line something like this:

java -Dcom.sun.management.jmxremote.port=9876
     -Dcom.sun.management.jmxremote.password.file=/some/path/jmxremote.password
     com.example.MyApp.Main

This causes your application to be run with a JMX agent accessible on port 9876, but protected by password authentication defined in the file /some/path/jmxremote.password.

Then you can connect with jconsole, and get an idea of what your application is doing. Mandy Chung (who also led the work on out-of-the-box management) describes this in detail in the article Using JConsole to Monitor Applications. Here's a picture from that article to whet your appetite, if you're not already familiar with jconsole:

Snapshot of jconsole showing heap memory graph 

Going beyond what's available out of the box

The idea behind out-of-the-box management was that people should be able to monitor their applications without having to learn theJMX Remote API. Just set a couple of properties, and off you go.

On the other hand, we didn't want to address every configuration that everybody could possibly want. The available properties can be used to create useful configurations, but our idea at the time was that there would obviously be problems that couldn't be addressed by setting these properties. We didn't want to add a new property every time somebody ran into such a problem, in effect creating a parallel version of the JMX Remote API defined with properties rather than Java method calls. Instead, we thought that people could start off using the properties, and if they ran into a need that wasn't addressed by the properties then they could switch over to writing code that calls the JMX Remote API explicitly.

For example, here are some interesting things that you can't do with the properties, but that you can do by using the JMX Remote API:

  • Obtain the same functionality on JDK 1.4.

  • Rather than hardwiring a port number like 9876 (and risking port conflicts), make the JMX agent available on a free port chosen by the operating system, and communicate the port number to management clients.

  • On a machine with multiple network interfaces, only export the JMX agent on one of them.

  • Use a different access-control scheme than the one available through the com.sun.management.jmxremote.access.fileproperty.

  • The remote connectivity works by creating an RMI registry on the given port number, and putting an RMI object in that registry that is the JMX agent that remote clients connect to. Properties allow you to protect this object with SSL, but in JDK 5.0 they didn't allow you to protect the RMI registry with SSL. (This functionality has been added in JDK 6.)

However, it's become clear over time that we omitted something important. Imagine somebody wants to monitor their app. They study the documentation to figure out how to use the properties to get what they want. This works fine at first. Then they realize there's some problem they can't solve using the properties. So now they're forced to throw away what they've learnt about the properties, and study another document, the JMX Remote API, to figure out how to obtain the same thing. Bummer.

What's needed is a document that lets you figure out, given a configuration defined using properties, how to achieve the same configuration with code. Then you can fiddle with the code to solve your problem. You only need to study what's needed for this change, not what's needed to achieve the initial functionality.

An omission rectified

Fortunately, my colleague Luis-Miguel Alventosa has now supplied exactly such a document in his blog entry Mimicking the out-of-the-box JMX management agent in J2SE 5.0. I expect a version of this to appear in a future version of the JDK documentation, but in the meantime, here I am giving it some googlejuice.

By the way, the fact that you are writing code to use the JMX Remote API still doesn't mean you have to change your application code. You can either write a new main method in a different class that calls the original main after exporting the JMX agent; or you can use an agent class as described in the documentation for java.lang.instrument and launch your application something like this:

java -javaagent:/some/path/myjmxagent.jar com.example.MyApp.Main