 |
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.
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:
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 Digg DZone Furl 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
|