Thursday, August 28, 2008

From JConsole to VisualVM

With the release of VisualVM as a standard part of Sun's JDK distribution since JDK 6 Update 7, it is likely that Java developers will begin using VisualVM in situations in which they previously used the separate tools such as jinfo, jmap, jstack, jstat, and JConsole. Fortunately, two of the main features JMX developers were likely to use in JConsole can be used with VisualVM as well.

One feature I use very often in JConsole is the "MBeans" tab for displaying manageable and monitorable attributes, operations, and notifications related to my custom MBeans. While this tab does not exist in VisualVM when it is first run from the command-line in Java SE 6, it is trivial to add the MBeans plugin to get behavior very similar to that provided in JConsole.

To run VisualVM provided with the JDK distribution, use the command jvisualvm from the command prompt. This is demonstrated in the following screen snapshot:



When first started up, VisualVM does not have an MBeans tab like that used in JConsole. However, one can easily add the MBeans plugin by selecting Tools->Plugins from VisualVM. The following screen snapshots show how to do this.

This first screen snapshot shows selection of Tools->Plugins.



The next screen snapshot shows the MBean tab selection from among the available VisualVM plugins.




The next screen snapshot demonstrates what the MBeans tab looks like in VisualVM.



The MBeans tab in VisualVM is very familiar to anyone who has used the MBeans tab in JConsole.


Another useful JConsole feature is the ability to add custom tabs to JConsole. While the VisualVM JConsole Plug-in Wrapper Tab document recommends using VisualVM's own customizability features when developing new support for VisualVM, it can be handy to use existing JConsole plug-ins with VisualVM. The just-referenced document VisualVM JConsole Plug-in Wrapper Tab does a nice job of covering how to use a custom JConsole plug-in tab in VisualVM. That example uses the JDK-provided JTop custom plugin for the example.

The three screen snapshots that follow show how easy it is to install the JConsole plugins tab wrapper in VisualVM and to select a JConsole plugin for use in VisualVM. The final image shows how one JConsole plugin, the JTop plugin provided with the SDK, appears in VisualVM.

This first image shows the details related to the JConsole plugin tab wrapper.



The next image shows selection of a particular JConsole tab plugin once the wrapper plug-in for VisualVM has been installed.



This final image demonstrates what the JTop JConsole tab looks like in VisualVM via VisualVM's JConsole plug-in tab wrapper.




For individuals and organizations with large investments in JConsole custom tabs, the VisualVM wrapper plug-in allows those plug-ins to be leveraged in VirtualVM without massive re-writes. For those who appreciate JConsole as an easy-to-use client of custom JMX applications, VisualVM can provide similar capabilities via its MBeans plugin. Besides providing many of the advantages of JConsole via plug-ins and out-of-the-box support, VisualVM provides graphical representation of the monitoring data provided by many tools other than JConsole. With the JConsole-friendly plug-ins mentioned in this blog entry in use, VisualVM is able to combine the strengths of JConsole with the value found in other Sun JDK-provided management and monitoring tools.

Monday, August 25, 2008

Flex Charts with Google Charts and Eastwood Charts

Flex provides a rich, sophisticated set of dynamic charts, but a license fee is required (via purchase of FlexBuilder Professional) to use these Flex charts for most realistic production situations. There are several other charting options including Open Flash Chart (runs as its own SWF and reads JSON data files to generate charts), amCharts (not open source and uses ActionScript 2 so cannot be used directly in Flex), Fusion Charts Free (sends SWF and data files from server to browser), Google Charts, and JFreeChart/Eastwood Charts. In this blog entry, I'll briefly look at using Google Charts and Eastwood Charts 1.1.0 with Flex because I believe these are the easiest to use in a Flex application.

Because the Eastwood Chart Servlet implements the most of the APIs of Google Charts, my example will be able, in most cases, to call either charting provider based on user selection. There are some extensions in Eastwood beyond the Google Chart API and the Google Chart API has some features still not implemented in Eastwood, but most of the common functionality is commonly implemented in both charting providers.

The concept behind Google Chart API (and hence applies to Eastwood Chart Servlet as well) involves providing data for chart generation to the chart provider via HTTP URL parameters and getting an image (PNG) as a response.

While URLs constructed to request chart generation from Google Charts and Eastwood Charts are almost always the same (because Eastwood is implemented to the Google Charts API), there are differences between the two implementations of these APIs. Using Google Charts is really easy because no work needs to be done on the server-side. All the developer needs to do is construct the appropriate URL conforming to the Google Charts API and invoke that to get the returned image. With Eastwood Charts, the developer needs to install the Eastwood-provided WAR file on his or her favorite Java EE container. It is important to note that this is all that must be done. The WAR files does not need to be tweaked or manually unzipped, but simply needs to be deployed by dropping it in the appropriate container directory or else through the server's administration tool.

The Eastwood Developer Manual that is included with the Eastwood download contains a list of several bullets outlining some advantages of the Eastwood Charts implementation of the Google Charts API. Most of these advantages stem ability to run the charting server on one's own network. Advantages of running the charting provider on one's own machine is include the ability to keep potentially sensitive data within one's own environment and the ability to run the chart generation in environments in which Internet connectivity is not available.

The following MXML code is a simple but complete example that accesses both Google Charts and Eastwood Charts via the same URL. I intentionally included 3D bar chart examples to demonstrate an extension provided by Eastwood Charts, but not by the Google Charts. While I don't show it here, Google Charts similarly provide some features not currently supported in Eastwood Charts (such as Maps).

Here is the source code for the Flex application taking advantage of both Google Charts and Eastwood Charts:

MXML Code for Application Using Eastwood Charts and Google Charts

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
width="900" height="900">
<mx:Script>
<![CDATA[
import mx.events.FlexEvent;

/**
* Generate chart using Eastwood Chart Servlet or Google Charts using
* submitted values.
*/
private function generateChart(event:FlexEvent):void
{
chartDisplayPanel.visible = true;
chartDisplayPanel.title = chartType.selectedValue.toString();
const url:String = buildUrl(chartType.selection.id);
chartUrlText.text = url;
generatedChart.source = url;
}

/**
* Determine maximum value entered for balances.
*
* @return Maximum balance from provided input balances.
*/
private function findMaximumDataValue():Number
{
const maximumBalance:Number =
Math.max(
Number(savingsInput.text),
Number(checkingInput.text),
Number(moneyMarketInput.text),
Number(taxableStocksInput.text),
Number(taxableBondsInput.text),
Number(iraInput.text),
Number(pensionInput.text) );
return maximumBalance;
}

/**
* Return the appropriate chart provider URL domain.
*
* @return URL domain for chart provider.
*/
private function findChartProvider():String
{
var chartProviderUrl:String;
if ( chartProvider.selection.id == "google" )
{
chartProviderUrl = "http://chart.apis.google.com";
}
else
{
chartProviderUrl = "http://localhost:8080/eastwood-1.1.0";
}
return chartProviderUrl;
}

/**
* Construct labels portion of chart generation URL.
*
* @return Labels portion of chart generation URL.
*/
private function constructLabels():String
{
const labelsStr:String =
"Savings|Checking|Money Market|Taxable Stocks|"
+ "Taxable Bonds|IRA|Pension";
return labelsStr;
}

/**
* Construct URL for accessing Eastwood-generated or Google-generated chart.
*/
private function buildUrl(chartType:String):String
{
const maximumBalance:Number = findMaximumDataValue();
const chartProviderDomainUrl:String = findChartProvider();
const urlString:String =
chartProviderDomainUrl + "/chart?cht="
+ chartType // type of chart
+ "&chs=400x400" // chart size: width x height
+ "&chd=t:" // build up the data over next several lines
+ savingsInput.text + ","
+ checkingInput.text + ","
+ moneyMarketInput.text + ","
+ taxableStocksInput.text + ","
+ taxableBondsInput.text + ","
+ iraInput.text + ","
+ pensionInput.text
+ "&chds=0," + Math.round(maximumBalance) // data scaling
+ "&chl=" + constructLabels();
return urlString;
}
]]>
</mx:Script>

<mx:HBox id="mainHorizontalBox" width="95%">
<mx:Panel id="inputPanel" title="Flex and Eastwood Charts">
<mx:Form>
<mx:FormHeading label="Investments Distribution" />
<mx:FormItem label="Savings ($)">
<mx:TextInput id="savingsInput" />
</mx:FormItem>
<mx:FormItem label="Checking ($)">
<mx:TextInput id="checkingInput" />
</mx:FormItem>
<mx:FormItem label="Money Market ($)">
<mx:TextInput id="moneyMarketInput" />
</mx:FormItem>
<mx:FormItem label="Taxable Stocks ($)">
<mx:TextInput id="taxableStocksInput" />
</mx:FormItem>
<mx:FormItem label="Taxable Bonds ($)">
<mx:TextInput id="taxableBondsInput" />
</mx:FormItem>
<mx:FormItem label="IRA ($)">
<mx:TextInput id="iraInput" />
</mx:FormItem>
<mx:FormItem label="Pension ($)">
<mx:TextInput id="pensionInput" />
</mx:FormItem>
<mx:FormItem label="Chart Type" id="chartTypeFormItem">
<mx:RadioButtonGroup id="chartType" />
<mx:RadioButton groupName="chartType" id="p"
label="Pie"
value="Investments Pie Chart" />
<mx:RadioButton groupName="chartType" id="p3"
label="3D Pie"
value="Investments 3D Pie Chart" />
<mx:RadioButton groupName="chartType" id="bvs"
label="Vertical Bar"
value="Investments Vertical Bar" />
<mx:RadioButton groupName="chartType" id="bvs3"
label="Vertical 3D Bar"
value="Investments Vertical 3D Bar"
selected="true" />
<mx:RadioButton groupName="chartType" id="bhs"
label="Horizontal Bar"
value="Investments Horizontal Bar" />
<mx:RadioButton groupName="chartType" id="bhs3"
label="Horizontal 3D Bar"
value="Investments Horizontal 3D Bar" />
</mx:FormItem>
<mx:FormItem label="Chart Provider"
id="chartProviderFormItem">
<mx:RadioButtonGroup id="chartProvider" />
<mx:RadioButton groupName="chartProvider" id="google"
label="Google Charts API"
value="Google Charts API" />
<mx:RadioButton groupName="chartProvider" id="eastwood"
label="Eastwood Chart Servlet"
value="Eastwood Chart Servlet"
selected="true" />
</mx:FormItem>
<mx:Button id="submitButton" label="Generate Chart"
buttonDown="generateChart(event);" />
</mx:Form>
</mx:Panel>
<mx:Panel id="chartDisplayPanel" height="{inputPanel.height}"
visible="false">
<mx:Image id="generatedChart" />
</mx:Panel>
</mx:HBox>
<mx:Panel id="summaryPanel" width="{mainHorizontalBox.width}"
title="URL Being Used for Chart Generation">
<mx:TextArea id="chartUrlText" width="{summaryPanel.width}"
height="75" />
</mx:Panel>

</mx:Application>


