Skip to main content

Adding information to a Standard MBean interface using annotations

Posted by emcmanus on July 25, 2005 at 9:52 AM PDT

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 href="http://java.sun.com/developer/technicalArticles/J2SE/jconsole.html">
jconsole to see what it looks like.

alt="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 original
CacheControllerMBean interface have been lost. The
reason for that is that the contents of the href="http://www.java.net/download/jdk6/doc/api/javax/management/MBeanInfo.html">
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 that
is 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 href="http://www.java.net/download/jdk6/doc/api/java/lang/annotation/package-frame.html">
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 href="http://java.sun.com/j2se/1.5.0/docs/guide/apt/index.html">
apt tool, in which case all three retention
values could be interesting. Usually, though, you will want
RetentionPolicy.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 href="http://www.java.net/download/jdk6/doc/api/javax/management/StandardMBean.html">
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. The
StandardMBean class defines a number of
protected 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 class
com.example.CacheController has to implement
interface com.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 of
op.getDescription().

Finding the method corresponding to an
MBeanOperationInfo is not completely straightforward
because the parameter types in the contained
MBeanParameterInfo[] are expressed as
String rather than Class. (This is
because Class is not serializable.) Each
String is the result of

Class.getName()
on the corresponding
Class. You're going to need the original
Class objects so that you can call

Class.getMethod
to find the
Method object and access its annotations.

Unfortunately, there is no standard method
to convert back from Class.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 to Class.forName it will
look for a class called int, 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 an
MBeanOperationInfo 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 the
getDescription 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 @PName
annotation needs the same kind of code. You need to do a little
more work, because there is no method
Method.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 the
annot parameter is PName.class, say,
then the return type is PName. This saves the caller
from having to cast. The standard methods such as
Method.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:

alt="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 the
AnnotatedStandardMBean 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 an

impact
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.

Related Topics >>

Comments

<p>Question about ...

Question about CompositeData

Hi

Thanks so much for the info, it works great. Now I have just a little drawback, i get an Unmarshall Exception when parsing some compositeData as result. But after creating compositeData implementing Serialiazable, i got a ClassNotFoundException. The weird thing is that when registering MBEan as usual that does not happend, but when doing it with the AnnotatedStandardMBean i got that problem, any idea or suggestion?

Thanks in advance

Hi, try using the StanardMBean's overridden constructor ...

Hi,

try using the StanardMBean's overridden constructor in AnnotatedStandardMBean:

super(impl, mbeanInterface, JMX.isMXBeanInterface(mbeanInterface));

If you are using anything else than a primitive class in one of your MBean's operations's signature, you have to use MXBeans, and you have to tell this to StandardMBean.

I hope this helps, it worked for me.

Inter-MXBean references and annotations

Thanks Eamonn for this post.
I have a question about the method used to obtaine a MBean operations descriptions (methodFor). This method works well when the parameters are primitive or object types but not if one of these is an MXBean . Here an example :

public void addModule(ModuleMXBean module)

at runtime has a signature like this:

public void addModule(javax.management.ObjectName)

therefore when the findMethod's return instruction is processed it raises an NoSuchMethodExecption.

How can I resolve this problem?

To resolve, I wrote a work around. It consists in :
  1. check parameter type equal to javax.management.ObjectName
  2. find a method into MXBean interface that has the same position
  3. verify the parameter's name terminates with MXBean string

however this solution is weakest.