(Small) Bug with Oracle’s HTML/JMX Interface

Posted by & filed under , , .

 network pc monitorsI posted before an article about how to use Oracle’s (well, Sun’s, since it was started really before the Oracle acquisition) HTML/JMX agent to monitor your apps via JMX here. For those of you who went ahead and decided to use that interface, you probably noticed a small (but rather annoying bug) in that component — or maybe you haven’t, in which case, you will know how to avoid it if you read this.

It’s not a big deal to be honest, as you are about to see — especially if you are dealing with clean code or code you’ve written yourself and understand the functionality ahead. But if you are trying to use the “help” facilities of Oracle’s JMX/HTML console you might find this rather annoying.

As a side note, I’d be more than glad to submit a patch for this somewhere, but bloody Oracle has discontinued this component 🙁 So if you find out where I can submit the patch, drop me a line and let me know — much appreciated in advance!

Anyway, back to the main point — this manifests itself if you use the DynamicMBean interface to “decorate” your beans when exporting them via JMX. Typically if you go to inspect your bean in the JMX console, you would see a screen like this:

 typical html jmx console

As it happens, this is the actual screen you get for the Java bean I’m about to talk about. But either way, you will get a set of attributes and a set of operations. Some operations (or attributes) are self-explanatory, if the name was chosen properly — so you can figure out at a glance what they do (e.g. increment() must increment a value); however, there are some that might be vague or misleading (for instance, what does validate() mean? Does it expect a certain format or value???). Normally in such situation when you are coding and encounter such a method would consult the JavaDoc, but you’re not coding: you’re looking at JMX console, there is not JavaDoc, in most cases! The DynamicMBean interface tries to help with that — as it allows the developer to specify descriptions about methods, attributes, parameters and so on. And the HTML console from Oracle normally uses that information — for instance if you click on a method name or attribute it will pop up an alert and display that description — such that if the description contains information about validation formats for instance you can tell after a click right away what value/format you should be using for that method.

However, this is where this annoying problem occurs — and like I said, it’s quite small, but annoying when you find yourself in that situation.

To start with, let’s consider a simple Java bean: it wraps up an integer (let’s make that AtomicInteger so we’re thread safe!) and allows for the int value to be read and set. It also allows for the same value to be returned as a String and a boolean (remember old style C where 0 means false and everything else is true? 😉 ). And it offers 2 operations: increment() and decrement(). Pretty simple to implement, right? Something like this:

/**
 * A simple DynamicMBean to demonstrate the problem with Sun's JMX tools. This
 * bean simply wraps up an integer and allows it to be incremented/decremented
 * and retrieve its value as an int, boolean and String. (The boolean follows
 * the convention 0=false, everything else =true).
 *
 * @author Liviu Tudor http://about.me/liviutudor
 */
public class SimpleDynamicMBean {
 /** The actual value we wrap up in this bean. */
 private AtomicInteger value;
 
 /**
  * Default constructor. Initializes {@link #value} with zero.
  */
 public SimpleDynamicMBean() {
  value = new AtomicInteger(0);
 }
 
 /**
  * Retrieve {@link #value current value} as an integer.
  *
  * @return Value of {@link #value} as an int
  */
 public int getIntValue() {
  return value.intValue();
 }
 
/**
 * Sets {@link #value current value} to the value passed in.
 *
 * @param value
 *            New value to be set in {@link #value}
 */
 public void setIntValue(int value) {
  this.value.set(value);
 }
 
 /**
  * Retrieve {@link #value current value} and interprets it as a boolean:
  * 0=false, anything else=true.
  *
  * @return Value of {@link #value} as a boolean
  */
 public boolean getBooleanValue() {
  return (value.intValue() != 0);
 }
 
 /**
  * Retrieve {@link #value} as a string.
  *
  * @return Value of {@link #value} as a string
  */
 public String getStringValue() {
  return String.valueOf(value.intValue());
 }
 
 /**
  * Simply increments {@link #value}.
  */
 public void increment() {
  value.incrementAndGet();
 }
 
 /**
  * Simply decrements {@link #value}.
  */
 public void decrement() {
  value.decrementAndGet();
 }
}

I’m sure there can be other implementations but let’s stick to this one for now. To expose this via JMX we can simply use the JMX MBean notation convention and define a SimpleDynamicMBean interface and change our class name to just SimpleDynamic — however, when looking at the above screen in the browser, one can ask: “Does increment() increment the value by one or by more than one? Does the boolean value follow the C convention or is it based on something else?”. As I said, in such situations we can provide some descriptive information to help the user understand these things. And that’s where DynamicMBean info comes in handy — this is the code adapted to implement this interface (I’m not going to go into the whole discussion again about getAttribute/getAttributes, but you can find all of that in this post if you’re interested):

