JMX + Spring + Commons Attributes

In my previous log4j blog I had used JMX to expose management interfaces to change log4j levels dynamically. If you look at the JBoss JMX console in that blog you will see that the parameter names are named as p1 and p2. Not very helpful. By default Spring uses reflection to expose the public methods of the MBean. Parameter names get thrown away once classes are compiled to byte code. No use of it further. Therefore there is no metadata available to print friendlier names in the JMX console.

I could use commons modeler project and externalize my MBean information completely to an XML file OR I can continue to use Spring and use Spring provided commons attribute annotations. Lets get straight to an example.

Note: The use of commons attributes is to attach metadata to classes or methods and have them available at runtime. If you are using Java 5 then commons attributes is not the best approach. Use Java 5 annotations since thats the standard now. Commons attributes is useful if you are still in JDK 1.4 world. Spring has Java 5 annotation equivalents for the same stuff described below.

package com.aver.jmx;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;

/**
* @@org.springframework.jmx.export.metadata.ManagedResource
* (description="Manage Log4j settings.", objectName="myapp:name=Log4jLevelChanger")
*/
public class Log4jLevelChanger {
/**
* @@org.springframework.jmx.export.metadata.ManagedOperation (description="Change the log level for named logger.")
* @@org.springframework.jmx.export.metadata.ManagedOperationParameter(index=0,name="loggerName",description="Logger name")
* @@org.springframework.jmx.export.metadata.ManagedOperationParameter(index=1,name="level",description="Log4j level")
*
* Sets the new log level for the logger and returns the updated level.
*
* @param loggerName logger name (like com.aver)
* @param level level such as debug, info, error, fatal, warn or trave
* @return current log level for the named logger
*/
public String changeLogLevel(String loggerName, String level) {
// validate logger name
if (StringUtils.isEmpty(loggerName)) {
return "Invalid logger name '" + loggerName + "' was specified.";
}

// validate level
if (!isLevelValid(level)) {
return "Invalid log level " + level + " was specified.";
}

// change level
switch (Level.toLevel(level).toInt()) {
case Level.DEBUG_INT:
Logger.getLogger(loggerName).setLevel(Level.DEBUG);
break;
case Level.INFO_INT:
Logger.getLogger(loggerName).setLevel(Level.INFO);
break;
case Level.ERROR_INT:
Logger.getLogger(loggerName).setLevel(Level.ERROR);
break;
case Level.FATAL_INT:
Logger.getLogger(loggerName).setLevel(Level.FATAL);
break;
case Level.WARN_INT:
Logger.getLogger(loggerName).setLevel(Level.WARN);
break;
}
return getCurrentLogLevel(loggerName);
}

/**
* @@org.springframework.jmx.export.metadata.ManagedOperation (description="Return current log level for named logger.")
* @@org.springframework.jmx.export.metadata.ManagedOperationParameter(index=0,name="loggerName",description="Logger name")
*
* Returns the current log level for the specified logger name.
*
* @param loggerName
* @return current log level for the named logger
*/
public String getCurrentLogLevel(String loggerName) {
// validate logger name
if (StringUtils.isEmpty(loggerName)) {
return "Invalid logger name '" + loggerName + "' was specified.";
}
return Logger.getLogger(loggerName) != null && Logger.getLogger(loggerName).getLevel() != null ? loggerName
+ " log level is " + Logger.getLogger(loggerName).getLevel().toString() : "unrecognized logger "
+ loggerName;
}

private boolean isLevelValid(String level) {
return (!StringUtils.isEmpty(level) && ("debug".equalsIgnoreCase(level) || "info".equalsIgnoreCase(level)
|| "error".equalsIgnoreCase(level) || "fatal".equalsIgnoreCase(level) || "warn".equalsIgnoreCase(level) || "trace"
.equalsIgnoreCase(level)));
}
}


You will need to include an additional step in your Ant build file.
<target name="compileattr">
<taskdef resource="org/apache/commons/attributes/anttasks.properties">
<classpath refid="classpath"/>
</taskdef>

<!-- Compile to a temp directory: Commons Attributes will place Java source there. -->
<attribute-compiler destdir="${gen}">
<fileset dir="${src}" includes="**/jmx/*.java"/>
</attribute-compiler=>
</target>

This attribute compiler generates additional java classes that will hold the metadata information provided in the attribute tags in the sample code. Make sure to compile the generated source along with your normal compile.

Finally Spring must be configured to use commons attributes. Once this step is done you are in business.
  <bean id="httpConnector" class="com.sun.jdmk.comm.HtmlAdaptorServer" init-method="start">
<property name="port" value="12001"/>
</bean>

<bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean"/>

<bean id="exporter" class="com.nasd.proctor.jmx.CommonsModelerMBeanExporter" lazy-init="false">
<property name="beans">
<map>
<entry key="myapp:name=Log4jLevelChanger" value-ref="com.aver.jmx.Log4jLevelChanger" />
<entry key="myapp:name=httpConnector"><ref bean="httpConnector"/></entry>
</map>
</property>
<property name="server" ref="mbeanServer"/>
<property name="assembler">
<ref local="assembler"/>
</property>
</bean>

<bean id="attributeSource" class="org.springframework.jmx.export.metadata.AttributesJmxAttributeSource">
<property name="attributes">
<bean class="org.springframework.metadata.commons.CommonsAttributes"/>
</property>
</bean>

<bean id="assembler" class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler">
<property name="attributeSource">
<ref local="attributeSource"/>
</property>
</bean>

I will not go into any explanations here. The AttributesJmxAttributeSource and MetadataMBeanInfoAssembler beans are the ones that configure Spring to use the commons attribute generated classes and thereby the metadata is available at runtime. Take a look at the generated attribute java source and you will quickly realize what commons-attributes is doing. By default Spring uses org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler which uses reflection to expose all of the public methods as JMX attributes/operations. With commons-attributes you can pick and choose which methods get exposed.

The only other thing to note; In the XML configuration above I start a JMX server (from Sun). Whether you want to use Sun's reference JMX console or use a commercial tool (or open source tool likeJManage) is your choice. Similarly I chose to create my own MBeanServer. You can tag along with your containers MBean Server if you prefer.



 del.icio.us  Stumbleupon  Technorati  Digg 

 

What did you think of this article?




Trackbacks
  • No trackbacks exist for this entry.
Comments

Leave a comment

Submitted comments will be subject to moderation before being displayed.

 Enter the above security code (required)

 Name

 Email (will not be published)

 Website

Your comment is 0 characters limited to 3000 characters.