Saturday, June 7, 2008

Remote JMX: With and Without Spring and Proxies

In this blog entry, I'll cover a simple set of remote JMX examples that demonstrate the use of remote JMX with focus on the client side. I'll show how use of the Spring Framework's JMX support and especially the use of proxies (with or without Spring) can make the client-side portion of remote JMX much easier.

For my example, I am only demonstrating one "server side" of the remote JMX example. I am using Spring to implement this server-side portion of the JMX RMI Connector because Spring makes it much easier. However, everything implemented here for the server side can be implemented without Spring. There is not as much difference in implementation details for the client side, so I will show examples both using and not using Spring on the client side.

The common JMX server-side code for my example is contained in three Java class files (a main executable class, a Spring-exposed bean class, and an interface that the Spring bean implements) along with a Spring XML configuration file. These four files constituting the server-side of the application are shown next.

spring-server.xml [Spring Configuration for Server]

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd">

<bean id="jmxStatusService"
class="jmx.spring.rmi.example.StatusBean"
p:status="Initializing" />

<util:map id="exposedMBeans">
<entry key="dustin:type=status,name=remoteJMX" value-ref="jmxStatusService" />
</util:map>

<bean class="org.springframework.jmx.export.MBeanExporter"
p:beans-ref="exposedMBeans"
p:assembler-ref="assembler" />

<util:list id="manageableInterfaces">
<value>jmx.spring.rmi.example.StatusBeanIF</value>
</util:list>

<bean id="assembler"
class="org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler"
p:managedInterfaces-ref="manageableInterfaces" />

<bean id="registry"
class="org.springframework.remoting.rmi.RmiRegistryFactoryBean"
p:port="1099" />

<bean id="serverConnector"
class="org.springframework.jmx.support.ConnectorServerFactoryBean"
depends-on="registry"
p:objectName="connector:name=rmi"
p:serviceUrl="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi" />

</beans>


StatusBeanIF.java (Server-side Spring-bean Interface)

package jmx.spring.rmi.example;

/**
* Interface to be used by Spring and by clients to access hosted MBean.
*
* @author Dustin
*/
public interface StatusBeanIF
{
public void setStatus(final String status);

public String getStatus();
}


StatusBean.java (Bean class exposed by Spring as MBean)

package jmx.spring.rmi.example;

/**
* Bean that will be served up by Spring as an MBean.
*
* @author Dustin
*/
public class StatusBean implements StatusBeanIF
{
private String status;

public StatusBean(){}

public void setStatus(final String status)
{
this.status = status;
}

public String getStatus()
{
return this.status;
}
}


ServerMain.java (Main Executable Class to Bootstrap Spring)

package jmx.spring.rmi.example;

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
* ServerMain Spring application executable.
*
* @author Dustin
*/
public class ServerMain
{
/**
* Run a simple long-running Spring-based application that can be managed via
* JMX. This class requires Java SE 6 or later because it uses the
* System.console().
*
* @param args The command line arguments
* @see java.lang.System
*/
public static void main(String[] args)
{
final ConfigurableApplicationContext context =
new ClassPathXmlApplicationContext("jmx\\spring\\rmi\\example\\spring-server.xml");
StatusBeanIF statusBean;
String status;
do
{
statusBean = (StatusBeanIF) context.getBean("jmxStatusService");
System.console().printf("Enter a status for this server: ");
status = System.console().readLine();
statusBean.setStatus(status);
}
while ( !status.toUpperCase().equals("EXIT") );
context.close();
}
}


As you can see in the code above, most of the work for exposing a simple bean as a JMX MBean is done in Spring's XML configuration (the first file listed). In that XML configuration file, the JMXServiceURL is highlighted in blue because this same URL will be needed for the client-side code, which is shown next.

The first type of client I'll demonstrate is a "straight JMX" client that does not use the Spring Framework. It does not matter that the server, as shown above, was ultimately implemented via the Spring Framework. The client can be run with or without Spring and I'll show both here.

ClientMain.java (Only Class Needed for JMX Client without Spring)

Because Spring is not involved, there is no XML file for configuration. To simplify things, I stuffed all of the client side functionality into a single class called ClientMain whose code listing is shown next.


package jmx.spring.rmi.example.client;

import java.io.IOException;
import java.net.MalformedURLException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanServerConnection;
import javax.management.MBeanServerInvocationHandler;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import jmx.spring.rmi.example.StatusBeanIF;

