The Source for Java Technology Collaboration
User: Password:



Eamonn McManus

Eamonn McManus's Blog

Adding information to a Standard MBean interface using annotations

Posted by emcmanus on July 25, 2005 at 09:52 AM | Comments (20)

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 original CacheControllerMBean 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 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 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 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 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:

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.


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

  • Fantastic article. Thanks for the level of detail.

    Posted by: markswanson on July 25, 2005 at 10:29 AM

  • Looks like a lot of duplication going on there. Couldn't you get APT to generate extra info classes from the existing parameters names and JavaDocs?

    Posted by: tackline on July 25, 2005 at 11:56 AM

  • tackline, yes, APT has some interesting potential here, which I'm hoping to write about in the not too distant future. Concerning duplication, I think it would only really be able to remove the duplication of parameter names (@PName("x") int x). Though I mentioned the possibility of pulling descriptions out of doc comments, I don't really believe in it. The audience for doc comments is developers while the audience of descriptions is users, and I wouldn't expect the same text to be appropriate for both in general. That's particularly true when you start to think about internationalization.

    Where I think APT would be particularly useful would be to allow you to add annotations directly into an MBean class, and dispense with the separate MBean interface altogether. E.g. you would add @Management in front of a public method of your MBean class and it would automatically become an operation or attribute of the MBean. APT could either generate an MBean interface with @Description and @PName and the rest, or it could generate an XML file that would be converted into an MBeanInfo by a support class at runtime. The latter would work well with Apache Commons Modeler.

    Posted by: emcmanus on July 27, 2005 at 03:25 AM

  • Hi,

    I've read your article on "Adding information to a Standard MBean interface using annotations". And also tried it out on a test project. At first everything seemed to work fine.. now I've noticed that when I'm using AnnotatedStandardMBean I don't get anny notifications in my JConsole wich a was receiving before. I'm kinda new too JMX (I'm a last year college student doing a internship by wich my task is to integrate JMX with the excisting project). And I don't see why I don't get these notifications anymore.
    By changing the following 2 lines back to how it was before I can recieve the notification.

    mbean = new AnnotatedStandardMBean(teken,TekenMBean.class);
    mbs.registerMBean(mbean, name);// No notifications

    Teken teken = new Teken();
    mbs.registerMBean(teken,name);//With notifications

    Can anyone help me solve this problem?
    thanks in advance.

    Posted by: livid on August 03, 2005 at 07:03 AM

  • livid, for an MBean to send notifications it must implement the interface javax.management.NotificationEmitter. The AnnotatedStandardMBean class that I described doesn't. In the next version of the JDK (Mustang), you'll be able to use the new class
    StandardEmitterMBean for cases like this. In the meantime, I would suggest you subclass AnnotatedStandardMBean to make AnnotatedStandardEmitterMBean, which can implement the NotificationEmitter interface by delegating to an instance of NotificationBroadcasterSupport, as I described in a
    later blog entry. You could also try
    downloading the Mustang sources to see how the StandardEmitterMBean class is implemented.

    Posted by: emcmanus on August 03, 2005 at 07:16 AM

  • well, lang.annotations is a new addition in jdk 1.5. But i need to make these annotations work on any old jdk versions. Can i make it work?

    I believe what ever the annotation way u suggested looks like a little bit crunchy.
    Is there any other alternate and easy way to add this information for an mbean?.

    Posted by: effigramy on December 17, 2005 at 10:53 PM

  • effigramy, for pre-Tiger platforms your best bet to replace annotations is Commons Attributes. The Spring Framework for example allows you to add MBean metadata either using annotations or using Commons Attributes; see the relevant
    section in their online documentation. You could look at the Spring source to see how that is done.

    Posted by: emcmanus on December 19, 2005 at 02:02 AM

  • I always wondered why so many Java tools (e.g. IDEs, JMX consoles) could not get the proper names of the parameters from the debug info in the class file. Especially as @PName breaks Don't Repeat Yourself rule.

    If only you could add a class annotation: @RememberAllParameterNames

    It could use the debug info to generate all the appropriate PNames.


    No doubt, I don't understand Annotations or the Java Reflection API well enough to understand why it is not possible.

    PS: Its good to see a Schol being so famous, Jonathan

    Posted by: ninkibah on February 22, 2006 at 07:08 AM

  • Hi Jonathan, fancy meeting you here!

    I think what you suggest would be the thrust of RFE 5082475. The idea would be that you could add an annotation @KeepParameterNames to a method or constructor, and the names in question would appear in the compiled class-file. Then they would be available through new Reflection methods, Method.getParameterNames and Constructor.getParameterNames. If you call these methods on a Method or Constructor without the @KeepParameterNames annotation you get nothing.

    As you suggest the @KeepParameterNames annotation could also work on an entire class or interface, though whether it should apply to all methods or constructors or only public (and maybe protected) ones is unclear.

    Either way, this would be clear win for MBean interfaces, and also for a number of other applications. The uglyish
    @PropertyNames annotation for MXBeans could disappear, and the clunky
    DefaultPersistenceDelegate from JavaBeans would be mostly redundant.

    However it doesn't look as if this change will make it into Mustang, which is a pity.

    Glad to see you are keeping up the chess.

    Posted by: emcmanus on February 22, 2006 at 08:34 AM

  • I am trying to implement AnnotatedStandardEmitterMBean. I have not looked at Mustang source yet but is the solution you recommend only going to work in the nistance when the mbean implementation extends AnnotatedStandardEmtterMBean? I'd really like the other case to work where the implementation is some other class.

    Posted by: seanl on August 01, 2006 at 05:21 PM

  • seanl, no, your MBean implementation class does not have to extend anything. The usage would be something like:

    public class Whatsit implements WhatsitMBean {...}
    Whatsit impl = new Whatsit();
    Object mbean = new AnnotatedStandardEmitterMBean(impl, WhatsitMBean.class);
    mbeanServer.registerMBean(mbean, someName);

    Your MBean code is in the class Whatsit, which doesn't have to know anything about AnnotatedStandardEmitterMBean.
    On the other hand, if you want to have both AnnotatedStandardMBean and AnnotatedStandardEmitterMBean, then because Java doesn't have multiple inheritance you'll have to work fairly hard to avoid code duplication.

    Posted by: emcmanus on August 02, 2006 at 12:56 AM

  • My confusion was that the actual call should be:
    Object mbean = new AnnotatedStandardEmitterMBean(impl, WhatsitMBean.class, emitter);

    It's all clear to me now. Thanks.

    Posted by: seanl on August 02, 2006 at 08:00 AM

  • I have returned to confusion. First, on your comment: "if you want to have both AnnotatedStandardMBean and AnnotatedStandardEmitterMBean, then because Java doesn't have multiple inheritance you'll have to work fairly hard to avoid code duplication." I don't know what that means. My AnnotatedStandardEmitterMBean extends AnnotatedStandardMBean so that's why I'm confused by your statement.

    Now I'm also having issues with my implementation. My Whatsit class contains MyEmitter that extends NotificationBroadcasterSupport and overrides getNotificationInfo(). My AnnotatedStandardEmitterMBean is the same as the Mustang one except it extends AnnotatedStandardMBean instead of MBean and it has just the two constructors that AnnotatedStandardMBean has.

    The problems I'm seeing are that 1) JConsole Info tab does not show the MBean notification. 2) After registering for the notification, I cause the notification to be sent and I get the following message from JConsole in the window I started it in:

    Aug 2, 2006 11:09:56 AM ClientNotifForwarder NotifFetcher.fetchOneNotif
    WARNING: Failed to deserialize a notification: java.io.NotSerializableException: jmx.DirectImpl

    Other straightforward notifications (w/o the Annotation* classes) don't have any problems. Any idea what's going on?


    Posted by: seanl on August 02, 2006 at 10:12 AM

  • seanl, having AnnotatedStandardEmitterMBean extend AnnotatedStandardMBean seems like a good choice. It does require you to reimplement the functionality of StandardEmitterMBean, since you can't also extend that. But that functionality is not very complicated.
    If AnnotatedStandardEmitterMBean doesn't show the notification info (when you open Notifications in the MBean tree, there should be one entry for each MBeanNotificationInfo), then it is probably because it isn't including them in the MBeanInfo it constructs. StandardEmitterMBean conspires with its parent class StandardMBean to include this information by overriding the method getNotifications(MBeanInfo), but that method is package-private so you can't do the same thing. Instead, I would recommend overriding cacheMBeanInfo(MBeanInfo) so that it checks if its parameter is non-null and does not already contain the MBeanNotificationInfo[] array. In that case, you can call super.cacheMBeanInfo with a rewritten MBeanInfo that does include the array. Otherwise, just call super.cacheMBeanInfo with an unmodified MBeanInfo.
    The problem you are seeing with NotSerializableException must be due to a nonserializable field in your Notification class. The value in that field is of type jmx.DirectImpl which should give you a clue as to which field it is. You probably want to make that field volatile.

    Posted by: emcmanus on August 03, 2006 at 04:21 AM

  • The NotSerializable exception is troubling because I don't see it unless I use the AnnotatedStandardEmitterMBean. Furthermore, if I use another MBean I also know to work: HelloMBean in the Notification example, it fails the same way. BTW, just to be clear, I am using JDK1.5. The hint regarding getNotifications is probably going to be a great help. I appreciate your help and admire your expertise! Your work has been most helpful.

    Posted by: seanl on August 03, 2006 at 07:42 AM

  • Overriding cacheMBeanInfo does not seem to help. The MBeanInfo parameter is always null. It appears that cacheMBeanInfo is called during construction and StandardMBean has not yet reflected on the implementation to determine the MBeanInfo. I believe this because if I call getMBeanInfo from within cacheMBeanInfo, I get a NPE.

    Posted by: seanl on August 07, 2006 at 10:26 AM

  • Hmmm, it works for me. On JDK 5, cacheMBeanInfo is called once with a null parameter, and a second time with the initial MBeanInfo. Only when the MBeanInfo is non-null should you rewrite it. You do not need to call getMBeanInfo from within cacheMBeanInfo, because the MBeanInfo parameter is the value you are looking for.

    Posted by: emcmanus on September 05, 2006 at 07:15 AM

  • Thanks for the article, very informative.

    One point is that it doesn't support tabular types, or generics gracefully.

    Any advances/mods on the following gratefully received i.e support for other OpenTypes.

    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++) {
    if (params[i] instanceof OpenMBeanParameterInfoSupport) {
    OpenType openType = ((OpenMBeanParameterInfoSupport)params[i]).getOpenType();

    if (openType instanceof javax.management.openmbean.TabularType) {
    TabularType tabType = (TabularType)openType;
    String className = tabType.getTypeName();
    paramTypes[i] = className.substring(0, className.indexOf("<")); // strip any generics
    } else {
    paramTypes[i] = params[i].getType();
    }
    } else {
    paramTypes[i] = params[i].getType();
    }
    }

    return findMethod(mbeanInterface, op.getName(), paramTypes);
    }

    Posted by: roberth1 on May 17, 2007 at 02:52 AM

  • roberth1, are you looking for MXBean support? I'm guessing so, since you wouldn't expect to find an OpenMBeanParameterInfoSupport inside an MBeanInfo for a Standard MBean.

    Rewriting methodFor so it does the right thing for MXBeans is fairly hard. You need to do the MXBean mapping on the parameter types, and there's no public API to do that (yet). I would suggest cheating by requiring that there be no overloaded methods in the MBean interface (or at least, no pair of methods with the same name and the same number of arguments). Then you don't need to look at the parameter types.

    Posted by: emcmanus on May 18, 2007 at 01:55 AM

  • Eamonn,

    Thanks for all the great work on JMX. Once one start using the jmx, it is difficult to development components without it. It makes java components so much easier to manage.
    Back to the topic of your article and as many others mentioned, it doesn't actually solve the "don't repeat rule". Javadoc is best suited for documenting and we should leverage that to show the same documentation on jmx agent view.

    I believe the easier thing to do is to use a doclet which can generate a subclass of StandardMBean, with all javadoc information automatically copied over and deliver to jmx agents. So anytime one adds a new parameter to managed operation or changes the documentation, it doesn't have to be maintained with other annotations or duplicate set of documentation.

    I have written such doclet and have been using successfully in our own projects and it has been working like a charm.

    http://civiccenterdr.blogspot.com/2007/10/standard-mbeans-and-documentation.html

    Posted by: brsanthu on October 29, 2007 at 11:07 AM





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