Monday, October 13, 2008

Querying in JMX 2.0

While some portions of forthcoming Java SE 7 still appear to be controversial, the JMX 2 portion (JSR-255) of Java SE 7 seems to be solidifying nicely. In this blog entry, I will look at some of the new JMX querying capabilities in JMX 2 such as the JMX Query Language.

JMX Specification Lead Eamonn McManus has posted several blog entries on JMX 2 including JMX Namespaces Now Available in JDK 7, Dropping Proposed Features from the New JMX API, and Playing with the JMX 2.0 API. Eamonn wrote about the JMX 2 Query Language several months ago in the blog entry A Query Language for the JMX API. My blog entry here will expand upon the concepts highlighted in that blog entry. Additional information on advancements in JMX 2 related to querying MBeans can be found in the 2008 JavaOne presentation JMX Technology Update (see slide 39).

To better appreciate what JMX 2 offers in terms of querying capabilities, one must first understand how JMX queries are performed in JMX 1.x. I'll look at the approaches to querying registered MBeans in JMX 1.x before moving onto demonstrating advancements in JMX 2.

When I think about JMX 1.x querying capabilities, I generally think of JMX 1.x querying falling into three different types. The first type of JMX MBeans querying is accomplished by querying against MBean ObjectNames and ObjectName patterns. The second type of JMX 1.x MBeans querying is accomplished by querying against characteristics of the MBeans such as MBean attributes, operation return values, or even the MBean's class type. The third type of MBean querying is a combination of the first two where the matching MBeans have matching ObjectNames or ObjectName patterns and also have matching characteristics.

To demonstrate querying the MBean server for registered MBeans, it is important to register some MBeans with the MBean server. While I could have used the JVM-provided MBeans to show off querying capabilities, I thought it might be a little more interesting if I used custom MBeans instead. However, JMX querying capabilities can be used with the JVM-provided MBeans in the same way they are used against custom MBeans.

The next two code listings show the interface and class for the first custom MBean. I'll reuse the particular MBean defined by this interface and class multiple times as different instances of the same MBean class with different ObjectNames and different characteristics.

SimpleMBeanIf.java


package dustin.jmx.query;

import javax.management.MXBean;

/**
* Simple MBean interface.
*
* @author Dustin
*
* @see <a href="http://marxsoftware.blogspot.com">Dustin's Software Development
* Cogitations and Speculations</a>
*/
@MXBean
public interface SimpleMBeanIf
{
/**
* Possible status values.
*
* @see <a href="http://marxsoftware.blogspot.com">Dustin's Software
* Development Cogitations and Speculations</a>
*/
public enum StatusEnum
{
/** Failed status. */
FAILED ("Failed"),
/** Successful status. */
SUCCESSFUL ("Successful"),
/** Unknown status. */
UNKNOWN ("Unknown");

/** Alternative String representation. */
private String representation;

/** Constructor accepting alternate String representation. */
StatusEnum(final String newString) {this.representation = newString;}

/**
* Provide a String representation as an alternate to that returned by
* toString().
*
* @return Alternative String representation.
*/
public String getStringRepresentation()
{
return this.representation;
}
};

/**
* Provide my resource status.
*
* @return My resource status.
*/
public StatusEnum getStatus();

/**
* Set/change my resource status.
*
* @param newStatus New value for my resource status.
*/
public void setStatus(final StatusEnum newStatus);

/**
* Provide status as String. This will be treated as an operation in JMX
* rather than simply as an attribute as the get/set methods are.
*
* @return Status as String.
*/
public String retrieveStatusString();

/**
* Provide my priority.
*
* @return My priority.
*/
public int getPriority();

/**
* Set/change my priority.
*
* @param newPriority My new priority value.
*/
public void setPriority(final int newPriority);

/**
* Provide the Name of the Resource that I manage/monitor.
*
* @return Name of the resource that I manage/monitor.
*/
public String getResourceName();
}



SimpleMBean.java


package dustin.jmx.query;

/**
* Implementation of a simple MBean.
*
* @author Dustin
*
* @see <a href="http://marxsoftware.blogspot.com">Dustin's Software Development
* Cogitations and Speculations</a>
*/
public class SimpleMBean implements SimpleMBeanIf
{
/** Status of the underlying resource. */
private StatusEnum status = StatusEnum.UNKNOWN;

/** Priority of this resource. */
private int priority;

/** Name of the resource I manage/monitor. */
private String resourceName;

/**
* Constructor accepting arguments to populate my state.
*
* @param newStatus Status value to use for my status.
* @param newPriority New value for my priority.
* @param newResourceName Name of the resource that I manage/monitor.
*/
public SimpleMBean(
final StatusEnum newStatus,
final int newPriority,
final String newResourceName)
{
this.status = newStatus;
this.priority = newPriority;
this.resourceName = newResourceName;
}

/**
* Provide my resource status.
*
* @return My resource status.
*/
@Override
public StatusEnum getStatus()
{
return this.status;
}

/**
* Set/change my resource status.
*
* @param newStatus New value for my resource status.
*/
@Override
public void setStatus(final StatusEnum newStatus)
{
this.status = newStatus;
}

/**
* Provide status as String. This will be treated as an operation in JMX
* rather than simply as an attribute as the get/set methods are.
*
* @return Status as String.
*/
@Override
public String retrieveStatusString()
{
return this.status.getStringRepresentation();
}

/**
* Provide my priority.
*
* @return My priority.
*/
@Override
public int getPriority()
{
return this.priority;
}

/**
* Set/change my priority.
*
* @param newPriority My new priority value.
*/
@Override
public void setPriority(final int newPriority)
{
this.priority = newPriority;
}

/**
* Provide the Name of the Resource that I manage/monitor.
*
* @return Name of the resource that I manage/monitor.
*/
@Override
public String getResourceName()
{
return this.resourceName;
}
}


