With a Standard MBean, you define the management interface of the MBean using a Java interface. Getters and setters in the interface define attributes, and other methods define operations. But the only information extracted out of the interface is the names and types of the attributes and operations, and just the types of the operation parameters. Although the JMX API allows for textual descriptions to be associated with attributes, operations, and parameters, when you use a Standard MBean these descriptions have meaningless default values. Parameters also have default names like p1, p2, etc. Here's how you can use subclassing and annotations to overcome these limitations.

Suppose you have a Standard MBean that looks like this:

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

You implement the MBean in the usual way, register it in your MBean Server...

        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
        ObjectName name = new ObjectName("com.example:type=CacheController");
        CacheController mbean = new CacheController(...);
        mbs.registerMBean(mbean, name);
   

...and then fire up jconsole to see what it looks like.

Snapshot of CacheController MBean with default metadata 

Well, this is good, the operation is there, but the information about it could be improved. The useful parameter names we gave have been replaced with p1, p2, p3, and when we let the mouse hover over the operation button, we get a tooltip with the default text "Operation exposed for management".

You wouldn't have expected the MBean Server to have been able to guess a better description than that, but you might be surprised that the parameter names that were in your originalCacheControllerMBean interface have been lost. The reason for that is that the contents of the MBeanInfo for a Standard MBean are determined using reflection, and parameter names are not available through the reflection API. (The reason for that is that parameter names are not needed when compiling or executing method calls from other classes, so they don't appear in .class files, except when they're compiled with extra debug info. We wouldn't want the behaviour to be different when compiled with debug info or not, so no parameter names for us.)

With a bit more work you can put more information into your MBeans to produce a better user interface. The technique I'm suggesting is to add annotations to your Standard MBean that will allow you to specify descriptions for operations (and attributes and MBeans) and to give parameter names in a way thatis accessible through reflection. The idea is that your MBean interface would look like this:

public interface CacheControllerMBean {
    @Description("Drop the n oldest entries whose size matches the given constraints")
    public int dropOldest(@PName("n") int n,
                          @PName("minSize") int minSize,
                          @PName("maxSize") int maxSize);
}

The @PName annotation could easily be added automatically by running some sort of script over your source files. Even the @Description could potentially be extracted automatically from doc comments.

Here's what the @PName annotation looks like:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface PName {
    String value();
}

When you're defining an annotation, you need to check out the meta-annotations in java.lang.annotation. (Meta-annotations are annotations for annotation definitions like the definition of@PName here.) They are:

Documented says whether the annotation should appear in documentation generated by the Javadoc tool or similar tools. In this case it would be redundant, giving you something like this:

dropOldest

@Description(value="Drop the n oldest entries whose size matches the given constraints")

int dropOldest(@PName(value="n")
               int n,
               @PName(value="minSize")
               int minSize,
               @PName(value="maxSize")
               int maxSize)

You probably don't want this redundant information, although it might be useful to make sure the annotations are in fact present and have the right values.

Inherited applies to annotations on classes only, so it is not relevant to any annotations for Standard MBean interfaces.

Retention specifies whether the annotation is used only by tools (like the compiler) that read source code; or by tools that read class files; or by code that uses reflection. You might want to define some annotations that work in conjunction with the apttool, in which case all three retention values could be interesting. Usually, though, you will wantRetentionPolicy.RUNTIME.

Target specifies a list of places in the Java language syntax where the annotation can be used. This one is only appropriate as an annotation on parameters, so that's what you say.

By the way, it's important that the name of the method in the@PName annotation be value and not anything else. If it were called name, say, you would have to write @PName(name="n") rather than just@PName("n").

The @Description annotation looks like this:

import static java.lang.annotation.ElementType.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({CONSTRUCTOR, METHOD, PARAMETER, TYPE})
public @interface Description {
    String value();
}

This should be fairly clear based on the discussion of the@PName annotation.

Now that you have these annotations, how do you arrange for them to be taken into account? The idea is to create a subclass of javax.management.StandardMBean that will add the information from the annotations to the information that is already deduced by the normal rules for Standard MBeans. TheStandardMBean class defines a number ofprotected methods that are specifically designed to be overridden for this sort of use. So you can define a class that looks like this:

public class AnnotatedStandardMBean extends StandardMBean {
    
    /** Instance where the MBean interface is implemented by another object. */
    public <T> AnnotatedStandardMBean(T impl, Class<T> mbeanInterface)
            throws NotCompliantMBeanException {
        super(impl, mbeanInterface);
    }
    
    /** Instance where the MBean interface is implemented by this object. */
    protected AnnotatedStandardMBean(Class<?> mbeanInterface)
            throws NotCompliantMBeanException {
        super(mbeanInterface);
    }

    @Override
    protected String getDescription(MBeanOperationInfo op) {
        ...
    }

    ...other overrides...
}

And the code that registers your MBean will now look like this:

        ObjectName name = new ObjectName("com.example:type=CacheController");
        CacheController cc = new CacheController(...);
        Object mbean = new AnnotatedStandardMBean(cc, CacheControllerMBean.class);
        mbs.registerMBean(mbean, name);