The above example provides a radio button group for the user to select which chart provider (Google Charts or Eastwood Charts) should be used. Another radio button group allows the user to select which chart type should be generated. The text fields allow for dynamic data values to be supplied that are used in the chart generation. The sample code takes these input values, the user's selection for chart type, and the user's selection for chart provider and builds a URL from these data points that meets the Google Charts API.

The following screen snapshots show charts generated from both Google Charts and Eastwood Charts (click on the images to see larger versions).

Eastwood Charts-Generated 3D Pie Chart



Google Charts-Generated 3D Pie Chart



Eastwood Charts-Generated Vertical Bar Chart



Google Charts-Generated Vertical Bar Chart



As you can see in the screen snapshots, there are some slight differences in the implementations of Eastwood Charts and Google Charts. However, they are remarkably similar and it is pretty convenient to be able to have the same code call either implementation to generate these charts. There are many more types of charts available than those shown in this example. Examples of these types of charts can be found in the Google Charts API page and in the Eastwood Chart Servlet samples page.

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.

Friday, August 15, 2008

Extraordinary Standard MBeans

Standard MBeans are commonly used for several reasons. They are easy to use, the interfaces can be applied to client-side proxies, and the interfaces allow for easy Javadoc-based documentation of the management interface on the Standard MBean. MXBeans share these advantages and add a few of their own such as looser required naming and package conventions and greater interoperability support via Open MBean Types.

However, as discussed in my blog entry The JMX Model MBean, there are some disadvantages of Standard MBeans (and MXBeans) that Model MBeans can address. The most significant reason for using the Model MBean (to wrap existing non-JMX resources such as employed in the Spring Framework) is not possible with Standard or MXBeans due to their static nature. However, the advantage of being able to provide information on operations, parameters, constructors, attributes, and notifications is not a sole advantage of Model MBeans or even of the class of dynamic MBeans. I will demonstrate in this blog entry one approach to specifying operation metadata for Standard MBeans and MXBeans that can be displayed in JConsole and other clients that know how to handle MBeanInfo.

The key to providing additional metadata above and beyond what is normally associated with a Standard MBean is use of the class javax.management.StandardMBean. The description portion of this class's Javadoc documentation shows two ways to use this class. They are using StandardMBean's public constructors or by having the MBean implementation class extend the StandardMBean class (but still implement the same interface). Using the StandardMBean class allows the naming conventions normally associated with Standard MBeans to be relaxed because arbitrary implementation classes and interfaces can be associated with this class. However, in this blog entry, I will keep the normal implementation/interface naming conventions and use the class StandardMBean to provide metadata to the Standard MBean and MXBean.

My first code listing shows a class called DustinMBean that extends StandardMBean so that I can override the StandardMBean.cacheMBeanInfo method.

DustinMBean.java

package dustin.jmx;

import javax.management.MBeanInfo;
import javax.management.StandardMBean;

/**
* Child of StandardMBean overridden for customization purposes.
*
* @author Dustin
*/
public class DustinMBean extends StandardMBean
{
/**
* Single constructor accepting MBean implementation class, MBean interface,
* and whether or not it is an MXBean (Standard MBean if not MXBean). Note
* that my parent, StandardMBean, has more than one constructor, but I'm only
* using one of them.
*
* @param implementation MBean implementation class.
* @param mbeanInterface MBean implementation class's interface.
* @param isMXBean true if MXBean; false if Standard MBean.
*/
public DustinMBean(
T implementation,
Class mbeanInterface,
boolean isMXBean )
{
super(implementation, mbeanInterface, isMXBean);
}

/**
* Specify an MBeanInfo to be used with a StandardMBean-turned-DynamicMBean.
*
* @param mbeanInfo MBeanInfo describing this standard MBean turned into a
* Dynamic MBean.
*/
public void setMBeanInfo(final MBeanInfo mbeanInfo)
{
cacheMBeanInfo(mbeanInfo);
}
}


The class shown above makes it easy to set a Standard MBean's MBeanInfo instance.

I will be using a Standard MBean and an MXBean in this blog entry. The next two code listings show the interface and implementation class for the Standard MBean.

SimpleCalculatorMBean.java (Standard MBean Interface)

package dustin.jmx;

/**
* Interface for a Standard MBean example using the Simple Calculator.
*
* @author Dustin
*/
public interface SimpleCalculatorMBean
{
/**
* Calculate the sum of the augend and the addend.
*
* @param augend First integer to be added.
* @param addend Second integer to be added.
* @return Sum of augend and addend.
*/
public int add(final int augend, final int addend);

/**
* Calculate the difference between the minuend and subtrahend.
*
* @param minuend Minuend in subtraction operation.
* @param subtrahend Subtrahend in subtraction operation.
* @return Difference of minuend and subtrahend.
*/
public int subtract(final int minuend, final int subtrahend);

/**
* Calculate the product of the two provided factors.
*
* @param factor1 First integer factor.
* @param factor2 Second integer factor.
* @return Product of provided factors.
*/
public int multiply(final int factor1, final int factor2);

/**
* Calculate the quotient of the dividend divided by the divisor.
*
* @param dividend Integer dividend.
* @param divisor Integer divisor.
* @return Quotient of dividend divided by divisor.
*/
public double divide(final int dividend, final int divisor);

/**
* Provides the type of operation last executed against this calculator class.
*
* @return Type of operation last executed against this calculator.
*/
public OperationType whatWasTheLastOperation();
}


SimpleCalculator.java (Standard MBean Implementation)

package dustin.jmx;