/**
* This simple example demonstrates a non-Spring remote JMX client.
*
* @author Dustin
*/
public class ClientMain
{
/**
* Demonstrate accessing a JMX agent remotely using RMI and without using
* a client-side proxy.
*
* @param mbsc MBeanServerConnection used to connect to server.
* @param objectName ObjectName pointing to MBeanServer-hosted MBean.
*/
public static void demonstrateClientJmxWithoutProxy(
final MBeanServerConnection mbsc,
final ObjectName objectName)
{
final String operationName = "getStatus";
try
{
final String status = (String) mbsc.invoke(objectName,
operationName,
null, // no parameter
null );
System.out.println("Status is: " + status);
}
catch (InstanceNotFoundException noMBeanInstanceFound) // checked
{
System.err.println( "ERROR: Could not find MBean with name "
+ objectName.toString() + " in MBeanServer:\n"
+ noMBeanInstanceFound.getMessage() );
}
catch (MBeanException mbeanEx) // checked
{
System.err.println( "ERROR: Exception encountered on invoked MBean:\n"
+ mbeanEx.getMessage() );
}
catch (ReflectionException reflectionEx) // checked
{
System.err.println( "ERROR trying to reflectively invoke remote MBean:\n"
+ reflectionEx.getMessage() );
}
catch (IOException ioEx) // checked
{
System.err.println( "ERROR trying to communicate with remote MBeanServer:\n"
+ ioEx.getMessage() );
}
}

/**
* Demonstrate accessing remote JMX agent via RMI and via a client-side proxy.
* This method uses MBeanServerInvocationHandler.newProxyInstance, but Java SE 6
* adds the easier-to-use JMX.newProxyInstance approach for obtaining a
* a client-side proxy.
*
* @param mbsc MBeanServerConnection used to connect to server.
* @param objectName ObjectName pointing to MBeanServer-hosted MBean.
*/
public static void demonstrateClientJmxWithProxy(
final MBeanServerConnection mbsc,
final ObjectName objectName)
{
final StatusBeanIF status = (StatusBeanIF)
MBeanServerInvocationHandler.newProxyInstance(
mbsc,
objectName,
StatusBeanIF.class,
false);
System.out.println("PROXY Says: The Status is " + status.getStatus());
}

/**
* The functionality here is available directly through the provided
* MBeanServerConnection with or without a proxy or reflection. Because it
* is information only at the MBeanServer level (not at each individually
* hosted MBean's level), no ObjectName is required.
*
* @param mbsc MBeanServerConnection for connecting to remote JMX agent.
*/
public static void demonstrateCommonMBeanServerInfo(
final MBeanServerConnection mbsc)
{
try
{
System.out.println( "MBean Count: " + mbsc.getMBeanCount() );
System.out.println( "MBean Default Domain: " + mbsc.getDefaultDomain() );
}
catch (IOException ioEx)
{
System.err.println( "ERROR encountered trying to get MBeanCount and "
+ "Default Domain for provided MBeanServer:\n"
+ ioEx.getMessage() );
}
}

public static void main(String[] aArguments)
{
final String objectNameStr = "dustin:type=status,name=remoteJMX";
final String jmxRmiStr =
"service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi";
try
{
final ObjectName objectName = new ObjectName(objectNameStr);
final JMXServiceURL jmxUrl = new JMXServiceURL(jmxRmiStr);
final JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl);
final MBeanServerConnection mbsc = jmxConnector.getMBeanServerConnection();

demonstrateCommonMBeanServerInfo(mbsc);
demonstrateClientJmxWithoutProxy(mbsc, objectName);
demonstrateClientJmxWithProxy(mbsc, objectName);
}
catch (MalformedObjectNameException malformedObjectNameEx)
{
System.err.println( "ERROR: Problem with object name ["
+ objectNameStr + "] instantiation:\n"
+ malformedObjectNameEx.getMessage() );
}
catch (MalformedURLException badUrl)
{
System.err.println( "ERROR: Problem with JMXServiceURL based on "
+ jmxRmiStr + ": " + badUrl.getMessage() );
}
catch (IOException ioEx)
{
System.err.println( "ERROR: IOException trying to connect to JMX "
+ "Connector Server: " + ioEx.getMessage() );
}
}
}


There is no Spring used in the code above. However, the code above does show how with regular JMX client-side code, one can connect to a JMX agent remotely either using direct reflection (ClientMain.demonstrateClientJmxWithoutProxy(MBeanServerConnection,ObjectName) method) or using a proxy (ClientMain.demonstrateClientJmxWithProxy(MBeanServerConnection,ObjectName) method). The main(String[]) function of this class performs all of the functionality required to connect to the JMX Server without Spring's help. This includes instantiating the JMXServiceURL and acquiring the JMXConnector to ultimately acquire an MBeanServerConnection. The MBeanServerConnection is required to connect to the JMX agent remotely whether direct reflection or proxies are used. To obtain this MBeanServerConnection, three checked exceptions must be handled.