The above interface and implementation class make this an MXBean thanks to the @MXBean annotation on the interface. For my second MBean class, shown in the next two code listings, I'll also use an MXBean, but this one will be implemented the more traditional way following the naming convention pattern of the interface having the same name as the implementation plus an MXBean suffix.


AnotherMXBean.java


package dustin.jmx.query;

/**
* Another MBean example used in querying. This one will primarily be used to
* demonstrate the Java SE 6 Query.isInstanceOf(StringValueExp) functionality.
*
* @see <a href="http://marxsoftware.blogspot.com">Dustin's Software Development
* Cogitations and Speculations</a>
*/
public interface AnotherMXBean
{
/**
* Provide my status string.
*
* @return My status string.
*/
public String getStatusString();

/**
* Set/change my status string.
*
* @param newStatusString New status string.
*/
public void setStatusString(final String newStatusString);

/**
* Same as getStatusString(), but enables JMX Clients to see this as an
* operation rather than as an attribute getter.
*
* @return Status String.
*/
public String provideStatusString();
}



Another.java


package dustin.jmx.query;

/**
* This MBean is intended to be an MBean defined by a different class than the
* class used for most MBeans in the querying examples. This MBean will be used
* to demonstrate the Query.isInstanceOf(StringValueExp) that was introduced
* in Java SE 6.
*
* @see <a href="http://marxsoftware.blogspot.com">Dustin's Software Development
* Cogitations and Speculations</a>
*/
public class Another implements AnotherMXBean
{
/** Status string attribute of MBean. */
private String statusString;

/**
* Provide my status string.
*
* @return My status string.
*/
@Override
public String getStatusString()
{
return this.statusString;
}

/**
* Set/change my status string.
*
* @param newStatusString New status string.
*/
@Override
public void setStatusString(final String newStatusString)
{
this.statusString = newStatusString;
}

/**
* Same as getStatusString(), but enables JMX Clients to see this as an
* operation rather than as an attribute getter.
*
* @return Status String.
*/
@Override
public String provideStatusString()
{
return this.statusString;
}
}



With two MBean classes defined above (SimpleMBeanIf and AnotherMXBean), it is time to register instances of these MBean class types with the MBean server. The following class, SimpleServer, does this. It registers multiple instances of the SimpleMBean with different ObjectNames and different MBean characteristics. It also registers one instance of the AnotherMXBean to demonstrate querying by MBean class type. Here is the code listing for SimpleServer.


SimpleServer.java


package dustin.jmx.query.server;

import dustin.jmx.PrintUtility;
import dustin.jmx.query.Another;
import dustin.jmx.query.SimpleMBean;
import dustin.jmx.query.SimpleMBeanIf.StatusEnum;
import static dustin.jmx.JmxQueryConstants.JMX_SERVICE_URL_STR;

import java.io.Console;
import java.io.IOException;

import java.lang.management.ManagementFactory;
import java.net.MalformedURLException;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;