By the way, an advantage of this approach is that you no longer have to follow the rigid naming convention where classcom.example.CacheController has to implement interfacecom.example.CacheControllerMBean.

Let's consider the @Description annotation for operations first. The method you want to override is this one:

    protected String getDescription(MBeanOperationInfo op) {...}

You're going to want to find the method corresponding to this operation, and if it has a @Description annotation, return the value of the annotation as the description. Otherwise just return the default value ofop.getDescription().

Finding the method corresponding to anMBeanOperationInfo is not completely straightforward because the parameter types in the containedMBeanParameterInfo[] are expressed asString rather than Class. (This is because Class is not serializable.) EachString is the result of Class.getName() on the correspondingClass. You're going to need the originalClass objects so that you can call Class.getMethod to find the Methodobject and access its annotations.

Unfortunately, there is no standard method to convert back fromClass.getName() to the original Class. Class.forName comes close, but it doesn't do the right thing for primitive types like int.class (also known as Integer.TYPE). int.class.getName()returns "int", but if you give that toClass.forName it will look for a class calledint, which it is unlikely to find. So you'll need a helper method:

    static Class<?> classForName(String name, ClassLoader loader)
            throws ClassNotFoundException {
        Class<?> c = primitiveClasses.get(name);
        if (c == null)
            c = Class.forName(name, false, loader);
        return c;
    }
    
    private static final Map<String, Class<?>> primitiveClasses =
            new HashMap<String, Class<?>>();
    static {
        Class<?>[] prims = {
            byte.class, short.class, int.class, long.class,
            float.class, double.class, char.class, boolean.class,
        };
        for (Class<?> c : prims)
            primitiveClasses.put(c.getName(), c);
    }

Though it's probably not really necessary, I've got into the habit of writing Class<?> everywhere rather than just plain Class. Occasionally that avoids compiler warnings about generics.

Now that you have the classForName method, you can write the method that finds the Method object for anMBeanOperationInfo that comes from a given MBean interface:

    private static Method methodFor(Class<?> mbeanInterface,
                                    MBeanOperationInfo op) {
        final MBeanParameterInfo[] params = op.getSignature();
        final String[] paramTypes = new String[params.length];
        for (int i = 0; i < params.length; i++)
            paramTypes[i] = params[i].getType();
        
        return findMethod(mbeanInterface, op.getName(), paramTypes);
    }
    
    private static Method findMethod(Class<?> mbeanInterface, String name,
                                     String... paramTypes) {
        try {
            final ClassLoader loader = mbeanInterface.getClassLoader();
            final Class<?>[] paramClasses = new Class<?>[paramTypes.length];
            for (int i = 0; i < paramTypes.length; i++)
                paramClasses[i] = classForName(paramTypes[i], loader);
            return mbeanInterface.getMethod(name, paramClasses);
        } catch (RuntimeException e) {
            // avoid accidentally catching unexpected runtime exceptions
            throw e;
        } catch (Exception e) {
            return null;
        }
    }

That gives you enough to be able to write thegetDescription override that will return the value of the @Description annotation if there is one:

    @Override
    protected String getDescription(MBeanOperationInfo op) {
        String descr = op.getDescription();
        Method m = methodFor(getMBeanInterface(), op);
        if (m != null) {
            Description d = m.getAnnotation(Description.class);
            if (d != null)
                descr = d.value();
        }
        return descr;
    }

Getting the parameter names out of the @PNameannotation needs the same kind of code. You need to do a little more work, because there is no methodMethod.getParameterAnnotation that would allow you get the value of a particular annotation for a particular method parameter, the way you can with Method.getAnnotation. But it's easy enough to define. If you just wanted to write a method that returned the @PName annotation you would write this:

        for (Annotation a : m.getParameterAnnotations()[paramNo]) {
            if (a instanceof PName)
                return (PName) a;
        }

You can generalize to a method that works for any annotation rather than just @PName:

    static <A extends Annotation> A getParameterAnnotation(Method m,
                                                           int paramNo,
                                                           Class<A> annot) {
        for (Annotation a : m.getParameterAnnotations()[paramNo]) {
            if (annot.isInstance(a))
                return annot.cast(a);
        }
        return null;
    }

The fiddling with generics is to say that, if theannot parameter is PName.class, say, then the return type is PName. This saves the caller from having to cast. The standard methods such asMethod.getAnnotation do the same thing. Writing<A extends Annotation> means you will get a compiler error if you accidentally call the method with a class that is not an annotation.

Now you have everything you need for the override that extracts a parameter name from the @PName annotation:

    @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();
        }
        return name;
    }

Using the AnnotatedStandardMBean we've just defined, the jconsole window now looks like this:

Snapshot of CacheController MBean with metadata from annotations

The tooltip contains useful text, and the parameters have real names. How gratifying!

There are still some other things you're likely to want in theAnnotatedStandardMBean class. It should be possible to add a @Description annotation to a getter and/or setter method to define the description of an attribute. You might want some support for internationalizing descriptions, for example by including a resource key in the annotation. Operations have animpact field that you might want to be able to set via annotations. All of these things can be built rather simply based on the concepts here.