/**
* Simple calculator class intended to demonstrate how a class with no knowledge
* of JMX or management can be used as a Standard MBean.
*
* @author Dustin
*/
public class SimpleCalculator implements SimpleCalculatorMBean
{
private OperationType lastOperation = OperationType.NO_OPERATIONS_INVOKED;

/**
* Calculate the sum of the augend and the addend.
*
* @param augend First integer to be added.
* @param addend Second integer to be added.
* @return Sum of augend and addend.
*/
public int add(final int augend, final int addend)
{
lastOperation = OperationType.INTEGER_ADDITION;
return augend + addend;
}

/**
* Calculate the difference between the minuend and subtrahend.
*
* @param minuend Minuend in subtraction operation.
* @param subtrahend Subtrahend in subtraction operation.
* @return Difference of minuend and subtrahend.
*/
public int subtract(final int minuend, final int subtrahend)
{
lastOperation = OperationType.INTEGER_SUBTRACTION;
return minuend - subtrahend;
}

/**
* Calculate the product of the two provided factors.
*
* @param factor1 First integer factor.
* @param factor2 Second integer factor.
* @return Product of provided factors.
*/
public int multiply(final int factor1, final int factor2)
{
lastOperation = OperationType.INTEGER_MULTIPLICATION;
return factor1 * factor2;
}

/**
* Calculate the quotient of the dividend divided by the divisor.
*
* @param dividend Integer dividend.
* @param divisor Integer divisor.
* @return Quotient of dividend divided by divisor.
*/
public double divide(final int dividend, final int divisor)
{
lastOperation = OperationType.INTEGER_DIVISION;
return dividend / divisor;
}

/**
* Provides the type of operation last executed against this calculator class.
*
* @return Type of operation last executed against this calculator.
*/
public OperationType whatWasTheLastOperation()
{
return this.lastOperation;
}
}


The two code listings immediately above are the interface and implementation for the Standard MBean. The next two listings are for the interface and implementation for a very similar MXBean. The MXBean is not really any more complicated than the Standard MBean, but I needed to name it something different.

LessSimpleCalculatorMXBean.java (MXBean Interface)

package dustin.jmx;

/**
* Interface for a standard MXBean example using the Simple Calculator.
*
* @author Dustin
*/
public interface LessSimpleCalculatorMXBean
{
/**
* Calculate the sum of the augend and the addend.
*
* @param augend First integer to be added.
* @param addend Second integer to be added.
* @return Sum of augend and addend.
*/
public int add(final int augend, final int addend);

/**
* Calculate the difference between the minuend and subtrahend.
*
* @param minuend Minuend in subtraction operation.
* @param subtrahend Subtrahend in subtraction operation.
* @return Difference of minuend and subtrahend.
*/
public int subtract(final int minuend, final int subtrahend);

/**
* Calculate the product of the two provided factors.
*
* @param factor1 First integer factor.
* @param factor2 Second integer factor.
* @return Product of provided factors.
*/
public int multiply(final int factor1, final int factor2);

/**
* Calculate the quotient of the dividend divided by the divisor.
*
* @param dividend Integer dividend.
* @param divisor Integer divisor.
* @return Quotient of dividend divided by divisor.
*/
public double divide(final int dividend, final int divisor);

/**
* Provides the type of operation last executed against this calculator class.
*
* @return Type of operation last executed against this calculator.
*/
public OperationType whatWasTheLastOperation();
}


LessSimpleCalculator.java (MXBean Implementation)

package dustin.jmx;

/**
* Simple calculator class intended to demonstrate how a class with no knowledge
* of JMX or management can be used as an MXBean.
*
* @author Dustin
*/
public class LessSimpleCalculator implements LessSimpleCalculatorMXBean
{
private OperationType lastOperation = OperationType.NO_OPERATIONS_INVOKED;

/**
* Calculate the sum of the augend and the addend.
*
* @param augend First integer to be added.
* @param addend Second integer to be added.
* @return Sum of augend and addend.
*/
public int add(final int augend, final int addend)
{
lastOperation = OperationType.INTEGER_ADDITION;
return augend + addend;
}

/**
* Calculate the difference between the minuend and subtrahend.
*
* @param minuend Minuend in subtraction operation.
* @param subtrahend Subtrahend in subtraction operation.
* @return Difference of minuend and subtrahend.
*/
public int subtract(final int minuend, final int subtrahend)
{
lastOperation = OperationType.INTEGER_SUBTRACTION;
return minuend - subtrahend;
}

/**
* Calculate the product of the two provided factors.
*
* @param factor1 First integer factor.
* @param factor2 Second integer factor.
* @return Product of provided factors.
*/
public int multiply(final int factor1, final int factor2)
{
lastOperation = OperationType.INTEGER_MULTIPLICATION;
return factor1 * factor2;
}

/**
* Calculate the quotient of the dividend divided by the divisor.
*
* @param dividend Integer dividend.
* @param divisor Integer divisor.
* @return Quotient of dividend divided by divisor.
*/
public double divide(final int dividend, final int divisor)
{
lastOperation = OperationType.INTEGER_DIVISION;
return dividend / divisor;
}

/**
* Provides the type of operation last executed against this calculator class.
*
* @return Type of operation last executed against this calculator.
*/
public OperationType whatWasTheLastOperation()
{
return this.lastOperation;
}
}


Both the Standard MBean and MXBean code listings above use the OperationType enum. The main reason for adding an enum into the mix is to demonstrate differences between Standard MBean and MXMBean.

OperationType.java

package dustin.jmx;

/**
* Simple enum representing a type of calculator operation.
*
* @author Dustin
*/
public enum OperationType
{
INTEGER_ADDITION,
INTEGER_SUBTRACTION,
INTEGER_MULTIPLICATION,
INTEGER_DIVISION,
NO_OPERATIONS_INVOKED
}


With the Standard MBean, MXBean, enum, and DustinMBean classes all shown above, it is time to look at the class that calls DustinMBean to turn the Standard and MXBeans into dynamic MBeans with metadata descriptions.