/**
* Simple JMX application intended to register some JMX MBeans with the MBean
* Server so that different JMX queries can be performed against them by a JMX
* client.
*
* @author Dustin
*
* @see <a href="http://marxsoftware.blogspot.com">Dustin's Software Development
* Cogitations and Speculations</a>
*/
public class SimpleServer
{
/**
* Obtain the platform JMX server and register some example MBeans with it.
*/
public static void configureJmxServerAndRegisterMBeans()
{
final MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
final JMXConnectorServer connectorServer = startConnectorServer(mbeanServer);
registerMBeanWithJMXServer(
new SimpleMBean(StatusEnum.SUCCESSFUL, 1, "Dustin-Host-1"),
"dustin:type=simple,name=One",
mbeanServer);
registerMBeanWithJMXServer(
new SimpleMBean(StatusEnum.SUCCESSFUL, 2, "Dustin-Host-2"),
"dustin:type=complex,name=One",
mbeanServer);
registerMBeanWithJMXServer(
new SimpleMBean(StatusEnum.UNKNOWN, 3, "Dustin-Application-1"),
"dustin:type=simple,name=Two",
mbeanServer);
registerMBeanWithJMXServer(
new SimpleMBean(StatusEnum.FAILED, 2, "Dustin-Application-2"),
"dustin:type=complex,name=Two",
mbeanServer);
registerMBeanWithJMXServer(
new Another(),
"dustin:type=alternate,name=Another",
mbeanServer);
waitForInput();
stopConnectorServer(connectorServer);
}

/**
* Start the JSR-160 JMX connector server.
*
* @param mbeanServer MBean Server for which connector server should be started.
* @return JMXConnectorServer for provided MBean server; may be null if there
* is an issue trying to set up the JMX Connector Server.
*/
private static JMXConnectorServer startConnectorServer(
final MBeanServer mbeanServer)
{
final String serviceUrl = JMX_SERVICE_URL_STR;
JMXConnectorServer connectorServer = null;
try
{
final JMXServiceURL jmxServiceUrl = new JMXServiceURL(serviceUrl);
connectorServer =
JMXConnectorServerFactory.newJMXConnectorServer(
jmxServiceUrl,
null,
mbeanServer);
connectorServer.start();
}
catch (MalformedURLException badJmxServiceUrl)
{
System.err.print(
"ERROR trying to create JMX server connector with service URL "
+ serviceUrl + ":\n" + badJmxServiceUrl.getMessage() );
}
catch (IOException ioEx)
{
System.err.println(
"ERROR trying to access server connector.\n"
+ ioEx.getMessage() );
}
return connectorServer;
}

/**
* Stop the provided JSR-160 JMX Connector Server.
*
* @param jmxConnectorServer JMX Connector Server to be stopped.
*/
public static void stopConnectorServer(final JMXConnectorServer jmxConnectorServer)
{
try
{
jmxConnectorServer.stop();
}
catch (IOException ioEx)
{
System.err.println(
"IOException encountered trying to close JMXConnectorServer:\n"
+ ioEx.getMessage() );
}
}

/**
* Register the provided MBean-compatible object in the provided MBean
* Server under the provided ObjectName.
*
* @param mbeanToRegister MBean-compatible object to be registed with MBeanServer.
* @param mbeanObjectNameStr ObjectName to be used to register MBean under.
* @param mBeanServer MBean Server on which the provided MBean should be registered.
*/
private static void registerMBeanWithJMXServer(
final Object mbeanToRegister,
final String mbeanObjectNameStr,
final MBeanServer mBeanServer)
{
try
{
final ObjectName objectName = new ObjectName(mbeanObjectNameStr);
mBeanServer.registerMBean(mbeanToRegister, objectName);
}
catch (MalformedObjectNameException badObjectNameEx)
{
System.err.println(
"The provided ObjectName [" + mbeanObjectNameStr + "] is improper:\n"
+ badObjectNameEx.getMessage() );
}
catch (InstanceAlreadyExistsException redundantMBeanEx)
{
System.err.println(
"You have already tried to register an MBean with the name "
+ mbeanObjectNameStr + ":\n" + redundantMBeanEx.getMessage() );
}
catch (MBeanRegistrationException mbeanRegistrationEx)
{
System.err.println(
"MBean registration exception encountered trying to register "
+ "MBean " + mbeanObjectNameStr + ":\n"
+ mbeanRegistrationEx.getMessage() );
}
catch (NotCompliantMBeanException badMBeanEx)
{
System.err.println(
"The MBean [" + mbeanToRegister.getClass().getName() + "]"
+ "with ObjectName " + mbeanObjectNameStr + " is NOT a compliant "
+ "MBean:\n" + badMBeanEx.getMessage() );
}
}

/**
* Wait until user presses ENTER. The purpose of this is to allow JMX MBeans
* to remain registed in MBean server while client interacts with it.
*/
public static void waitForInput()
{
final Console console = System.console();
if ( console == null )
{
System.err.println(
"Please use Java SE 6 in an environment with a console.");
System.exit(-1);
}
console.printf("Press ENTER to exit.");
final String unusedReturnString = console.readLine();
}

/**
* Main function for setting up JMX MBeans that can be queried.
*
* @param arguments The command line arguments; none anticipated.
*/
public static void main(String[] arguments)
{
PrintUtility.writeAttributionInformation(
"SimpleServer in JMX Querying Example",
System.out);
configureJmxServerAndRegisterMBeans();
}
}



With the MBeans for the example registered with the MBean server, it is time to turn to the client. The client class is called SimpleClient. I will show its code listing first and then make some key observations about that code afterward.


SimpleClient.java


package dustin.jmx.query.client;

import dustin.jmx.PrintUtility;
import dustin.jmx.query.SimpleMBeanIf.StatusEnum;
import static dustin.jmx.JmxQueryConstants.JMX_SERVICE_URL_STR;

import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.management.MBeanServerConnection;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.Query;
import javax.management.QueryExp;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