/**
 * A simple DynamicMBean to demonstrate the problem with Sun's JMX tools. This
 * bean simply wraps up an integer and allows it to be incremented/decremented
 * and retrieve its value as an int, boolean and String. (The boolean follows
 * the convention 0=false, everything else =true).
 *
 * @author Liviu Tudor http://about.me/liviutudor
 */
public class SimpleDynamicMBean implements DynamicMBean {
 /** The actual value we wrap up in this bean. */
 private AtomicInteger value;
 
 /**
  * Default constructor. Initializes {@link #value} with zero.
  */
 public SimpleDynamicMBean() {
  value = new AtomicInteger(0);
 }
 
 /**
  * Retrieve {@link #value current value} as an integer.
  *
  * @return Value of {@link #value} as an int
  */
 public int getIntValue() {
  return value.intValue();
 }
 
 /**
  * Sets {@link #value current value} to the value passed in.
  *
  * @param value
  *            New value to be set in {@link #value}
  */
 public void setIntValue(int value) {
  this.value.set(value);
 }
 
 /**
  * Retrieve {@link #value current value} and interprets it as a boolean:
  * 0=false, anything else=true.
  *
  * @return Value of {@link #value} as a boolean
  */
 public boolean getBooleanValue() {
  return (value.intValue() != 0);
 }
 
 /**
  * Retrieve {@link #value} as a string.
  *
  * @return Value of {@link #value} as a string
  */
 public String getStringValue() {
  return String.valueOf(value.intValue());
 }
 
 /**
  * Simply increments {@link #value}.
  */
 public void increment() {
  value.incrementAndGet();
 }
 
 /**
  * Simply decrements {@link #value}.
  */
 public void decrement() {
  value.decrementAndGet();
 }
 
 /* * * * * * * JMX * * * * * * */
 /** Name of the property implemented by {@link #getIntValue()}. */
 private static final String ATTR_INT_VALUE    = "IntValue";
 
 /** Name of the property implemented by {@link #getStringValue()}. */
 private static final String ATTR_STRING_VALUE = "StringValue";
 
 /** Name of the property implemented by {@link #getBooleanValue()}. */
 private static final String ATTR_BOOL_VALUE   = "BooleanValue";
 
 /** Name of the operation implemented by {@link #increment()}. */
 private static final String OP_INCREMENT      = "increment";
 
 /** Name of the operation implemented by {@link #decrement()}. */
 private static final String OP_DECREMENT      = "decrement";
 
 /**
  * {@inheritDoc}
  * <p>
  * If <code>attribute</code> is one of {@link #ATTR_BOOL_VALUE},
  * {@link #ATTR_INT_VALUE} or {@link #ATTR_STRING_VALUE} then it returns the
  * corresponding property, otherwise it throws an
  * <code>AttributeNotFoundException</code>.
  */
 @Override
 public Object getAttribute(String attribute) throws AttributeNotFoundException {
  if (ATTR_INT_VALUE.equals(attribute)) {
   return getIntValue();
  } else if (ATTR_STRING_VALUE.equals(ATTR_STRING_VALUE)) {
   return getStringValue();
  } else if (ATTR_BOOL_VALUE.equals(attribute)) {
   return getBooleanValue();
  }
  // unrecognized attribute
  throw new AttributeNotFoundException(attribute);
 }
 
 @Override
 public AttributeList getAttributes(String[] attributes) {
  AttributeList list = new AttributeList(attributes.length);
  for (String a : attributes) {
   try {
    Object value = getAttribute(a);
    list.add(value);
   } catch (Exception e) {
    System.err.println("Asked to retrieve value for " + a + " failed:" + e.getMessage());
   }
  }
  return list;
 }
 
