Monday, February 18, 2008

Publishing User Objects in JMX Notifications

In my Publishing JMX Notifications with Spring blog entry, I demonstrated the use of the Spring framework to publish JMX notifications from Spring-exposed MBeans to clients. In that blog entry, I passed a Java String as the optional User Data object in the published Notification. In this blog entry, I'll cover how to pass a custom object rather than a standard Java String.

The code for the main executable is the same as shown in my last blog entry. Likewise, the Spring configuration XML is also the same as shown in that blog entry and so is not shown here. The one piece of code that is different to supply a custom object to a Notification is the class that is Spring-exposed as an MBean. That class's source code is shown here with the new code highlighted.

SomeJavaObject.java

package dustin.server;

import javax.management.Notification;
import org.springframework.jmx.export.notification.NotificationPublisher;
import org.springframework.jmx.export.notification.NotificationPublisherAware;

/**
* Nothing in this class makes it explicitly a JMX MBean. Instead, Spring will
* expose this as an MBean. The Spring-specific NotificationPublisherAware
* interface is implemented by this class, which means that Spring will also
* allow this bean-turned-MBean to easily publish JMX notifications.
*
* @author Dustin
*/
public class SomeJavaObject implements NotificationPublisherAware
{
private int notificationIndex = 0;
private NotificationPublisher notificationPublisher;
private String someValue = "Nada";

public SomeJavaObject()
{
// empty default constructor
}

public String getSomeValue()
{
return this.someValue;
}

public void setSomeValue(final String aSomeValue)
{
notificationPublisher.sendNotification(
buildNotification(this.someValue, aSomeValue) );
this.someValue = aSomeValue;
}

/**
* Generate a Notification that will ultimately be published to interested
* listeners.
*
* @param aOldValue Value prior to setting of new value.
* @param aNewValue Value after setting of new value.
* @return Generated JMX Notification.
*/
private Notification buildNotification(
final String aOldValue,
final String aNewValue )
{
final String notificationType = "dustin.jmx.spring.notification.example";
final String message = "Converting " + aOldValue + " to " + aNewValue;
final Notification notification =
new Notification( notificationType,
this,
notificationIndex++,
System.currentTimeMillis(),
message );
UserDataObject userData =
new UserDataObject("UserData", "Java Management Extensions");
notification.setUserData(userData);

return notification;
}

/**
* This is the only method required to fully implement the
* NotificationPublisherAware interface. This method allows Spring to
* inject a NotificationPublisher into me.
*
* @param aPublisher The NotificationPublisher that Spring injects into me.
*/
public void setNotificationPublisher(NotificationPublisher aPublisher)
{
this.notificationPublisher = aPublisher;
}
}


In my previous blog entry, I provided a simple String to the Notification.setUserData(Object) method and did not need to do anything else, even on the client side, because String is Serializable and JMX clients understand Java Strings without any extra classpath information. However, with my own custom class now, I'll need to specify more on the classpath when running the JMX client.

First, however, let's define the UserDataObject class used in the code above.

UserDataObject.java

import java.io.Serializable;

/**
* A simplistic data object meant to be used in JMX Notifications as a user
* data object.
*
* @author Dustin
*/
public class UserDataObject implements Serializable
{
private String objectName = null;
private String objectType = null;

/**
* Constructor accepting parameters to set all of my necessary parts.
*
* @param aObjectName
* @param aObjectType
*/
public UserDataObject(final String aObjectName, final String aObjectType)
{
this.objectName = aObjectName;
this.objectType = aObjectType;
}

/**
* Provide String representation of this object for use with JMX Notifications.
*
* @return String representation of me.
*/
@Override
public String toString()
{
return "Name of " + this.objectName + " and type of " + this.objectType;
}
}


Because this UserDataObject will need to be used by the JMX Client that listens for the published JMX Notifications, it is important that this class be declared as Serializable . The code to ensure that this class is Serializable was highlighted above.

If the user data object was not serializable, an error similar to that shown in the following snapshot (click on it to see larger version) would appear.



The error message is repeated in text here for search purposes:

WARNING: Failed to deserialize a notification: java.io.NotSerializableException: dustin.server.UserDataObject


Even with the custom user data object declared as Serializable, JConsole will not be able to display the remote JMX Notifications if run locally with no extra options set. When we try to use JConsole to listen to these published JMX Notifications without specifying any extra options, we'll see an error like that shown in the next screen snapshot (click on image to see larger version).



The main text of this error message is:

WARNING: Failed to deserialize a notification: java.lang.ClassNotFoundException: dustin.server.UserDataObject (no security manager: RMI class loader disabled)


The problem here is that the custom user data object we included in the Notification is not available to JConsole in its classpath when run without options. As shown in Using JConsole - Java SE Monitoring and Management Guide, we can use the -J-Djava.class.path syntax to specify to JConsole (-J) that a Java system property (-D) needs to be specified and then specify the java.class.path system property. The jconsole command to provide this custom user data object is shown next:


jconsole -J-Djava.class.path="%JAVA_HOME%\lib\jconsole.jar";"%JAVA_HOME%\lib\tools.jar";C:\userClient.jar


Because this example was run on Windows, the included class path uses double quotes around the %JAVA_HOME% environment variables and semicolons are used to separate classpath entries. Because I was overriding the JConsole classpath, I needed to provide the jconsole.jar and tools.jar JARs on the classpath in addition to my custom user data class (which I encapsulated in userClient.jar).

Here are the contents of userClient.jar:


0 Sun Feb 17 23:00:00 MST 2008 META-INF/
71 Sun Feb 17 23:00:00 MST 2008 META-INF/MANIFEST.MF
805 Sun Feb 17 22:50:34 MST 2008 dustin/server/UserDataObject.class


With the custom user data object specified as Serializable and provided on the classpath to the JMX client (JConsole), we can now see the successful results. The first of the next two screen snapshots shows a snippet of running the main application and entering three strings to be published in the JMX Notification. The second of the next two screen snapshots shows the JMX Notifications received by the JConsole listener when it is executed as shown above and in the first screen snapshot.





In this blog entry, I have attempted to demonstrate how custom user data objects can be associated with JMX Notifications even when they are published to remote clients. The two main things to remember are to make the user data object serializable and to make it available on the client's classpath.


UPDATE:
Two useful forum threads on custom objects on the JMX client side and ways to work with them are jconsole context classloader issue and jconsole no security manager exception (RMI classloader disabled).

No comments: