Saturday, August 23, 2008

Remote JMX: Connectors and Adapters

One of the things that was most difficult for me to learn when first learning about Remote JMX was the difference between a JMX Connector and a JMX Adapter (also spelled Adaptor in many cases). Part of this confusion was a result from trying to understand the difference based on books written before the Remote JMX specification was written or finalized. Another part of the confusion is in the actual names. In this blog entry, I'll point to a few references and resources that I think best explain the JMX Connector and the JMX Adapter and then I'll go through some source code examples because I believe that using these in practice makes them easier to understand than simply reading the words.

The JMX 1.4 specification includes both "regular" JMX and remote JMX in a single, consolidated specification. Part III of this consolidated JMX specification is focused on Remote JMX and is called "JMX Remote API Specification." The first chapter in this Part III, Chapter 13, covers JMX Connectors in general and is followed by Chapter 14 focusing on the RMI-based JMX Connector and Chapter 15 focusing on the Generic Connector.

I think two sentences in Chapter 13 of the JMX 1.4 specification are particularly important to understanding JMX connectors:

1. "The client end of a connector exports essentially the same interface as the
MBean server."

2. "A connector consists of a connector client and a connector server."

Section 5.3 of the JMX 1.4 specification is called "Protocol Adaptors and Connectors" and covers basics of both adapters and connectors. Related to JMX connectors, this section makes the important observation that "A connector is specific to a given protocol, but the management application can use any connector indifferently because they have the same remote interface."

This section also clearly outlines differentiating characteristics of JMX Protocol Adaptors:

1. "Protocol adaptors provide a management view of the JMX agent through a given protocol. They adapt the operations of MBeans and the MBean server into a representation in the given protocol, and possibly into a different information
model."

2. "Management applications that connect to a protocol adaptor are usually specific to the given protocol."

With the key characteristics of JMX connectors and protocol adaptors highlighted above, there are some quickly identifiable differences between the two. These differences may be most succinctly summarized in Daniel Fuchs's blog entry What is JMX?, where he states that JMX Protocol Connectors represent the MBeans to the remote client the same way they would be represented to a local client and that a JMX Protocol Adaptor adapts the server-side model to what the client expects.

JMX Protocol Connectors and JMX Protocol Adaptors both typically work with a single protocol. The difference between the two is that Adaptors massage the management interface for the client's benefit while the Connectors provide a protocol-independent API that is essentially the same as the local API.