StandardMBeanDemonstrator.java

package dustin.jmx;

import java.lang.management.ManagementFactory;
import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import javax.management.ReflectionException;

/**
* This class is intended to demonstrate a more powerful Standard MBean
* available from overriding the javax.management.StandardMBean class.
*
* @author Dustin
*/
public class StandardMBeanDemonstrator
{
/**
* Pause for the specified number of milliseconds.
*
* @param millisecondsToPause Milliseconds to pause execution.
*/
public static void pause(final int millisecondsToPause)
{
try
{
Thread.sleep(millisecondsToPause);
}
catch (InterruptedException threadAwakened)
{
System.err.println("Don't wake me up!\n" + threadAwakened.getMessage());
}
}

/**
* Construct the meta information for the SimpleCalculator
* Standard-turned-Dynamic MBeans operations and the operations' parameters.
*
* Note that this method was adapted from a very similar method for
* constructor Model MBean operation info data that was discussed in the
* method buildModelMBeanOperationInfo() in the class
* ModelMBeanDemonstrator in the blog entry "The JMX Model MBean"
* (http://marxsoftware.blogspot.com/2008/07/jmx-model-mbean.html).
*
* @return Metadata about MBean's operations.
*/
private static MBeanOperationInfo[] buildMBeanOperationInfo()
{
//
// Build the PARAMETERS and OPERATIONS meta information for "add".
//

final MBeanParameterInfo augendParameter =
new MBeanParameterInfo(
"augend",
Integer.TYPE.toString(),
"The first parameter in the addition (augend)." );
final MBeanParameterInfo addendParameter =
new MBeanParameterInfo(
"addend",
Integer.TYPE.toString(),
"The second parameter in the addition (addend)." );

final MBeanOperationInfo addOperationInfo =
new MBeanOperationInfo(
"add",
"Integer Addition",
new MBeanParameterInfo[] {augendParameter, addendParameter},
Integer.TYPE.toString(),
MBeanOperationInfo.INFO );


//
// Build the PARAMETERS and OPERATIONS meta information for "subtract".
//

final MBeanParameterInfo minuendParameter =
new MBeanParameterInfo(
"minuend",
Integer.TYPE.toString(),
"The first parameter in the substraction (minuend)." );

final MBeanParameterInfo subtrahendParameter =
new MBeanParameterInfo(
"subtrahend",
Integer.TYPE.toString(),
"The second parameter in the subtraction (subtrahend)." );

final MBeanOperationInfo subtractOperationInfo =
new MBeanOperationInfo(
"subtract",
"Integer Subtraction",
new MBeanParameterInfo[] {minuendParameter, subtrahendParameter},
Integer.TYPE.toString(),
MBeanOperationInfo.INFO );

//
// Build the PARAMETERS and OPERATIONS meta information for "multiply".
//

final MBeanParameterInfo factorOneParameter =
new MBeanParameterInfo(
"factor1",
Integer.TYPE.toString(),
"The first factor in the multiplication." );

final MBeanParameterInfo factorTwoParameter =
new MBeanParameterInfo(
"factor2",
Integer.TYPE.toString(),
"The second factor in the multiplication." );

final MBeanOperationInfo multiplyOperationInfo =
new MBeanOperationInfo(
"multiply",
"Integer Multiplication",
new MBeanParameterInfo[] {factorOneParameter, factorTwoParameter},
Integer.TYPE.toString(),
MBeanOperationInfo.INFO );

//
// Build the PARAMETERS and OPERATIONS meta information for "divide".
//

final MBeanParameterInfo dividendParameter =
new MBeanParameterInfo(
"dividend",
Integer.TYPE.toString(),
"The dividend in the division." );

final MBeanParameterInfo divisorParameter =
new MBeanParameterInfo(
"divisor",
Integer.TYPE.toString(),
"The divisor in the division." );

final MBeanOperationInfo divideOperationInfo =
new MBeanOperationInfo(
"divide",
"Integer Division",
new MBeanParameterInfo[] {dividendParameter, divisorParameter},
Double.TYPE.toString(),
MBeanOperationInfo.INFO );

//
// Build the PARAMETERS and OPERATIONS meta information for
// "whatWasTheLastOperation" method.
//

final MBeanOperationInfo lastOperationOperationInfo =
new MBeanOperationInfo(
"whatWasTheLastOperation",
"Last Calculator Operation Performed",
null,
"OperationType",
MBeanOperationInfo.INFO );

return new MBeanOperationInfo[]
{ addOperationInfo, subtractOperationInfo,
multiplyOperationInfo, divideOperationInfo,
lastOperationOperationInfo };

}

/**
* Create a SimpleCalculator-specific MBeanInfo.
*
* @param mbeanClassName Name of MBean class being used.
* @param description Description for MBean display.
* @return Generated MBeanInfo.
*/
private static MBeanInfo createCalculatorMBeanInfo(
final String mbeanClassName,
final String description )
{

final MBeanInfo mbeanInfo =
new MBeanInfo(
mbeanClassName, // underlying class name
description, // description of MBean meant for humans
null, // attributes
null, // constructors
buildMBeanOperationInfo(), // operations
null); // notifications
return mbeanInfo;
}

/**
* Register the provided MBean (implementation and interface provided) with
* the provided ObjectName on the provided MBeanServer as a Standard MBean
* if isMXBean is false or as an MXBean if isMXBean is true.
*
* @param mBeanClass MBean implementation class.
* @param interfaceClass MBean interface.
* @param objectNameStr ObjectName under which MBean will be registered.
* @param isMXBean true if this is MXBean; false if Standard MBean.
* @param mbs MBean Server on which to register MBean.
*/
private static void registerStandardMBean(
final Class mBeanClass,
final Class interfaceClass,
final String objectNameStr,
final boolean isMXBean,
final MBeanServer mbs)
{
try
{
final Object objectForMBean = mBeanClass.newInstance();
final ObjectName objectName = new ObjectName(objectNameStr);
final DustinMBean standardMBean =
new DustinMBean(
mBeanClass.cast(objectForMBean),
interfaceClass,
isMXBean);
standardMBean.setMBeanInfo(
createCalculatorMBeanInfo(
mBeanClass.getName(),
"Simple Standard MBean-turned Dynamic Example.") );
mbs.registerMBean(standardMBean, objectName);
System.out.println(
"MBean with ObjectName " + objectNameStr + " based on class "
+ mBeanClass.getCanonicalName() + " and interface "
+ interfaceClass.getCanonicalName() + " has been registered.");
}
catch (InstantiationException ex)
{
System.err.println(
"Unable to instantiate provided class or interface.\n"
+ ex.getMessage() );
}
catch (IllegalAccessException ex)
{
System.err.println(
"Cannot access the provided class for a new instance.\n"
+ ex.getMessage() );
}
catch (MalformedObjectNameException ex)
{
System.err.println(
"Cannot create an ObjectName " + objectNameStr + ":\n"
+ ex.getMessage() );
}
catch (InstanceAlreadyExistsException ex)
{
System.err.println(
"An MBean instance already exists with name " + objectNameStr
+ ":\n" + ex.getMessage() );
}
catch (MBeanRegistrationException ex)
{
System.err.println(
"Failed to register MBean " + objectNameStr + " based on class "
+ mBeanClass.getCanonicalName() + ":\n" + ex.getMessage() );
}
catch (NotCompliantMBeanException ex)
{
System.err.println(
"The class " + mBeanClass.getCanonicalName() + " is not a "
+ "compliant MBean: " + ex.getMessage() );
}
}

/**
* Create and register an MBean given the provided MBean's implementation
* class name, the desired Object Name for the MBean, and the MBeanServer
* on which to register the MBean.
*
* @param mBeanClass MBean implementation class.
* @param objectNameStr ObjectName to be used for the MBean instance.
* @param mbs MBean Server that will be hosting the MBean.
*/
private static void createAndRegisterStandardMBean(
final Class mBeanClass,
final String objectNameStr,
final MBeanServer mbs)
{

String standardMBeanClassName = "";
try
{
final ObjectName standardMBeanName = new ObjectName(objectNameStr);
standardMBeanClassName = mBeanClass.getCanonicalName();
mbs.createMBean(standardMBeanClassName, standardMBeanName);
System.err.println(
"Standard MBean " + standardMBeanName + " created and registered "
+ "using the class " + standardMBeanClassName );
}
catch (ReflectionException ex)
{
System.err.println(
"Problem trying to get name of class for MBean:\n"
+ ex.getMessage() );
}
catch (InstanceAlreadyExistsException ex)
{
System.err.println(
"MBean already exists and is registered with name "
+ objectNameStr + ":\n" + ex.getMessage() );
}
catch (MBeanRegistrationException ex)
{
System.err.println(
"Error trying to register MBean " + objectNameStr + ":\n"
+ ex.getMessage() );
}
catch (NotCompliantMBeanException ex)
{
System.err.println(
"Class " + standardMBeanClassName + " is not a compliant MBean:\n"
+ ex.getMessage() );
}
catch (MalformedObjectNameException ex)
{
System.err.println( "Bad objectname " + objectNameStr + ":\n"
+ ex.getMessage() );
}
catch (MBeanException ex)
{
System.err.println(
"MBeanException encountered trying to register class "
+ standardMBeanClassName + " as MBean with ObjectName of "
+ objectNameStr + ":\n" + ex.getMessage() );
}
}

/**
* Main executable for running Standard and MXBean examples.
*
* @param arguments The command line arguments.
*/
public static void main(String[] arguments)
{
final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();

// Create and register a standard MBean the standard way.
createAndRegisterStandardMBean(
SimpleCalculator.class, // SimpleCalculator is Standard MBean
"standardmbean:type=standard", // ObjectName string
mbs); // MBean server to register on

// Create and register an MXBean the standard way.
createAndRegisterStandardMBean(
LessSimpleCalculator.class,
"standardmbean:type=mxbean",
mbs);

// Register a standard MBean using the StandardMBean-extended class.
registerStandardMBean(
SimpleCalculator.class,
SimpleCalculatorMBean.class,
"dynamicmbean:type=standard",
false,
mbs);

// Register an MXBean using the StandardMBean-extended class.
registerStandardMBean(
LessSimpleCalculator.class,
LessSimpleCalculatorMXBean.class,
"dynamicmbean:type=mxbean",
true,
mbs);

pause(100000);
}
}


