Using JMX for Managing Application Properties in Production
JEE web applications that use the Spring container typically use property files for configuring the application for different customers, resources or boundary conditions. The property file is either read by a utility class or used by Spring’s popular PropertyPlaceholderConfigurer class to substitute properties in the spring application context at runtime.
When an application is deployed, the properties are (typically) loaded in the JVM in some sort of cache at startup and then used throughout the life of the application. Most applications provide a UI to manipulate the properties in production but more often than not, UI development cannot keep up with the rate at which properties are added to the application. The application is deployed and before long it becomes necessary to change a property value in production.. but.. oops.. no UI! The only option left is to change the property, rebuild and redeploy, which, of course means an outage for the users.
That’s when this relatively easy method of manipulating application properties via JMX at runtime may be useful. Using Spring JMX, we will expose properties to a controlled set of users so that they can be read and written to.
Broadly, the steps are:
- Implement the DynamicMBean interface to expose all the properties in an application as attributes.
- Start the MBean server on Tomcat
- Expose that implementation using Spring JMX and deploy the application.
- Use JConsole to access the MBean Server implementation and manipulate the values of the properties.
Let’s look at each in turn:
Implement DynamicMBean
Spring JMX has made it extremely easy to expose a bean via JMX. Through Spring’s proxy mechanism, we can expose any Spring managed bean as a Managed Bean. The bean does not even have to implement the MBean interface. However, if we are using Spring’s interface proxies (as against class based proxies), it may be a good idea to have the managed bean implement some interface.
Additionally, if it is necessary to transport data back to the JMX Client, then using an MXBean may be a better approach. (See here for a comparison of MBean to an MXBean).
For what we are trying to do here, we need to expose an arbitrary number of properties in the JMX client. The number (and names) of properties is not known at compile time; the JMX specification provides the DynamicMBean interface where the interface (to be exposed) is defined at runtime.
Let’s begin by defining a business interface like so:
public interface ManageableProperties{
public Properties getProperties();
public Collection getHiddenPropertiesList();
public Collection getReadOnlyPropertiesList();
}
This ‘business’ aspect of this interface tells the JMX client what properties need exposed, what properties are hidden and what are read-only.
Then define the class that is going to be managed via JMX and make that class implement the ManageableProperties interface.
public class JMXPropertyManager implements ManageableProperties {
@Override
public Properties getProperties(){
return (Properties)AcmePropertyManager.getProperties();
}
@Override
public Collection<String> getHiddenPropertiesList() {
Collection<String> c = new ArrayList<String>();
c.add("acme.db.password");
return c;
}
@Override
public Collection<String> getReadOnlyPropertiesList() {
Collection<String> c = new ArrayList<String>();
c.add("acme.allowConcurrentUsers");
return c;
}
}
Here the implementation of the interface is specifying what properties should be hidden (after all, you may not want to expose all properties in your application for administration), and what properties should be read-only.
Finally, let’s introduce the DynamicMBean interface to the same class (JMXPropertyManager). Doing that adds the following code:
private Properties properties;
public JMXPropertyManager(){
properties = this.getProperties();
}
private boolean isHidden(String key){
boolean boo = false;
Collection hiddenList = this.getHiddenPropertiesList();
for (String string : hiddenList) {
if (key.equalsIgnoreCase(string)){
boo = true;
break;
}
}
return boo;
}
private boolean isReadOnly(String key){
boolean boo = false;
Collection roList = this.getReadOnlyPropertiesList();
for (String string : roList) {
if (key.equalsIgnoreCase(string)){
boo = true;
break;
}
}
return boo;
}
@Override
public MBeanInfo getMBeanInfo() {
MBeanAttributeInfo[] attributes = new MBeanAttributeInfo[properties.size()];
MBeanInfo mBeanInfo = null;
MBeanOperationInfo[] operations = new MBeanOperationInfo[1];
int i = 0;
for (Iterator iterator = properties.keySet().iterator(); iterator.hasNext();) {
String key = (String) iterator.next();
if (this.isReadOnly(key) || isHidden(key)){
attributes[i] = new MBeanAttributeInfo
(key, "java.lang.String", key, true, false, false);
} else {
attributes[i] = new MBeanAttributeInfo
(key, "java.lang.String", key, true, true, false);
}
i++;
}
operations[0] = new MBeanOperationInfo("refreshCache", "Refresh Caches", null , null, MBeanOperationInfo.ACTION);
mBeanInfo = new MBeanInfo(this.getClass().getName(),
"Manage Properties", attributes, null, operations, null);
return mBeanInfo;
}
@Override
public Object getAttribute(String attribute) throws AttributeNotFoundException,
MBeanException, ReflectionException {
Object o = null;
if (isHidden(attribute)) {
o = "XXX-HIDDEN-VALUE-XXX";
} else {
o = properties.get(attribute);
}
return o;
}
@Override
public void setAttribute(Attribute attribute) throws AttributeNotFoundException,
InvalidAttributeValueException, MBeanException, ReflectionException {
String key = attribute.getName();
String value = (String)attribute.getValue();
properties.put(key, value);
}
@Override
public Object invoke(String actionName, Object[] params, String[] signature) throws MBeanException, ReflectionException {
Object ret = null;
if (actionName == null){
throw new RuntimeOperationsException( new IllegalArgumentException( "Operation name cannot be null"), "Cannot invoke a null operation");
}
if (actionName.equals("refreshCache")){
AcmePropertyManager.refreshCache();
}
//Returning null because we would like to avoid passing back a complex object
//to the JMXClient because we have not implemented a MXBean
return ret;
}
//---other unimplemeted methods of the DynamicMBean interface------
Here is where most of the action happens: The getMBeanInfo method creates a MBeanInfo object that has all the attributes (hidden, non-hidden, read-only and writable) defined. The In addition, an operation called refreshCaches is also defined.
Start the MBean server on Tomcat
Starting Tomcat with JMX enabled is explained here.
Deploying to a MBean Server
Thank God for Spring JMX! Deploying to an existing MBean Server was never easier! Just add the following to you Spring context:
<!-- this bean must not be lazily initialized if the exporting is to happen -->
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter" lazy-init="false">
<property name="beans">
<map>
<entry key="bean:name=acmeProperties" value-ref="jmxPropertyManager"/>
</map>
</property>
</bean>
<bean id="jmxPropertyManager" class="com.acme.JMXPropertyManager">
</bean>
Assuming that you are deploying your application to an application server that has an inbuilt (exactly one) MBeanServer, such at Tomcat, that’s all there is to it! The JMXPropertyManager is now available as a MBean as seen in the picture below.
Use JConsole to access the MBean Server
Accessing a Tomcat JMX Server is explained here.
Clicking on the bean | acmeProperties node shows us:
We see that the properties that are masked and read-only in our ‘business’ interface, ManegableProperties, correctly behave as specified.
That’s it. Have fun managing application properties in production!