To see how much easier it is to use a proxy as compared to direct reflection, one need only compare the lines of code in the method ClientMain.demonstrateClientJmxWithProxy (proxy) to the lines of code in the method ClientMain.demonstrateClientJmxWithoutProxy (direct reflection). The direct reflection approach requires handling of four checked exceptions compared to none required for the proxy approach. Also, as the number of operations desired to be run against a particular remote MBean increases, the additional complexity of the reflection approach becomes greater compared to the proxy approach.

The above single class demonstrated how one can access a remote JMX agent from a client. I'll now move onto how to accomplish the same functionality, but will take advantage of the Spring Framework on the client side this time.

My JMX Client assisted by Spring example has three files. The three files are the main Java class that bootstraps the Spring container, the Spring bean that interacts with the MBeanServer with direct reflection and with proxies, and the XML configuration file for the client application's Spring context.

spring-client.xml (XML Configuration for Client-side Spring Container)
Note that this XML file is much simpler than that used for the JMX server side.


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd">

<bean id="mBeanServerClient"
class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean"
p:serviceUrl="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi" />

<bean id="jmxSpringClient"
class="jmx.spring.rmi.example.springClient.SpringJmxClientBean"
p:mbeanServerConnection-ref="mBeanServerClient" />

</beans>


SpringClientMain.java

package jmx.spring.rmi.example.springClient;

import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
* Executable to run sample JMX remote client based on Spring framework.
*
* @author Dustin
*/
public class SpringClientMain
{
public static void main(final String[] arguments)
{
final ConfigurableApplicationContext context =
new ClassPathXmlApplicationContext(
"jmx\\spring\\rmi\\example\\springClient\\spring-client.xml");
SpringJmxClientBean jmxClient =
(SpringJmxClientBean) context.getBean("jmxSpringClient");
final String objectNameStr = "dustin:type=status,name=remoteJMX";
try
{
final ObjectName objectName = new ObjectName(objectNameStr);
jmxClient.demonstrateCommonMBeanServerInfo();
jmxClient.demonstrateClientJmxWithoutProxy(objectName);
jmxClient.demonstrateClientJmxWithProxy(objectName);
}
catch (MalformedObjectNameException malformedObjectNameEx)
{
System.err.println( "ERROR: Problem with object name ["
+ objectNameStr + "] instantiation:\n"
+ malformedObjectNameEx.getMessage() );
}
context.close();
}
}


SpringJmxClientBean.java

package jmx.spring.rmi.example.springClient;

import java.io.IOException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanServerConnection;
import javax.management.MBeanServerInvocationHandler;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import jmx.spring.rmi.example.StatusBeanIF;

/**
* Simple example of using Spring with JMX on the client side of a remote JMX
* connection.
*
* @author Dustin
*/
public class SpringJmxClientBean
{
private MBeanServerConnection mbsc;

public SpringJmxClientBean() {}

/**
* Allows Spring to inject MBeanServerConnection.
*
* @param mbsc MBeanServerConnection to be injected.
*/
public void setMbeanServerConnection(final MBeanServerConnection mbsc)
{
this.mbsc = mbsc;
}

/**
* Demonstrate accessing a JMX agent remotely using RMI and without using
* a client-side proxy.
*
* @param objectName ObjectName pointing to MBeanServer-hosted MBean.
*/
public void demonstrateClientJmxWithoutProxy(final ObjectName objectName)
{
final String operationName = "getStatus";
try
{
final String status = (String) mbsc.invoke(objectName,
operationName,
null, // no parameter
null );
System.out.println("Status (Spring) is: " + status);
}
catch (InstanceNotFoundException noMBeanInstanceFound) // checked
{
System.err.println( "ERROR: Could not find MBean with name "
+ objectName.toString() + " in MBeanServer:\n"
+ noMBeanInstanceFound.getMessage() );
}
catch (MBeanException mbeanEx) // checked
{
System.err.println( "ERROR: Exception encountered on invoked MBean:\n"
+ mbeanEx.getMessage() );
}
catch (ReflectionException reflectionEx) // checked
{
System.err.println( "ERROR trying to reflectively invoke remote MBean:\n"
+ reflectionEx.getMessage() );
}
catch (IOException ioEx) // checked
{
System.err.println( "ERROR trying to communicate with remote MBeanServer:\n"
+ ioEx.getMessage() );
}
}

/**
* Demonstrate accessing remote JMX agent via RMI and via a client-side proxy.
* This method uses MBeanServerInvocationHandler.newProxyInstance, but Java SE 6
* adds the easier-to-use JMX.newProxyInstance approach for obtaining a
* a client-side proxy.
*
* @param objectName ObjectName pointing to MBeanServer-hosted MBean.
*/
public void demonstrateClientJmxWithProxy(final ObjectName objectName)
{
final StatusBeanIF status = (StatusBeanIF)
MBeanServerInvocationHandler.newProxyInstance(
mbsc,
objectName,
StatusBeanIF.class,
false);
System.out.println("PROXY (Spring) Says: The Status is " + status.getStatus());
}

/**
* The functionality here is available directly through the provided
* MBeanServerConnection with or without a proxy or reflection. Because it
* is information only at the MBeanServer level (not at each individually
* hosted MBean's level), no ObjectName is required.
*/
public void demonstrateCommonMBeanServerInfo()
{
try
{
System.out.println( "MBean Count: " + mbsc.getMBeanCount() );
System.out.println( "MBean Default Domain: " + mbsc.getDefaultDomain() );
}
catch (IOException ioEx)
{
System.err.println( "ERROR encountered trying to get MBeanCount and "
+ "Default Domain for provided MBeanServer:\n"
+ ioEx.getMessage() );
}

}
}


