The JMX API includes the possibility to create "Dynamic MBeans", whose management interface is determined at run time. When might that be useful? Here's an example.

In the JMX forum on the Sun Developer Network, Athar asks how to load a properties file in a dynamic MBean. I think that's an excellent question, because it's exactly the example I usually use for "Runtime" Dynamic MBeans.

What I call a "Runtime" Dynamic MBean is one whose management interface you cannot determine by looking at the source code. Obviously Standard MBeans aren't like this, because you can just look at WhateverMBean.java to see what the management interface is going to be. This is still true for MBeans constructed using the StandardMBean class, and it's also true for MXBeans.

It isn't necessarily true for Dynamic MBeans. A Dynamic MBean is a Java object of a class that implements the DynamicMBean interface. This interface includes a method getMBeanInfo(). A class that implements DynamicMBean can construct the MBeanInfo object that it returns from getMBeanInfo() however it likes. It can even return a different MBeanInfo every time it is called!

This flexibility is almost never necessary. Nearly always, when you create a Dynamic MBean, it is because you want to add extra information to the MBeanInfo, or because you want to implement the logic to get an attribute or call an operation in some particular way. Just like dynamic code generation, my advice if you are considering making a Runtime Dynamic MBean is to think really hard about whether you couldn't redesign things so that the interface is known at compile time. The problem with an MBean interface only known at run time is that it's hard for a client to interact with it. Suppose your client wants to call getAttribute on your MBean. The only way it can know what attributes are available is to call getMBeanInfo beforehand. If the MBean's interface can change as it is running, even this isn't guaranteed to work!

However, there are some cases where it makes a certain amount of sense to have a Runtime Dynamic MBean, and Athar's question suggests one of them. Suppose you have a properties file containing configuration for your application, and you'd like to expose its contents for management, so that you can see the values of configuration items, and perhaps change them as the application is running. The obvious way to do this is to have a ConfigurationManagementMBean that is linked to the properties file.

Every time you change your app to add a new configuration item, you'll need to add it to the initial configuration file, and you'll need to add code to interpret it. But it would be a pain to have to add a new attribute explicitly to the ConfigurationManagementMBean as well. So this argues for one of two approaches:

  1. The ConfigurationManagementMBean has one big attribute that is a Properties, say, or aMap<String,String> if you're using MXBeans. This is workable, but in practice it is very clumsy. In order to change a configuration item, you'll have to get the complete set of items, change the item within it, and write the result back to the MBean. Furthermore, JConsole doesn't currently support changing an item in the middle of a Properties or Mapattribute.
  2. The ConfigurationManagementMBean has one attribute per configuration item, or in other words per property. It determines these attributes at run time by reading the properties file. So it's a Runtime Dynamic MBean!

If you adopt the second approach, then JConsole looking at your ConfigurationManagementMBean might look like this:

JConsole looking at a Runtime Dynamic MBean 

I'll present the code to implement this below. A few things are worth noting. First of all, the DynamicMBean interface is a little bit clunky, in particular the getAttributes and (especially) setAttributes methods. The problem that generates this clunkiness is what to do if one of the attributes to be set produces an error. Should you throw an exception? If so, have any of the other attributes been set? The cleanest solution would be to say that setAttributes is an all-or-nothing operation: either it sets all of the given attributes, or it sets none of them and throws an exception. However, the designers of the JMX API felt that this was a harsh constraint to put on MBean writers. What's more it is not at all obvious how it should apply to Standard MBeans. So instead, setAttributes returns an AttributeList containing the attributes that were actually set. The caller needs to check that this contains all the values that were supposed to be set, and react appropriately if not.

The code doesn't let you set a value for a property that was not already present. The MBean Server does not check that the attribute name in setAttribute is present in the MBeanInfo. It is up to the MBean to do that. An MBean could choose to accept such a name, which in this case would allow you to define new properties. But I think it would be better to achieve that in some other way, for example an explicit addProperty operation.

In addition to one attribute per property, I've defined an operation reload which reloads the properties from the file. If there are properties in the file that were not present before, then they will appear as new attributes. Notice that adding an operation requires you both to mention it ingetMBeanInfo and to recognize it ininvoke. If there are many operations, you might want to consider getting the StandardMBean class to do some of the work for you.

Finally, every time you change a property the code updates the configuration file. The way it does this is intended to be a safe way to update a file. It writes a new properties file in the same directory, then renames it over the original. On most operating systems, renaming is atomic, so even if your app is interrupted in the middle of this operation, you will end up with either the old file or the new file, but not with a missing or partially-written file.

package propertymanager;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Properties;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.management.Attribute;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.DynamicMBean;
import javax.management.InvalidAttributeValueException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.ReflectionException;

public class PropertyManager implements DynamicMBean {
    private final String propertyFileName;
    private final Properties properties;
    
    public PropertyManager(String propertyFileName) throws IOException {
        this.propertyFileName = propertyFileName;
        properties = new Properties();
        load();
    }

    public synchronized String getAttribute(String name)
    throws AttributeNotFoundException {
        String value = properties.getProperty(name);
        if (value != null)
            return value;
        else
            throw new AttributeNotFoundException("No such property: " + name);
    }

    public synchronized void setAttribute(Attribute attribute)
    throws InvalidAttributeValueException, MBeanException, AttributeNotFoundException {
        String name = attribute.getName();
        if (properties.getProperty(name) == null)
            throw new AttributeNotFoundException(name);
        Object value = attribute.getValue();
        if (!(value instanceof String)) {
            throw new InvalidAttributeValueException(
                    "Attribute value not a string: " + value);
        }
        properties.setProperty(name, (String) value);
        try {
            save();
        } catch (IOException e) {
            throw new MBeanException(e);
        }
    }

    public synchronized AttributeList getAttributes(String[] names) {
        AttributeList list = new AttributeList();
        for (String name : names) {
            String value = properties.getProperty(name);
            if (value != null)
                list.add(new Attribute(name, value));
        }
        return list;
    }

    public synchronized AttributeList setAttributes(AttributeList list) {
        Attribute[] attrs = (Attribute[]) list.toArray(new Attribute[0]);
        AttributeList retlist = new AttributeList();
        for (Attribute attr : attrs) {
            String name = attr.getName();
            Object value = attr.getValue();
            if (properties.getProperty(name) != null && value instanceof String) {
                properties.setProperty(name, (String) value);
                retlist.add(new Attribute(name, value));
            }
        }
        try {
            save();
        } catch (IOException e) {
            return new AttributeList();
        }
        return retlist;
    }

    public Object invoke(String name, Object[] args, String[] sig)
    throws MBeanException, ReflectionException {
        if (name.equals("reload") &&
                (args == null || args.length == 0) &&
                (sig == null || sig.length == 0)) {
            try {
                load();
                return null;
            } catch (IOException e) {
                throw new MBeanException(e);
            }
        }
        throw new ReflectionException(new NoSuchMethodException(name));
    }
    
    public synchronized MBeanInfo getMBeanInfo() {
        SortedSet<String> names = new TreeSet<String>();
        for (Object name : properties.keySet())
            names.add((String) name);
        MBeanAttributeInfo[] attrs = new MBeanAttributeInfo[names.size()];
        Iterator<String> it = names.iterator();
        for (int i = 0; i < attrs.length; i++) {
            String name = it.next();
            attrs[i] = new MBeanAttributeInfo(
                    name,
                    "java.lang.String",
                    "Property " + name,
                    true,   // isReadable
                    true,   // isWritable
                    false); // isIs
        }
        MBeanOperationInfo[] opers = {
            new MBeanOperationInfo(
                    "reload",
                    "Reload properties from file",
                    null,   // no parameters
                    "void",
                    MBeanOperationInfo.ACTION)
        };
        return new MBeanInfo(
                this.getClass().getName(),
                "Property Manager MBean",
                attrs,
                null,  // constructors
                opers,
                null); // notifications
    }

    private void load() throws IOException {
        InputStream input = new FileInputStream(propertyFileName);
        properties.load(input);
        input.close();
    }

    private void save() throws IOException {
        String newPropertyFileName = propertyFileName + "$$new";
        File file = new File(newPropertyFileName);
        OutputStream output = new FileOutputStream(file);
        String comment = "Written by " + this.getClass().getName();
        properties.store(output, comment);
        output.close();
        if (!file.renameTo(new File(propertyFileName))) {
            throw new IOException("Rename " + newPropertyFileName + " to " +
                    propertyFileName + " failed");
        }
    }
}