/**
* Simple JMX Client intended to demonstrate querying with JMX 1.0 style queries
* and JMX 2.0 style queries.
*
* @author Dustin
*
* @see <a href="http://marxsoftware.blogspot.com">Dustin's Software Development
* Cogitations and Speculations</a>
*/
public class SimpleClient
{
final static String NEW_LINE = System.getProperty("line.separator");
final static String MAIN_HEADER_SEPARATOR =
"======================================================================"
+ NEW_LINE;
final static String SUB_HEADER_SEPARATOR =
" ----------------------------------------------------------------"
+ NEW_LINE;

/**
* Demonstrate how to query JMX MBeans using JMX 1.0 style querying.
*
* @param mbsc MBeanServerConnection.
*/
public static void demonstrateJmxOneQuerying(
final MBeanServerConnection mbsc)
{
printMainHeader("Querying with JMX 1.x", System.out);

// === Use ObjectName pattern matching to query by ObjectNames ===

printSubHeader(
"Querying with ObjectName Pattern Matching (dustin:type=simple,*)",
System.out);
final List<String> matchedMBeansByObjectNamePattern =
queryMBeansViaObjectNamePattern(
"dustin:type=simple,*",
mbsc);
printContentsOfListOfStrings(matchedMBeansByObjectNamePattern, System.out);


// === Use QueryExp to query by JMX 1.x query expression ===

final int examplePriority = 2;
printSubHeader(
"Querying with QueryExp (using null for all ObjectNames) and a "
+ "priority of " + examplePriority,
System.out);

final List<String> matchedMBeansByQueryExp =
queryMBeansWithQueryExpForIntegerAttribute(
null, // All registed MBeans considered
"Priority", // Attribute needs to be capitalized (not "priority")
examplePriority,
mbsc);
printContentsOfListOfStrings(matchedMBeansByQueryExp, System.out);


// === Get MBeans matching priority and resource name.

final String exampleResourceName = "Dustin-Host-2";
printSubHeader(
"Querying JMX MBeans based on Priority ("
+ examplePriority + ") and Resource Name ("
+ exampleResourceName + ")",
System.out);

final List<String> matchedMBeansByCompoundQueryExp =
queryMBeansWithQueryExpForIntAttrAndStringAttr(
null, // All registered MBeans considered
"Priority", // Attribute needs to be capitalized
examplePriority,
"ResourceName", // Attribute needs to be capitalized
exampleResourceName,
mbsc);
printContentsOfListOfStrings(matchedMBeansByCompoundQueryExp, System.out);


// === Get MBeans matching priority and status.

final String exampleStatus = StatusEnum.FAILED.toString();
printSubHeader(
"Querying JMX MBeans based on Priority ("
+ examplePriority + ") and Status ("
+ exampleStatus + ")",
System.out);

final List<String> matchedMBeansByCompoundQueryEnumExp =
queryMBeansWithQueryExpForIntAttrAndStringAttr(
null, // All registered MBeans considered
"Priority", // Attribute needs to be capitalized
examplePriority,
"Status", // Attribute needsto be capitalized
exampleStatus,
mbsc);
printContentsOfListOfStrings(matchedMBeansByCompoundQueryEnumExp, System.out);


// === Get all registered MBeans that are of class AnotherMXBean.

printSubHeader(
"Querying MBeans based on MBean class type",
System.out);

final List<String> matchedMBeansByClassType =
queryMBeansByClassType("dustin.jmx.query.AnotherMXBean", mbsc);
printContentsOfListOfStrings(matchedMBeansByClassType, System.out);


// === Get all registered MBeans.

printSubHeader(
"Querying with two nulls passed to queryMBeans to get all MBeans",
System.out);

final List<String> allMBeans = queryAllMBeans(mbsc);
printContentsOfListOfStrings(allMBeans, System.out);
}

/**
* Query MBeans using ObjectName pattern matching available since JMX 1.0.
*
* @param objectNamePattern Pattern to use for matching ObjectNames.
* @param mbsc MBeanServerConnection.
*/
private static List<String> queryMBeansViaObjectNamePattern(
final String objectNamePatternStr,
final MBeanServerConnection mbsc)
{
final List<String> matchedMBeans =
queryMBeanServerForMatchingMBeansNames(
objectNamePatternStr,
null, // no query expression
mbsc);

return matchedMBeans;
}

/**
* Query MBeans using QueryExp and Query classes and looking for an
* Integer value.
*
* @param objectNameStr ObjectName pattern to be used in MBeans query.
* @param queryAttrStr Name of attribute to be used in query.
* @param queryValueInt Integer value to be used in query for attribute.
* @param mbsc MBean Server Connection.
*/
private static List<String> queryMBeansWithQueryExpForIntegerAttribute(
final String objectNameStr,
final String queryAttrStr,
final int queryValueInt,
final MBeanServerConnection mbsc)
{
final QueryExp queryExp =
Query.eq(
Query.attr(queryAttrStr),
Query.value(queryValueInt) );

final List<String> matchedMBeans =
queryMBeanServerForMatchingMBeansNames(
objectNameStr,
queryExp,
mbsc);

return matchedMBeans;
}

/**
* Query MBeans using JMX 1.x QueryExp approach with a combination of an
* integer comparison and a String comparison.
*
* @param objectNameStr ObjectName pattern to be used in MBeans query.
* @param queryIntAttrStr Name of integer attribute to be used in query.
* @param queryIntValue Integer value to be used in query for integer attribute.
* @param queryStrAttrStr Name of String attribute to be used in query.
* @param queryStrValue String value to be used in query for String attribute.
* @param mbsc MBean Server Connection.
*/
private static List<String> queryMBeansWithQueryExpForIntAttrAndStringAttr(
final String objectNameStr,
final String queryIntAttrStr,
final int queryIntValue,
final String queryStrAttrStr,
final String queryStrValue,
final MBeanServerConnection mbsc)
{
/*
* The three statements below that set up the compound JMX 1.x query
* could also be written as separate statements like this:
*
* final QueryExp queryIntExp =
* Query.eq(
* Query.attr(queryIntAttrStr),
* Query.value(queryIntValue) );
* final QueryExp queryStringExp =
* Query.eq(
* Query.attr(queryStrAttrStr),
* Query.value(queryStrValue) );
* final QueryExp queryExp =
* Query.and(queryIntExp, queryStringExp);
*/
final QueryExp queryExp =
Query.and(
Query.eq(
Query.attr(queryIntAttrStr),
Query.value(queryIntValue) ),
Query.eq(
Query.attr(queryStrAttrStr),
Query.value(queryStrValue) ) );

final List<String> matchedMBeans =
queryMBeanServerForMatchingMBeansNames(
objectNameStr,
queryExp,
mbsc);

return matchedMBeans;
}

/**
* Query MBeans using JMX 1.x QueryExp approach using the Query.isInstanceOf()
* feature introduced with Java SE 6.
*
* @param mbeanClassTypeStr MBean's class's type in String format.
* @param mbsc MBean Server Connection.
* @return Names of registered MBeans of the provided class type.
*/
private static List<String> queryMBeansByClassType(
final String mbeanClassTypeStr,
final MBeanServerConnection mbsc)
{
final QueryExp queryExp =
Query.isInstanceOf(Query.value(mbeanClassTypeStr));
final List<String> matchingMBeans =
queryMBeanServerForMatchingMBeansNames(
null,
queryExp,
mbsc);
return matchingMBeans;
}

/**
* Query for all MBeans.
*
* @param mbsc MBean Server Connection.
* @return Names of MBeans returned from query.
*/
private static List<String> queryAllMBeans(final MBeanServerConnection mbsc)
{
final List<String> matchedMBeans =
queryMBeanServerForMatchingMBeansNames(
null, null, mbsc);

return matchedMBeans;
}

/**
* Demonstrate how to query JMX MBeans using JMX 2.0 style querying.
*
* See Javadoc documentation on Java SE 7 (and JMX 2) at
* http://download.java.net/jdk7/docs/api/.
*/
public static void demonstrateJmxTwoQuerying(
final MBeanServerConnection mbsc)
{
printMainHeader("Querying with JMX 2.0", System.out);

printSubHeader(
"JMX 2.0 Single Attribute Query (Priority = 2)",
System.out);
final QueryExp simpleSingleAttrQuery = Query.fromString("Priority = 2");
final List<String> matchingMBeansSimpleSingleQuery =
queryMBeanServerForMatchingMBeansNames(null, simpleSingleAttrQuery, mbsc);
printContentsOfListOfStrings(matchingMBeansSimpleSingleQuery, System.out);

printSubHeader(
"JMX 2.0 Compound Query (Priority = 2 and Status = '"
+ StatusEnum.FAILED.toString() + "')",
System.out);
final QueryExp compoundAttrQuery =
Query.fromString(
"Priority = 2 and Status = '"
+ StatusEnum.FAILED.toString() + "'");
final List<String> matchingMBeansPriorityAndStatus =
queryMBeanServerForMatchingMBeansNames(null, compoundAttrQuery, mbsc);
printContentsOfListOfStrings(matchingMBeansPriorityAndStatus, System.out);
}

/**
* Query the MBean Server for MBeans matching the conditions specified in
* the provided ObjectName pattern and the QueryExp.
*
* @param objectNameStr Object Name (or pattern) of matching MBeans.
* @param queryExp Query expression for matching MBeans.
* @param mbsc MBean Server Connection.
* @return Names of MBeans matching the provided pattern and query expression.
*/
private static List<String> queryMBeanServerForMatchingMBeansNames(
final String objectNameStr,
final QueryExp queryExp,
final MBeanServerConnection mbsc)
{
final List<String> matchedMBeans = new ArrayList<String>();
try
{
ObjectName objectName = null;
if ( objectNameStr != null )
{
objectName = new ObjectName(objectNameStr);
}
final Set<ObjectInstance> matchingMBeans =
mbsc.queryMBeans(objectName, queryExp);
for ( final ObjectInstance mbeanName : matchingMBeans )
{
matchedMBeans.add(mbeanName.getObjectName().getCanonicalName());
}
}
catch (IOException ioEx)
{
System.err.println(
"IOException encountered while attempting to query MBeans:\n"
+ ioEx.getMessage() );
}
catch (MalformedObjectNameException badObjectNameEx)
{
System.err.println(
"The ObjectName " + objectNameStr + " is not valid:\n"
+ badObjectNameEx.getMessage() );
}
return matchedMBeans;
}

/**
* Start the JMX Connector Client.
*
* @return MBeanServerConnection to connector server.
*/
private static MBeanServerConnection startConnectorClient()
{
MBeanServerConnection mbsc = null;
final String jmxServiceUrl = JMX_SERVICE_URL_STR;
try
{
final JMXServiceURL jmxUrl = new JMXServiceURL(jmxServiceUrl);
final JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl);
mbsc = jmxConnector.getMBeanServerConnection();
}
catch (MalformedURLException badServiceUrlEx)
{
System.err.println(
"The JMXServiceURL [" + jmxServiceUrl + "] is improper:\n"
+ badServiceUrlEx.getMessage() );
}
catch (IOException ioEx)
{
System.err.println(
"IOException encountered while trying to start JMX Connector Client:\n"
+ ioEx.getMessage() );
}
return mbsc;
}