In the class above, the method that sets up the metadata [buildMBeanOperationInfo()] is extremely similar to the same method used in an earlier blog entry to set up metadata on operations for the Model MBean class.

It is inside the try block in the registerStandardMBean method that most of the action occurs in terms of using the StandardMBean-derived DustinMBean. This method is a little more complicated than it would need to be if was less generic and reflection was not used. The example usage code shown in the Javadoc for StandardMBean shows how easy it can be.

For instance, instead of using reflection like this:


final Object objectForMBean = mBeanClass.newInstance();
final ObjectName objectName = new ObjectName(objectNameStr);
final DustinMBean standardMBean =
new DustinMBean(
mBeanClass.cast(objectForMBean),
interfaceClass,
isMXBean);
standardMBean.setMBeanInfo(
createCalculatorMBeanInfo(
mBeanClass.getName(),
"Simple Standard MBean-turned Dynamic Example.") );
mbs.registerMBean(standardMBean, objectName);


it could be done less generically but much more simply like this


final ObjectName objectName = new ObjectName(objectNameStr);
final DustinMBean standardMBean =
new DustinMBean(
new SimpleCalculator(),
SimpleCalculatorMBean.class,
isMXBean);
standardMBean.setMBeanInfo(
createCalculatorMBeanInfo(
"dustin.jmx.SimpleCalculator",
"Simple Standard MBean-turned Dynamic Example.") );
mbs.registerMBean(standardMBean, objectName);