 /**
  * {@inheritDoc}
  * <p>
  * This is our concern, here. The value supplied for
  * {@link #ATTR_BOOL_VALUE} has an apostrophe and also the value supplied
  * for {@link #OP_INCREMENT}. As such, these will cause JavaScript errors in
  * the HTML console.
  */
 @Override
 public MBeanInfo getMBeanInfo() {
  MBeanAttributeInfo[] attributes = new MBeanAttributeInfo[] {
   new MBeanAttributeInfo(ATTR_INT_VALUE, int.class.getName(), "Retrieve the integer value", true,
    true, false),
   new MBeanAttributeInfo(ATTR_STRING_VALUE, String.class.getName(),
    "Retrieve the value as a string", true, false, false),
   new MBeanAttributeInfo(ATTR_BOOL_VALUE, boolean.class.getName(),
    "Is this equivalent to 'true' or 'false'?", true, false, false)};
  MBeanOperationInfo[] operations = new MBeanOperationInfo[] {
   new MBeanOperationInfo(OP_INCREMENT, "Increment the value. Increment is done in 1's.", null,
    void.class.getName(), MBeanOperationInfo.ACTION),
   new MBeanOperationInfo(OP_DECREMENT, "Decrement the value. Decrement is done in units of 1.",
    null, void.class.getName(), MBeanOperationInfo.ACTION)};
  return new MBeanInfo(SimpleDynamicMBean.class.getName(),
   "Simple implementation of DynamicMBean which wraps up an integer", attributes, null, operations,
   null);
 }
 
 /**
  * {@inheritDoc}
  * <p>
  * It ignores all the parameters apart from <code>actionName</code>. If this
  * parameter is equal to either {@link #OP_DECREMENT} or
  * {@link #OP_INCREMENT} then it calls the corresponding function, otherwise
  * it throws a <code>ReflectionException</code>.
  */
 @Override
 public Object invoke(String actionName, Object[] params, String[] signature) throws ReflectionException {
  if (OP_INCREMENT.equals(actionName)) {
   increment();
   return null;
  } else if (OP_DECREMENT.equals(actionName)) {
   decrement();
   return null;
  }
 
  // unrecognized method
  throw new ReflectionException(new IllegalArgumentException("Method not found: " + actionName));
 }
 
 /**
  * {@inheritDoc}
  * <p>
  * If attribute name supplied equals {@link #ATTR_INT_VALUE} then it invokes
  * {@link #setIntValue(int)} with the given value. Otherwise it throws a
  * <code>ReflectionException</code> since that is the only writeable
  * attribute. If the value supplied in is not an Integer it also throws an
  * <code>InvalidAttributeValueException</code>.
  */
 @Override
 public void setAttribute(Attribute attribute) throws ReflectionException, InvalidAttributeValueException {
  if (ATTR_INT_VALUE.equals(attribute.getName())) {
   if (!(attribute.getValue() instanceof Integer)) {
    throw new InvalidAttributeValueException("Need an integer for this attribute: " + attribute);
   }
   Integer i = (Integer) attribute.getValue();
   setIntValue(i.intValue());
  } else {
   throw new ReflectionException(new IllegalArgumentException("Can't set attribute:" + attribute));
  }
 }
 
 @Override
 public AttributeList setAttributes(AttributeList attributes) {
  AttributeList list = new AttributeList(attributes.size());
  for (Object o : attributes) {
   Attribute a = (Attribute) o;
   try {
    setAttribute(a);
    list.add(a);
   } catch (Exception e) {
    System.err.println("Asked to set value for " + a + " failed:" + e.getMessage());
   }
  }
  return list;
 }
}

As you can see, we’re providing some (basic) explanation in the getMBeanInfo() method about each method and attribute. And this info, as I said, is used by the console when you click on an attribute name or operation name — for instance, clicking on “Description of decrement” pops up this:

JMX console description of "decrement" method

Pretty helpful, right?

Now let’s figure out if the same applies to increment() method too — click on it! Err… nothing happens, right? Click again — still nothing! In fact you can click millions of time and still nothing happens! If you have Firebug or some other JavaScript debugging tool, all of a sudden you find out why the behaviour: a JavaScript error is triggered by the click event:

firebug console

Hmmmm look at that! An error caused by some unterminated string! Closer inspection shows that the JMX console, rather than generating a proper JavaScript event handler for the description label, it creates an anchor <a> and it assigns it the old fashioned JavaScript “protocol”:

javascript:alert('Increment the value. Increment is done in 1's.');

Basically the server-side code doesn’t escape the apostrophes already in the string — d’oh! Such a junior mistake!

Nevertheless, rather annoying, right?

So remember, when dealing with JMX and planning on using the Oracle console, always escape your apostrophes — or avoid them! In the above example, rather than specify “Increment the value. Increment is done in 1’s.” simply use a similar approach to what we did for decrement(): “Increment the value. Increment is done in units of 1.”. Or if you want to escape it: “Increment the value. Increment is done in 1\\’s.” — however, bear in mind that using this with other tools might render the ugly \’ you’ve used, so I’d advise against it.

Finally, the complete code for this, available for download here, if you want to try it for yourself: jmx-dynamic-mbean-bug.tar