/**
* Print main separation header to provided OutputStream.
*
* @param headerString Text string to be included in header separator.
* @param out OutputStream to which to write the header separator; will be
* written to System.out if an IOException is encountered while trying to
* write to this OutputStream.
*/
private static void printMainHeader(
final String headerString,
final OutputStream out)
{
try
{
out.write((NEW_LINE + MAIN_HEADER_SEPARATOR).getBytes());
out.write(MAIN_HEADER_SEPARATOR.getBytes());
out.write(("== " + headerString + NEW_LINE).getBytes());
out.write(("== (" + PrintUtility.BLOG_URL + ")" + NEW_LINE).getBytes());
out.write(MAIN_HEADER_SEPARATOR.getBytes());
out.write(MAIN_HEADER_SEPARATOR.getBytes());
}
catch (IOException ioEx)
{
System.out.println(MAIN_HEADER_SEPARATOR);
System.out.println("== " + headerString + NEW_LINE);
System.out.println("== (" + PrintUtility.BLOG_URL + ")" + NEW_LINE);
System.out.println(MAIN_HEADER_SEPARATOR);
}
}

/**
* Print secondary separation header to provided OutputStream.
*
* @param subHeaderString Text string to be included in header separator.
* @param out OutputStream to which to write the header separator; will be
* written to System.out if an IOException is encountered while trying to
* write to this OutputStream.
*/
private static void printSubHeader(
final String subHeaderString,
final OutputStream out)
{
try
{
out.write((NEW_LINE + SUB_HEADER_SEPARATOR).getBytes());
out.write((" -- " + subHeaderString + NEW_LINE).getBytes());
out.write((" -- (" + PrintUtility.BLOG_URL + ")" + NEW_LINE).getBytes());
out.write(SUB_HEADER_SEPARATOR.getBytes());
}
catch (IOException ioEx)
{
System.out.println(NEW_LINE + SUB_HEADER_SEPARATOR);
System.out.println(" -- " + subHeaderString + NEW_LINE);
System.out.println(" -- (" + PrintUtility.BLOG_URL + ")" + NEW_LINE);
System.out.println(SUB_HEADER_SEPARATOR);
}
}