The JMX Reference Implementation (the implementation of JMX included in Sun's Java SE 6 distribution) provides the one Connector required of the specification: the Remote Method Invocation (RMI) Connector. The JMX 1.4 specification only requires a JMX implementation to provide an RMI-based Connector, but the specification outlines an optional JMXMP-based Connector. For this example, I use the JMXMP (JMX Message Protocol) Connector provided by OpenDMK. The third JMX Connector used in this example is the JSR-262 JMX Web Services Connector.

I now delve into some code samples to illustrate JMX Connectors versus JMX Adaptors. For simplicity, I have a single class that runs three types of JMX Connector Servers and also runs an Adapter Agent. This class is called JmxServerMain and its main() method is listed here first.

JmxServerMain.java main() Method

/**
* Main method to set up various server-side remote JMX constructs.
*
* @param arguments Command-line arguments.
*/
public static void main(final String[] arguments)
{
final List connectorServers
= new ArrayList();
registerExampleMBean("dustinApp:type=exampleMBean");
useJmxServiceUrlBasedConnector(
"service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi",
"connector:type=standard_rmi",
connectorServers );
useJmxServiceUrlBasedConnector(
"service:jmx:jmxmp://localhost:1098",
"connector:type=optional_jmxmp",
connectorServers );
useJmxServiceUrlBasedConnector(
"service:jmx:ws://localhost:1097/jmxws",
"connector:type=jsr262_jmxws",
connectorServers);
final HtmlAdaptorServer htmlAdaptor =
useHtmlAdaptor(1096, "adaptor:type=opendmk_html");
waitForInput();
stopConnectorServers(connectorServers);
stopAdaptors(htmlAdaptor);


I will show the implementations of the methods called above later in this entry, but even this high-level look reveals clear and easily identifiable differences between Connectors and Adapters. Note that all three JMX Connectors can be treated the same and can take advantage of the same method. In other words, all three Connectors can be set up using the useJmxServiceUrlBasedConnector method. The Adapter, on the other hand, can not take advantage of the same method as the Connectors and must be set up with its own useHtmlAdaptor method. Note that this method name, useHtmlAdaptor, is specific to the adaptor involved.

The definition of the useJmxServiceUrlBasedConnector method is shown next.

JmxServerMain and its useJmxServiceUrlBasedConnector Method

/**
* Use the platform MBean server in conjunction with the JMX connector
* specified in the provided JMXServiceURL.
*
* @param serviceUrl JMXServiceURL to be used in connector.
* @param connectorMBeanName MBean registration name for the connector.
* @param connectorServers Collection to which my instantiated JMX Connector
* Server should be added.
*/
public static boolean useJmxServiceUrlBasedConnector(
final String serviceUrl,
final String connectorMBeanName,
final List connectorServers)
{
boolean success = true;
final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
printOutputHeader(
"Setting up connector for JMXServiceURL " + serviceUrl,
System.out);
try
{
final JMXServiceURL jmxServiceUrl = new JMXServiceURL(serviceUrl);
final JMXConnectorServer connectorServer =
JMXConnectorServerFactory.newJMXConnectorServer(
jmxServiceUrl,
null,
mbs);
connectorServer.start();
registerProvidedMBean(connectorServer, connectorMBeanName);
connectorServers.add(connectorServer);
}
catch (MalformedURLException badJmxServiceUrl)
{
System.err.print(
"ERROR trying to create JMX server connector with service URL "
+ serviceUrl + ":\n" + badJmxServiceUrl.getMessage() );
success = false;
}
catch (IOException ioEx)
{
System.err.println(
"ERROR trying to access server connector.\n"
+ ioEx.getMessage() );
success = false;
}

if ( success )
{
System.out.println(
connectorMBeanName
+ " registered in MBean server as connector with JMXServiceURL of "
+ serviceUrl + ".");
}
else
{
System.out.println("\n\nERROR encountered.");
}
System.out.println("\n\n");

return success;
}


The useJmxServiceUrlBasedConnector method shown above is generic and supports all types of specification-compliant JMX connectors provided to it. In fact, the only thing that differentiates one type of connector from another is the protocol embedded within the string that forms the JMXServiceURL. For connector management purposes, it is a recommended practice to register connectors themselves as MBeans and that is also done in this code.

We have now seen that the same generic code can be used to set up all the Connectors for Remote JMX access. In the example above, the RMI, JMXMP, and JMX Web Services Connector are all used this way. The next code listing shows the code for the useHtmlAdaptor method, which is also part of the JmxServerMain class.

JmxServerMain.java useHtmlAdaptor Method

/**
* Provide server-side functionality for HTML Adaptor to be used by client
* web page.
*
* @param htmlAdaptorPort Port on which web browser will see this.
* @param adaptorMBeanName Name of MBean by which adaptor will be registered.
* @return Handle to the HTML Adaptor that can be stopped when finished.
*/
public static HtmlAdaptorServer useHtmlAdaptor(
final int htmlAdaptorPort,
final String adaptorMBeanName)
{
final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
final HtmlAdaptorServer htmlAdaptor = new HtmlAdaptorServer(htmlAdaptorPort);

printOutputHeader(
"Setting up HtmlAdaptor for port " + htmlAdaptorPort,
System.out);

registerProvidedMBean(htmlAdaptor, adaptorMBeanName);
htmlAdaptor.start();
System.out.println("HTML Adaptor started.\n\n");
return htmlAdaptor;
}


The HTML Adaptor is not acquired with a standardized JMXConnectorServerFactory.newJMXConnectorServer call like the connectors were able to be acquired. Instead, the HtmlAdaptorServer is explicitly instantiated. The HTMLAdaptorServer used here is provided by OpenDMK.

All three JMX Connectors (RMI, JMXMP, and WS-JMX) can be connected to by JConsole or by any other standard JMX client. I'll first show the code for accessing these three connectors from a simple client.

ClientMain.java

package dustin.jmx.client;

import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Arrays;
import java.util.List;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

/**
* This simple example demonstrates a flexible/generic remote JMX client.
*
* @author Dustin
*/
public class ClientMain
{
private enum ConnectionProtocol
{
RMI ("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi"),
JMXMP ("service:jmx:jmxmp://localhost:1098"),
JMXWS ("service:jmx:ws://localhost:1097/jmxws");

String jmxServiceUrl;

ConnectionProtocol(final String jmxServiceUrl)
{
this.jmxServiceUrl = jmxServiceUrl;
}

public String getJmxServiceUrl() { return this.jmxServiceUrl; }
};

/**
* 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() );
final List domains = Arrays.asList(mbsc.getDomains());
System.out.println("DOMAINS:");
for ( final String domain : domains )
{
System.out.println ("\t- " + domain);
}
}
catch (IOException ioEx)
{
System.err.println( "ERROR encountered trying to get MBeanCount and "
+ "Default Domain for provided MBeanServer:\n"
+ ioEx.getMessage() );
}
}

/**
* Run client that can talk to a JMX Connector Server of one of three types:
* Remote Method Invocation (RMI), JMX Message Protocol (JMXMP), or JMX-WS
* (JMX Web Services Connector/JSR-262).
*
* @param aArguments Command-line arguments; one expected to indicate
* Remote JMX protocol (RMI, JMXMP, or JMXWS).
*/
public static void main(String[] aArguments)
{
ConnectionProtocol connectionProtocol = null;
if ( aArguments.length > 0 )
{
final String protocolStr = aArguments[0].toUpperCase();
System.out.println("Protocol String: " + protocolStr);
connectionProtocol = ConnectionProtocol.valueOf(protocolStr);
}
if ( connectionProtocol == null )
{
connectionProtocol = ConnectionProtocol.RMI;
}
final String jmxServiceUrl = connectionProtocol.getJmxServiceUrl();
System.out.println("JMXServiceURL: " + jmxServiceUrl);

try
{
final JMXServiceURL jmxUrl = new JMXServiceURL(jmxServiceUrl);
final JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl);
final MBeanServerConnection mbsc = jmxConnector.getMBeanServerConnection();

System.out.println("Using " + connectionProtocol.toString() + "!!!");
demonstrateCommonMBeanServerInfo(mbsc);
}
catch (MalformedURLException badUrl)
{
System.err.println( "ERROR: Problem with JMXServiceURL based on "
+ jmxServiceUrl + ": " + badUrl.getMessage() );
}
catch (IOException ioEx)
{
System.err.println( "ERROR: IOException trying to connect to JMX "
+ "Connector Server: " + ioEx.getMessage() );
}
}
}


The simple client whose code is shown above allows the user to specify which protocol connector to use (RMI, JMXMP, JMXWS) as a command-line argument and RMI is used if none is specified. What this simple client demonstrates is that the JMX Connector Servers are accessed in the identical manner regardless of the underlying protocol. Only the JMXServiceURL needs to be different.

The three JMX Connector Servers can also be accessed from JConsole. As with the simple client, the only thing that needs to be different is the JMXServiceURL provided to JConsole in the remote field.

The HTML Adaptor is not accessed via the simple client shown above or via JConsole. Rather, as an HTML adapter, it has adapted the model for HTML presentation and one accesses its exposed management interfaces via web browser with URL of http://localhost:1096 (1096 in this example because we established this as the port for the HtmlAdaptorServer).

I am not going to show the simple client output, the JConsole output, or the HTML web page output here, because they are nothing different from what one would see using these tools for other JMX uses. For convenience, I am including the entire JmxServerMain class next. I included significant portions of it above, but this listing includes the entire class with all of its convenience methods.

Entire JmxServerMain.java Class

package dustin.jmx.server;

import dustin.jmx.ApplicationState;

import com.sun.jdmk.comm.HtmlAdaptorServer;

import java.io.Console;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.management.ManagementFactory;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;

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;

/**
* Main executable to showcase various commonly used JMX connectors and adapters
* (or adaptors).
*
* @author Dustin
*/
public class JmxServerMain
{
/**
* Artificial pause until ENTER button is pressed.
*/
public static void waitForInput()
{
final Console console = System.console();
if ( console == null )
{
System.err.println(
"ERROR. Please run this application on a machine with a console.");
return;
}
console.printf("Press to exit.");
final String unusedInput = console.readLine();
}

/**
* Print out header to provided output stream.
*
* @param headerText Text to be displayed in output header/separator.
* @param os OutputStream to which header should be written (such as
* System.out).
*/
public static void printOutputHeader(
final String headerText,
final OutputStream os )
{
final String newLine = System.getProperty("line.separator");
final String headerLine = "-- " + headerText + newLine;
final String separator =
"------------------------------------------------------------------"
+ newLine;

try
{
os.write(separator.getBytes());
os.write(headerLine.getBytes());
os.write(separator.getBytes());
}
catch (IOException ioEx)
{
System.err.println(
"ERROR trying to write header '" + headerText + "' out to the "
+ " provided OutputStream:\n" + ioEx.getMessage() );
}
}

/**
* Stop all opened and started JMXConnectorServer instances.
*
* @param connectors JMXConnectorServer instances.
* @return Number of connector servers stopped.
*/
public static int stopConnectorServers(final List connectors)
{
System.out.println("Stopping Connector Servers ...");
int numberOfConnectorsClosed = 0;
for ( final JMXConnectorServer connector : connectors )
{
numberOfConnectorsClosed++;
try
{
connector.stop();
}
catch (IOException ioEx)
{
System.err.println(
"ERROR trying to close JMXConnectorServer:\n" + ioEx.getMessage());
}
}
System.out.println(
"Stopped " + numberOfConnectorsClosed + " connector servers.");
return numberOfConnectorsClosed;
}

/**
* Stop the adaptors.
*
* @param htmlAdaptor Handle to HTML Adaptor to be stopped.
* @return Number of adaptors stopped.
*/
public static int stopAdaptors(HtmlAdaptorServer htmlAdaptor)
{
System.out.println("Stopping Adaptors ...");
htmlAdaptor.stop();
System.out.println("Stopped HtmlAdaptorServer.");
return 1;
}

/**
* Register the provided object as an MBean with the provided ObjectName.
*
* @param objectToBeRegisteredAsMBean Object to be registered with MBean
* Server.
* @param nameForMBean Name to be used for ObjectName of registered MBean.
*/
public static void registerProvidedMBean(
final Object objectToBeRegisteredAsMBean,
final String nameForMBean)
{
final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
try
{
mbs.registerMBean(
objectToBeRegisteredAsMBean,
new ObjectName(nameForMBean) );
}
catch (MalformedObjectNameException badMBeanNameEx)
{
System.err.println(
"ERROR trying to create ObjectName with " + nameForMBean + ":\n"
+ badMBeanNameEx.getMessage() );
}
catch (MBeanRegistrationException badMBeanRegistrationEx)
{
System.err.println(
"ERROR trying to register MBean " + nameForMBean + ":\n"
+ badMBeanRegistrationEx.getMessage() );
}
catch (NotCompliantMBeanException nonCompliantEx)
{
System.err.println(
"ERROR: " + nameForMBean + " is not a compliant MBean.:\n"
+ nonCompliantEx.getMessage() );
}
catch (InstanceAlreadyExistsException redundantMBeanEx)
{
System.err.println(
"ERROR: MBean instance " + nameForMBean + " already exists:\n"
+ redundantMBeanEx.getMessage() );
}
}

/**
* Register an example MBean with the Platform MBean server to be looked up
* by clients connected via a connector or adapter. Note that this is not
* absolutely necessary in Java SE 6, but it is interesting.
*
* @param nameForMBean Name for MBean to be registered with MBeanServer.
*/
public static void registerExampleMBean(
final String nameForMBean )
{
registerProvidedMBean(new ApplicationState(), nameForMBean);
}

/**
* Use the platform MBean server in conjunction with the JMX connector
* specified in the provided JMXServiceURL.
*
* @param serviceUrl JMXServiceURL to be used in connector.
* @param connectorMBeanName MBean registration name for the connector.
* @param connectorServers Collection to which my instantiated JMX Connector
* Server should be added.
*/
public static boolean useJmxServiceUrlBasedConnector(
final String serviceUrl,
final String connectorMBeanName,
final List connectorServers)
{
boolean success = true;
final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
printOutputHeader(
"Setting up connector for JMXServiceURL " + serviceUrl,
System.out);
try
{
final JMXServiceURL jmxServiceUrl = new JMXServiceURL(serviceUrl);
final JMXConnectorServer connectorServer =
JMXConnectorServerFactory.newJMXConnectorServer(
jmxServiceUrl,
null,
mbs);
connectorServer.start();
registerProvidedMBean(connectorServer, connectorMBeanName);
connectorServers.add(connectorServer);
}
catch (MalformedURLException badJmxServiceUrl)
{
System.err.print(
"ERROR trying to create JMX server connector with service URL "
+ serviceUrl + ":\n" + badJmxServiceUrl.getMessage() );
success = false;
}
catch (IOException ioEx)
{
System.err.println(
"ERROR trying to access server connector.\n"
+ ioEx.getMessage() );
success = false;
}

if ( success )
{
System.out.println(
connectorMBeanName
+ " registered in MBean server as connector with JMXServiceURL of "
+ serviceUrl + ".");
}
else
{
System.out.println("\n\nERROR encountered.");
}
System.out.println("\n\n");

return success;
}

/**
* Provide server-side functionality for HTML Adaptor to be used by client
* web page.
*
* @param htmlAdaptorPort Port on which web browser will see this.
* @param adaptorMBeanName Name of MBean by which adaptor will be registered.
* @return Handle to the HTML Adaptor that can be stopped when finished.
*/
public static HtmlAdaptorServer useHtmlAdaptor(
final int htmlAdaptorPort,
final String adaptorMBeanName)
{
final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
final HtmlAdaptorServer htmlAdaptor = new HtmlAdaptorServer(htmlAdaptorPort);

printOutputHeader(
"Setting up HtmlAdaptor for port " + htmlAdaptorPort,
System.out);

registerProvidedMBean(htmlAdaptor, adaptorMBeanName);
htmlAdaptor.start();
System.out.println("HTML Adaptor started.\n\n");
return htmlAdaptor;
}

/**
* Main method to set up various server-side remote JMX constructs.
*
* @param arguments Command-line arguments.
*/
public static void main(final String[] arguments)
{
final List connectorServers
= new ArrayList();
registerExampleMBean("dustinApp:type=exampleMBean");
useJmxServiceUrlBasedConnector(
"service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi",
"connector:type=standard_rmi",
connectorServers );
useJmxServiceUrlBasedConnector(
"service:jmx:jmxmp://localhost:1098",
"connector:type=optional_jmxmp",
connectorServers );
useJmxServiceUrlBasedConnector(
"service:jmx:ws://localhost:1097/jmxws",
"connector:type=jsr262_jmxws",
connectorServers);
final HtmlAdaptorServer htmlAdaptor =
useHtmlAdaptor(1096, "adaptor:type=opendmk_html");
waitForInput();
stopConnectorServers(connectorServers);
stopAdaptors(htmlAdaptor);
}
}


Because the RMI connector is included with the Reference Implementation of JMX that is included with Java SE 6, I did not need to explicitly include anything on the classpath to use it. However, to use the JSR-262 WS-JMX and to use OpenDMK for both the HTMLAdaptor and for the JMXMP Connector, I did need to add relevant JARs to the classpaths.

I ran JmxServerMain as follows (thankfully, Java SE 6 supports wildcards for including JAR files in the classpath or else the WS-JMX entries would be numerous):


java -cp C:\OpenDMK-bin\lib\jdmkrt.jar;C:\OpenDMK-bin\lib\jmxremote_optional.jar;dist\RemoteJMX.jar;C:\jmx-ws-262\jsr262-ri\lib\* dustin.jmx.server.JmxServerMain


To run JConsole to "see" the OpenDMK and WS-JMX stuff, I used this command:


jconsole -J-Djava.class.path=C:\jmx-ws-262\jsr262-ri\lib\*;"C:\Program Files\Java\jdk1.6.0_07"\lib\jconsole.jar;"C:\Program Files\Java\jdk1.6.0_07"\lib\tools.jar;C:\OpenDMK-bin\lib\jmxremote_optional.jar


Finally, to run the simple programmatic client, I used this command:


java -cp dist\RemoteJMX.jar;C:\jmx-ws-262\jsr262-ri\lib\*;C:\OpenDMK-bin\lib\* dustin.jxm.client.ClientMain


If you don't include the appropriate JAR files for WS-JMX or for OpenDMK's JMXMP Connector, you will see errors like those shown in the following two images ("Unsupported protocol: jmxmp" and "Unsupported protocol: ws"):

Failure to Include OpenDMK JARs on ClassPath




Failure to Include JSR-262 JARs on ClassPath



Failure to include the OpenDMK JARs for HTMLAdaptor will be revealed at compile time instead of runtime because of the direct instantiation of the HtmlAdaptorServer class described above. Of course, the runtime classpath will need them as well, but failure to do that is marked by the well-known NoClassDefFoundError.

In this blog entry, I've attempted to demonstrate key differences between JMX Protocol Adaptors and JMX Protocol Connectors. In general, I prefer Connectors because of the standardized approach that can be used to work with them both on the Connector Client and Connector Server side. I also generally like having the same interface both locally and remotely. However, there are times when adapters have their advantages. For example, it is nice not to have to include OpenDMK or WS-JMX libraries on the client machine when using a web browser with the HTML Adaptor. Perhaps at least partially for this reason (simplicity on the client side), early JMX books often favored the HTML Adaptor as the view into managed applications. The fact that JConsole was not yet available probably had something to do with the prevalent use of HTML Adaptor as well. With JConsole, VisualVM, and the ability to easily write JMX Connector Clients, I find myself generally favoring Connectors over Adaptors unless there is something specific about the client format (such as HTML or SNMP) that is required.

Other resources describing JMX Connectors and Adaptors include the JMX Tutorial (focus on connectors), the JMX Accelerated How-to, and the Remote Management Applications section of the Sun Java Dynamic Management Kit Tutorial.

1 comment:

Unknown said...

Hi,
I would like to understand if we can customize the HTML adapter...
Please advice.

Thnks
vilas