Finally, note that the "common" features of the JMX MBeanServer (default domain, hosted MBean count, etc.) can be obtained directly from the MBeanServerConnection and therefore the decision whether to use proxy or direct reflection does not apply to those calls. Rather, one must decide between proxy or direct reflection when operating on remotely hosted MBeans themselves (not the host).

The above example of a JMX client with Spring's assistance demonstrates that one thing that Spring provides on the JMX client side is the ability to inject an MBeanServerConnection into the code via Spring's container and wiring. Once the MBeanServerConnection is obtained, everything else associated with accessing a remote JMX agent is essentially the same in the above example. Like the non-Spring client example, the Spring-assisted client accesses the server side both using reflection and using proxies. The proxy approach is essentially the same as for the non-Spring client shown earlier just as the reflection approach is also essentially the same for a Spring-assisted approach as for a non-Spring JMX client.

However, Spring can do even more for us on the client side using a proxy. Instead of using the MBeanServerInvocationHandler.newProxyInstance, we can configure Spring in its XML configuration file to inject the proxy into the code. This allows us to make the client side access of the remote JMX MBean even easier. The method for accessing the remote object now simplifies to this:


/**
* Demonstrate accessing remote JMX agent via RMI and via a client-side proxy
* with Spring injecting the proxy so that it does not need to be provided as
* a parameter.
*/
public void demonstrateClientJmxWithProxyInjected()
{
System.out.println( "PROXY (Spring-injected) Says: The Status is "
+ this.status.getStatus() );
}


As the simple code listing above demonstrates, it is really easy now to access the remote MBean. In fact, this code shows no obvious signs that an MBean is being accessed remotely. To make this work, a data member was added to the class to store the status and it was injected via Spring using the additional XML snippet shown next:


<bean id="remoteMBean"
class="org.springframework.jmx.access.MBeanProxyFactoryBean"
p:objectName="dustin:type=status,name=remoteJMX"
p:server-ref="mBeanServerClient"
p:proxyInterface="jmx.spring.rmi.example.StatusBeanIF" />


I then need to wire this remotely accessible MBean to the bean that will be using it. The XML for that is shown next with the new line added for this wiring highlighted.


<bean id="jmxSpringClient"
class="jmx.spring.rmi.example.springClient.SpringJmxClientBean"
p:mbeanServerConnection-ref="mBeanServerClient"
p:remoteMbean-ref="remoteMBean" />


When used to its full extent, Spring allows for the configuration and injection of both the MBeanServerConnection and the proxy to the remote MBean to be implemented via XML and removed from the code. This greatly simplifies the client-side code.

The conclusion from all of this is that whether using Spring for your JMX client or not using Spring for your JMX client, the use of proxies is generally simpler and safer than using direct reflection. It is not surprising, therefore, that the JMX Best Practices recommend using proxies for the client-side access of remote JMX MBeans.

3 comments:

codecraig said...

How do you handle the case where the remote JMX bean is not yet exposed (i.e. its server hasn't started yet, etc)?

The client side spring app will error out when trying to load it.

Dustin said...

codecraig,

I typically have the server running first. If I absolutely need the client to start the server before the remote JMX MBean is available, I have usually used scripts and had the client invoke the scripts to start the server. It seems cleanest, however, to have the client try to connect only after the server is available.

codecraig said...

My situation is I have a bean that is exposed via JMX on one server. On another server I have tomcat and the Java application deployed there has a reference to the bean on the other server (using Spring of course).

The problem I run into sometimes is if for some reason the "other" server is down when I bring up the Tomcat server, Spring throws an error and my app isn't loaded. I then I have to go bring up the other server and then restart Tomcat.

Because of this I am considering using JMS to accomplish the tasks that I am doing right now with JMX, but I was hoping there was a better way provided by Spring.