/**
* Write the contents ofthe provided list of Strings to the provided
* OutputStream.
*
* @param listToPrint List of Strings to be printed.
* @param out OutputStream to be written to.
*/
private static void printContentsOfListOfStrings(
final List<String> listToPrint,
final OutputStream out)
{
try
{
for ( String itemToPrint : listToPrint )
{
out.write((itemToPrint + NEW_LINE).getBytes());
}
}
catch (IOException ioEx)
{
for ( String itemToPrint : listToPrint )
{
System.out.println(itemToPrint);
}
}
}

/**
* The main function for running the client that demonstrates JMX querying.
*
* @param arguments Command-line arguments; none anticipated.
*/
public static void main(final String[] arguments)
{
PrintUtility.writeAttributionInformation(
"SimpleClient in JMX Querying Example",
System.out);
final MBeanServerConnection mbsc = startConnectorClient();
demonstrateJmxOneQuerying(mbsc);
demonstrateJmxTwoQuerying(mbsc);
}
}



The SimpleClient class actually demonstrates JMX 1.x and JMX 2 MBeans querying, but I'll focus on JMX 1.x-style querying first. The method demonstrateJmxOneQuerying() in the SimpleClient class calls individual methods that each demonstrate a different type of JMX 1.x querying.

All JMX queries (1.x and 2) boil down eventually to making a call on the MBeanServerConnection class's queryMBeans(ObjectName,QueryExp) method for MBeans themselves or queryNames(ObjectName,QueryExp) for names of the matching MBeans. The SimpleClient.queryMBeanServerForMatchingMBeansNames method makes use of the first of these two methods to perform all queries used in this blog entry's examples, both for JMX 1.x and JMX 2 style querying. I actually could have used the second method (for querying names) because the names are all I am returning from this method, but I wanted to demonstrate the ability to get the entire MBean object.

The first example of a JMX 1.x MBean query queries only on ObjectName. This is done by passing the ObjectName pattern ultimately to the MBeanServerConnection.queryMBeans method for the ObjectName parameter and passing null for the QueryExp parameter. By passing null for the QueryExp parameter, only the ObjectName or pattern in the ObjectName is used in querying JMX MBeans and any other characteristics are insignificant in the query.

In this example, the ObjectName pattern was "dustin:type=simple,*". The Javadoc document for ObjectName explains ObjectName patterns and what is allowed in detail. For this entry, the most important observation is that the pattern "dustin:type=simple,*" means that any MBean with an ObjectName that includes "dustin:type=simple" will be returned. The asterisk is a wildcard. A query by ObjectName can also be for exact ObjectName rather than for a pattern. The results of running this particular SimpleClient.queryMBeansViaObjectNamePattern method with the ObjectName pattern of "dustin:type=simple,*" is shown next.



From the above screen snapshot, we see that the two returned MBeans do indeed have ObjectNames that include "dustin:type=simple" in them. Another observation here is that ObjectName is "smarter" than just a simple String. It actually treats "dustin:name=One,type=simple" and "dustin:name=Two,type=simple" as both instances matching the wildcard pattern "dustin:type=simple,*" even though there is a "name=" portion in between the "dustin:" and "type=simple" portions.