In the main() of the StandardMBeanDemonstrator class, the code demonstrates that the Standard MBean and the MXBean are each used twice, once in the standard way and once via the StandardMBean class. The standard approach depends entirely on naming conventions between implementation and interface while the StandardMBean approach allows the implementation and interface to be explicitly associated in code. The latter approach also allows extra metadata to be specified.

I won't show it here, but the whatWasTheLastOperation() operation only works in JConsole for the two uses of MXBean and not for the two uses of Standard MBean for reasons displayed in a previous blog entry.

I'll end this entry with two screen snapshots. One shows the MXBean that was created and registered with the MBeanServer in the standard way. It works perfectly well, but lacks descriptive information in JConsole. The second shows the same MXBean registered with the StandardMBean approach. It works as well, but also shows dynamic MBean level of description.

MXBean Registered on MBean Server in Standard Way




MXBean Registered on MBean Server via StandardMBean Class




In conclusion, use of the Java SE-provided StandardMBean class allows JMX developers to treat Standard and MXBeans more like dynamic MBeans. Because all MBean types support Descriptors as of Java SE 6, the differences between the types of MBeans continue to blur.

Monday, August 11, 2008

Playing with JMX 2.0 Annotations

Eamonn McManus's blog entry Playing with JMX 2.0 API provided me with the encouragement and advice I needed to try out JMX 2.0's likely annotation implementation. Toward the end of his blog entry, Eamonn provides detailed instructions on how Java SE 7 can be downloaded and the jmx.jar built and run. I followed those instructions and everything worked fine for me on my Vista-based laptop.

As explained in Playing with JMX 2.0 API (see that entry for additional/different details), the following steps can be followed to start experimenting with the current in-work version of JMX 2.0:

1. Download the JAR with the JDK source (jdk-7-ea-src-b32-jrl-04_aug_2008.jar in my case) from the Java SE 7 download site (http://download.java.net/jdk7/).

2. Execute the downloaded JAR with a command like this: java -jar jdk-7-ea-src-b32-jrl-04_aug_2008.jar and follow the wizard (including accepting the JRL license terms). Make note of where you have the source code placed. For my examples, I had the contents placed in C:\java7.

3. Change directory to the "jdk" directory under the expanded Java SE 7 source code and run Ant like this: ant -f make/netbeans/jmx/build.xml

(The -f option in Ant specifies the location of an alternative build file instead of Ant looking for a build.xml file in the same directory. Alternatively, one could change directories all the way to the make/netbeans/jmx directory and simply run "ant" there.)

4. If all has gone well, there will be a generated jmx.jar file in the dist/lib directory. The next screen snapshot shows how this appears on my machine.



I copied this generated jmx.jar file into my C:\jmx2 directory for easy reference.


5. Write the JMX 2.0 annotation-based Standard MBean and MBean Server management code.

With jmx.jar built, it can be placed on the classpath to build and run examples based on JMX 2.0. In my case, I added it to NetBeans 6.1. In a later step, I'll show how I ran the sample using it in the runtime classpath. For now, though, I will show the two code listings for my example. The first is a class called SimpleCalculator adapted from my blog entries on Model MBeans (direct, with Spring, with Apache Commons Modeler, and with EasyMBean).

SimpleCalculator.java


package dustin.jmx2;

import javax.management.Description;
import javax.management.DescriptorFields;
import javax.management.MBean;
import javax.management.ManagedOperation;

import static javax.management.Impact.INFO;

/**
* Simple calculator class intended to demonstrate use of the JMX 2.0
* annotations as currently implemented for proposed JSR-255 and Java SE 7.
*
* @author Dustin
*/
@MBean
@Description(
"Example of using JMX 2.0/JSR-255 JMX annotations to expose "
+ "an otherwise normal class as a Standard MBean.")
public class SimpleCalculator
{
/**
* Calculate the sum of the augend and the addend.
*
* @param augend First integer to be added.
* @param addend Second integer to be added.
* @return Sum of augend and addend.
*/
@ManagedOperation(impact=INFO)
@Description(
"Integer Addition: First parameter is the augend and second parameter "
+ "is the addend.")
@DescriptorFields({"p1=augend","p2=addend"})
public int add(final int augend, final int addend)
{
return augend + addend;
}

/**
* Calculate the difference between the minuend and subtrahend.
*
* @param minuend Minuend in subtraction operation.
* @param subtrahend Subtrahend in subtraction operation.
* @return Difference of minuend and subtrahend.
*/
@ManagedOperation(impact=INFO)
@Description(
"Integer Subtraction: First parameter is minuend and second parameter "
+ "is subtrahend.")
@DescriptorFields({"p1=minuend","p2=subtrahend"})
public int subtract(final int minuend, final int subtrahend)
{
return minuend - subtrahend;
}

/**
* Calculate the product of the two provided factors.
*
* @param factor1 First integer factor.
* @param factor2 Second integer factor.
* @return Product of provided factors.
*/
@ManagedOperation(impact=INFO)
@Description(
"Integer Multiplication: First parameter is one factor and second "
+ "parameter is other factor.")
@DescriptorFields({"p1=factor1","p2=factor2"})
public int multiply(final int factor1, final int factor2)
{
return factor1 * factor2;
}

/**
* Calculate the quotient of the dividend divided by the divisor.
*
* @param dividend Integer dividend.
* @param divisor Integer divisor.
* @return Quotient of dividend divided by divisor.
*/
@ManagedOperation(impact=INFO)
@Description(
"Integer Division: First parameter is the dividend and second "
+ "parameter is divisor.")
@DescriptorFields({"p1=dividend","p2=divisor"})
public double divide(final int dividend, final int divisor)
{
return dividend / divisor;
}
}


This version of SimpleCalculator differs from the various versions used in my previous Model MBeans examples because this version uses JMX 2.0 annotations. This example is different as well in the sense that these JMX 2.0 annotations allow this class to be used as a Standard MBean rather than as a Model MBean (the Spring and EasyMBean annotations exposed their decorated SimpleCalculator class as ModelMBeans).

The four import statements in SimpleCalculator correspond to the four JMX 2.0 annotations used in the creation of the Standard MBean. The @MBean annotation on the class itself states that it will be a Standard MBean (and @MXBean does the same for MXBeans). The @ManagedOperation annotation specifies which operations are exposed by the class-turned-Standard MBean. Because I cannot specify information on the parameters to the operations like I can do with Model MBeans, I have included parameter information via the @DescriptorFields annotation. Finally, I used @Description annotations to describe the class and each of the operations. Information on each of these annotations is available in the Javadoc documentation for the javax.management package.

The next class, Main, registers the annotated SpringCalculator Standard MBean with the platform MBean server.

Main.java


package dustin.jmx2;

import java.lang.management.ManagementFactory;

import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;

/**
* Giving JMX 2.0 (JSR-255) a spin.
*
* @author Dustin
*/
public class Main
{
/**
* Pause for the specified number of milliseconds.
*
* @param millisecondsToPause Milliseconds to pause execution.
*/
public static void pause(final int millisecondsToPause)
{
try
{
Thread.sleep(millisecondsToPause);
}
catch (InterruptedException threadAwakened)
{
System.err.println("Don't wake me up!\n" + threadAwakened.getMessage());
}
}

/**
* Main executable to test out JMX 2.0 annotation-powered MBean.
*
* @param arguments the command line arguments
*/
public static void main(final String[] arguments)
{
final SimpleCalculator calculator = new SimpleCalculator();
final String mbeanName = "jmx2:type=annotatedMBean";
try
{
final ObjectName objectName = new ObjectName(mbeanName);
final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
mbs.registerMBean(calculator, objectName);
}
catch (MBeanRegistrationException cannotRegisterMBean)
{
System.err.println(
"ERROR: Could not register MBean with name " + mbeanName + ":\n"
+ cannotRegisterMBean.getMessage() );
}
catch (MalformedObjectNameException badObjectName)
{
System.err.println(
"ERROR: Had trouble with ObjectName based on string "
+ mbeanName + ":\n" + badObjectName.getMessage() );
}
catch (InstanceAlreadyExistsException redundantMBean)
{
System.err.println(
"ERROR: MBean with name " + mbeanName + " already exists:\n"
+ redundantMBean.getMessage() );
}
catch (NotCompliantMBeanException notCompliantMBean)
{
System.err.println(
"ERROR: Object attempted to be used as an MBean ("
+ calculator.getClass().getCanonicalName()
+ ") is not a compliant MBean:\n" + notCompliantMBean.getMessage());
}
System.out.println("JMX 2/JSR-255 Annotation-Based MBean Registered...");
pause(100000);
}
}



6. Run the Code and Monitor with JConsole

With the above two classes implemented, they can be built and the resulting .class or .jar files can be executed. In my case, I built the classes with NetBeans 6.1 and it generated a JAR file called JMX2.jar containing the compiled Main.class and SimpleCalculator.class files.

To run the example and use the Java SE 7 jmx.jar in my Java SE 6 environment, I took advantage of Eamonn's recommendation regarding the use of the non-standard option bootclasspath to prepend (the p in the command) the JMX 2.0/Java SE 7 jmx.jar to the front of my boot class path. Running this application with this JMX 2.0/Java SE 7 jmx.jar in the boot class path looks like this:


java -Xbootclasspath/p:C:\jmx2\jmx.jar -cp JMX2.jar dustin.jmx2.Main


I can then run JConsole and view the exposed Standard MBean. The next two screen snapshots show this. The first screen snapshot shows some general MBean information and the second screen snapshot shows a particular operation being invoked via JConsole. Of special interest are the descriptor and description sections. I took advantage of both of these (the descriptors and the descriptions) to specify information about the operations' parameters that would be labeled simply as "p1" and "p2" otherwise.





I have attempted to demonstrate in this blog entry how easy it is to turn an otherwise normal Java class into a JMX Standard MBean using the proposed JMX 2.0/JSR-255 annotations. MXBeans are similarly annotated (the primary difference being the use of @MXBean rather than @MBean). The obvious advantage of these JMX 2.0 annotations is convenience and ease of use. However, Eammon points out in the "Pros and Cons of @MBeans" section of Defining MBeans with Annotations (and the Javadoc documentation points out in Defining Standard MBeans with annotations section of
javax.management documentation) that there are disadvantages to using these annotations to define standard and MXBeans. The primary disadvantages result from the mixing of potentially JMX-exposed methods in a class with non-JMX-exposed methods in the same class (and hence confusing Javadoc documentation) and from the lack of an interface to use with JMX client proxies.