The next style of JMX 1.x MBean query demonstrated in SimpleClient is not providing an ObjectName pattern at all, but instead querying by an attribute of the MBean. In this case, the int attribute "priority" is the differentiating characteristic of the MBean query. The QueryExp instance is built up to query on the attribute "Priority" (capitalization is important) with a value of "2".

Other examples of querying MBeans by attributes (including by String and Enum attributes and by a combination of attributes) are also shown in the SimpleClient. One of the most interesting is the preparation of a QueryExp for a JMX 1.x-style query. This is where JMX 2 and its JMX Query Language shine, so it is important here to show the code required to do it in JMX 1.x. The method SimpleClient.queryMBeansWithQueryExpForIntAttrAndStringAttr demonstrates how a compound QueryExp can be constructed. In this case, it builds up a query based on an integer attribute of the MBean and a String attribute of the MBean. For convenience and emphasis, that method is reproduced here:


/**
* Query MBeans using JMX 1.x QueryExp approach with a combination of an
* integer comparison and a String comparison.
*
* @param objectNameStr ObjectName pattern to be used in MBeans query.
* @param queryIntAttrStr Name of integer attribute to be used in query.
* @param queryIntValue Integer value to be used in query for integer attribute.
* @param queryStrAttrStr Name of String attribute to be used in query.
* @param queryStrValue String value to be used in query for String attribute.
* @param mbsc MBean Server Connection.
*/
private static List<String> queryMBeansWithQueryExpForIntAttrAndStringAttr(
final String objectNameStr,
final String queryIntAttrStr,
final int queryIntValue,
final String queryStrAttrStr,
final String queryStrValue,
final MBeanServerConnection mbsc)
{
/*
* The three statements below that set up the compound JMX 1.x query
* could also be written as separate statements like this:
*
* final QueryExp queryIntExp =
* Query.eq(
* Query.attr(queryIntAttrStr),
* Query.value(queryIntValue) );
* final QueryExp queryStringExp =
* Query.eq(
* Query.attr(queryStrAttrStr),
* Query.value(queryStrValue) );
* final QueryExp queryExp =
* Query.and(queryIntExp, queryStringExp);
*/
final QueryExp queryExp =
Query.and(
Query.eq(
Query.attr(queryIntAttrStr),
Query.value(queryIntValue) ),
Query.eq(
Query.attr(queryStrAttrStr),
Query.value(queryStrValue) ) );

final List<String> matchedMBeans =
queryMBeanServerForMatchingMBeansNames(
objectNameStr,
queryExp,
mbsc);

return matchedMBeans;
}


The output for running these JMX 1.x-style queries is shown in the next screen snapshot.




Before moving onto JMX 2 querying, I want to focus on one additional characteristic that can be used for querying JMX MBeans in JMX 1.x. That approach is to query on the MBean class type of the MBean instance. This is done with the Query.isInstanceOf(StringValueExp) method introduced with Java SE 6.

The SimpleClient.queryMBeansByClassType method demonstrates use of the Query.isInstanceOf method and the most important statement of that method is reproduced here:


final QueryExp queryExp =
Query.isInstanceOf(Query.value(mbeanClassTypeStr));


This leads to the output shown in the next screen snapshot.



The JMX 1.x querying examples so far have demonstrated querying based on ObjectName and querying based on MBean characteristic (including MBean class type). If you wish to query all registered MBeans, you should pass null to both the ObjectName and QueryExp parameters of the MBeanServerConnection.queryMBeans() method. When this is done, output like that shown in the next screen snapshot is observed.



Up until this point, I've only used JMX 1.x querying techniques. So why are improvements needed in JMX 2? The most significant improvement to JMX 2 addresses the complexity of building up QueryExp instances as shown above. In JMX 1.x style, I had to build the composite query of an integer attribute and String attribute like this:


/*
* The three statements below that set up the compound JMX 1.x query
* could also be written as separate statements like this:
*
* final QueryExp queryIntExp =
* Query.eq(
* Query.attr(queryIntAttrStr),
* Query.value(queryIntValue) );
* final QueryExp queryStringExp =
* Query.eq(
* Query.attr(queryStrAttrStr),
* Query.value(queryStrValue) );
* final QueryExp queryExp =
* Query.and(queryIntExp, queryStringExp);
*/
final QueryExp queryExp =
Query.and(
Query.eq(
Query.attr(queryIntAttrStr),
Query.value(queryIntValue) ),
Query.eq(
Query.attr(queryStrAttrStr),
Query.value(queryStrValue) ) );


The beauty of JMX 2 and its JMX Query Language is that the above can be written much more simply as:


final QueryExp compoundAttrQuery =
Query.fromString(
"Priority = 2 and Status = '"
+ StatusEnum.FAILED.toString() + "'");


In the JMX 2 case, I used an enum instead of a string, but the principle is the same. The JMX 2 Query Language makes the code much more succinct and readable. While JMX 1.x queries were "inspired" by SQL queries, JMX 2 queries are much more obviously SQL-based than JMX 1.x queries.

The next screen snapshot demonstrates the JMX 2 query output as generated by SimpleClient.



It is important to note that the SimpleClient code shown above will not run in Java SE 6 because of the JMX 2 dependencies. To run SimpleClient, one must either remove (or comment out) the JMX 2 specific methods or one must download the latest released JMX drop from the Java 7 distribution. The steps for doing the latter are covered in Playing with the JMX 2.0 API and Playing with JMX 2.0 Annotations.

For building the code above, I added the JMX2 JAR to my NetBeans project classpath and prepended it to my NetBeans Boot ClassPath as shown in the next screen snapshot.



I ran the examples from the terminal window and so needed to ensure that the JMX2 JAR was first on that boot class path as well. This was done with the command

java -Xbootclasspath/p:C:\jmx2\jmx.jar -cp dist\JMXQueryExample.jar dustin.jmx.query.client.SimpleClient


I did not need to do this for the SimpleServer because it did not have anything JMX2-specific in it.

Finally, I used a couple utility classes in this blog entry that I will include here for convenience. Following these code listings, I will bring this rather lengthy blog entry to a conclusion.

JmxQueryConstants.java


package dustin.jmx;

/**
* Constant used in client and server portions of JMX application intended to
* demonstrate querying with JMX in JMX 1.x and JMX 2.0.
*
* @author Dustin
*
* @see <a href="http://marxsoftware.blogspot.com">Dustin's Software Development
* Cogitations and Speculations</a>
*/
public class JmxQueryConstants
{
public static final String JMX_SERVICE_URL_STR =
"service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi";
}



PrintUtility.java


package dustin.jmx;

import java.io.IOException;
import java.io.OutputStream;

/**
* Utility class for JMX Querying example that prints out attribution details.
*
* @author Dustin
*
* @see <a href="http://marxsoftware.blogspot.com">Dustin's Software Development
* Cogitations and Speculations</a>
*/
public class PrintUtility
{
public static final String BLOG_URL = "http://marxsoftware.blogspot.com/";

/**
* Write attributtion information to provided OutputStream.
*
* @param workName Name of work to which attribution applies.
* @param out OutputStream to which attribution information should be'
* written.
*/
public static void writeAttributionInformation(
final String workName,
final OutputStream out)
{
final String newLine = System.getProperty("line.separator");
final String headerSeparator =
"==================================================================="
+ newLine;
try
{
out.write(headerSeparator.getBytes());
out.write(("===== " + workName + newLine).getBytes());
out.write(("===== (" + BLOG_URL + ")" + newLine).getBytes());
out.write(headerSeparator.getBytes());
}
catch (IOException ioEx)
{
System.out.println(headerSeparator);
System.out.println("===== " + workName);
System.out.println("===== {" + BLOG_URL + ")");
System.out.println(headerSeparator);
}
}
}



JMX 2 offers simpler JMX MBean querying when the querying needs to be done against attributes of the registered MBeans. ObjectName matching was already relatively straightforward in terms of syntax, so it is not surprising that the main effort in JMX 2 improvements to querying were on the QueryExp side of things. For a list of some of the other syntax of the JMX 2 query language as well as additional improvements in querying JMX and potential gotchas associated with Query.isInstanceOf (and the new dotted attribute syntax), see A Query Language for the JMX API. It is currently anticipated that JMX 2 will be part of Java SE 7.

3 comments:

kpb said...

Hi Dustin,

do you know if Sun's com.sun.jdmk.comm.HtmlAdaptorServer
is also capable of accessing a JMX server on a different(!) host, e.g., to alter a Log4J log level in my application...?

--Klaus

Dustin said...

Klaus,

I have not used the HTML Adaptor in a long time (other than for my recent post on the Adaptor) because I prefer the JMX connectors. That being stated, it doesn't seem feasible to me to directly use the HTML Adaptor to monitor or manage a JMX server on a different host because of the tight coupling between the Adaptor and the MBean server it is associated with. The only way that I can see being able to use the HTML Adaptor associated with one MBean server to affect another MBean server is to use the cascading concept (hand-coded or OpenDMK) where an MBean on the first JMX server would expose operations and attributes to the web browser via HtmlAdaptorServer and would then itself turn around and act as a JMX client to call the different host's platform-provided MXBean for altering logging. The problem now is that you'd need to expose the ability to change Log4J logging level from the other machine via JMX. This is already done for java.util.logging thanks to the Logging MXBean, but I'm not aware of anything as standard for changing Log4j levels. Your MBean that was on the first host and was accessible by web browser via the HTML Adaptor Server, would need to, as a JMX client, use a JSR-160 connector to connect to the other host to access its MBean server to affect its logging level.

For ideas on how to configure Log4j remotely similar to how the Logging MXBean is used, you might check out the reference JMX Meets Log4j (Spring Framework/2005).

kpb said...

Hi Dustin,
thanks for your explanations. I think I will just make this experiment and try to use the HtmlAdapter being contacted from a Browser running on a different host.
The argument mentioned in the "JMX meets Log4J" post that this will make no sence because logs are written to LOCAL files cannot convince me. Changing log levels remotely CAN MAKE SENCe because we can ask our customer later on to SEND us the LOG e.g. via email ZIP.


On the other hand I have started using org.apache.log4j.jmx.LoggerDynamicMBean; together with the RMI connector and jManage. BTW: Your post that introduced jManage helped me a lot ;-)

--Klaus