In the lesson on "Agent Applications", we saw how to access and manage a Java Dynamic Management agent through the HTML protocol adaptor. Protocol adaptors provide a view of an agent through some communication protocol. In this lesson, we present protocol connectors and proxy MBeans for managing agents programmatically.
The Java Dynamic Management Kit goes beyond the scope of the JMX specification to define the APIs needed to develop distributed management applications in the Java programming language. These remote applications establish connections with agents through protocol connectors over RMI, HTTP or HTTPS. The connector client object exposes a remote version of the MBean server interface. The connector server object in the agent transmits management requests to the MBean server and forwards any replies.
Connectors allow you to develop a management application which is both protocol independent and location independent. Once the connection is established, the communication layer is transparent, and the manager can issue requests, as if it were directly invoking the MBean server. Using proxy objects which represent MBeans, the design of the management application is even simpler and development time is reduced.
This homogeneity of the API makes it possible to develop portable management applications which can run either in an agent or in a remote management application. This simplifies the development and testing of applications, and it also allows functionality to evolve along with the management solution. As your agent and manager platforms evolve, management policies can be implemented at higher levels of management, and intelligent monitoring and processing logic can be moved down into agents.
Connectors and proxies provide an abstraction of the communication layer between agent and manger, but they also provide mechanisms for simplifying the management task. Notification forwarding with configurable caching and pull policies lets you dynamically optimize bandwidth usage. The connector heartbeat monitors a connection, applies a retry policy, and automatically frees the resources when there is a communication failure. Finally, the connector server in an agent can act as a watchdog and refuse unauthorized requests based on the type of operation.
This lesson contains the following topics:
"Protocol Connectors" establish a connection between a management application written in the Java programming language and a Java Dynamic Management agent. Agents are identified by an address and port number, all other details of the communication are hidden. Once the connection is established, the remote management application may access the MBeans in the agent. All connectors also implement the heartbeat mechanism to monitor the connection.
"MBean Proxies" represent MBeans so that they are easier to access. A proxy object exposes the interface of its MBean for direct invocation. It then encodes the management request which it forwards to the MBean server and returns any response to its caller. Specific proxies can be generated for standard MBeans, but dynamic MBeans can only be accessed through generic proxies, similar to their DynamicMBean interface.
"Notification Forwarding" extends the concept of notification listeners to the manager side. This topic covers how manager-side listeners can register with agent-side broadcasters. The example then shows how the connector client and server interact to provide both pull and push modes for forwarding notifications from the agent to the manager.
"Access Control and Security" presents the security features that can be enabled for a given connection. The HTTP-based connectors include a password mechanism that will refuse unauthorized connections. Context checking and the general filtering mechanism can implement a complex algorithm for allowing or disallowing management requests to an agent. Finally, the HTTPS connector encrypts and protects data as it transits over the network between connector client and server components.
Protocol connectors provide a point-to-point connection between a Java Dynamic Management agent and a management application. Each connector relies on a specific communication protocol, but the API that is available to the management application is identical for all connectors and is entirely protocol-independent.
A connector consists of a connector server component registered in the agent and a connector client object instance in the management application. The connector client exposes a remote version of the MBean server interface. Each connector client represents one agent to which the manager wishes to connect. The connector server replies to requests from any number of connections and fulfills them through its MBean server. Once the connection is established, the remoteness of the agent is transparent to the management application, except for any communication delays.
Connectors rely on the Java serialization package to transmit data as Java objects between client and server components. Therefore, all objects needed in the exchange of management requests and responses must be instances of a serializable class. However, the data encoding and sequencing are proprietary, and the raw data of the message contents in the underlying protocol are not exposed by the connectors.
All connectors provided with the Java Dynamic Management Kit implement a heartbeat mechanism to automatically detect when a connection is lost. This allows the manager to be notified when the communication is interrupted and when it is restored. If the connection is not reestablished, the connector automatically frees the resources that were associated with the lost connection.
The code samples in this topic are taken from the files in the SimpleClients and HeartBeat example directories located in the main examplesDir (see "Directories and Classpath" in the preface).
Contents:
"Connector Servers" presents the agent-side component of a protocol connector.
"Connector Clients" presents the manager-side component and how it establishes a connection.
"The Heartbeat Mechanism" explains how a connector monitors a connection and demonstrates how to run the heartbeat example.
The connector server on the agent side listens for management requests issued through a corresponding connector client. It transmits these requests to its MBean server and forwards any response back to the management application. The connector server also forwards notifications, when the management application has registered to receive them through its connector client (see "Notification Forwarding" for more details).
There is a connector server for each of the protocols supported in the Java Dynamic Management Kit: RMI, HTTP and HTTPS. They all inherit from the CommunicatorServer class which is the superclass for all protocol adaptors and connector servers. This class defines the methods needed to control the port and communication settings that are common to all. Each connector server class then provides specific controls, such as the service name for RMI and authentication information for HTTP. This example covers the RMI connector server, and the HTTP authentication mechanism is covered in "Password-Based Authentication".
A connector server listens for incoming requests from its corresponding connector client, decodes that request and encodes the reply. Several connector clients may establish connections with the same connector server, and the connector server can handle multiple requests simultaneously. There only needs to be one connector server MBean per protocol to which the agent needs to respond. However, several connector servers for the same protocol can coexist in an agent for processing requests on different ports.
The RMI connector server is an MBean, so we instantiate its class and register it in the MBean server. This operation could also be performed remotely, for example if a management application wishes to access an agent through an alternate protocol.
// Instantiate an RMI connector server with default port // CommunicatorServer rmiConnector = new RmiConnectorServer(); try { // We let the RMI connector server provides its default name ObjectName rmiConnectorName = null; ObjectInstance rmiConnectorInstance = myMBeanServer.registerMBean( rmiConnector, rmiConnectorName ); } catch(Exception e) { e.printStackTrace(); } |
Other constructors for the RmiConnectorServer class have parameters for specifing the port and the RMI service name that the connector server will use. The default constructor assigns port 1099 and "name=RmiConnectorServer", as given by the static variables RMI_CONNECTOR_PORT and RMI_CONNECTOR_SERVER, respectively, of the ServiceName class. Both attributes can also be accessed through the getter and setter methods of the RmiConnectorServer class.
Each connector uses different parameters that are specific to its protocol. For example, the HTTP connector does not need a service name. The default values for all parameters are given by the static variables of the ServiceName class.
Registering a connector server as an MBean implies that its MBean server will handle the remote requests that it receives. However, you may specify a different object for fulfilling management requests through the setMBeanServer method that the RmiConnectorServer class inherits from the CommunicatorServer class. For security reasons, this method is not exposed in the RMI connector server MBean, so it must be called from the agent application.
Registering the connector server as an MBean is optional: for example, you may not want it exposed for management. In this case, you must use the setMBeanServer method to specify an object which implements the MBeanServer interface so that it can fulfill management requests.
Like all communicator servers, the RMI connector server has a connection state which is identified by the static variables of the CommunicatorServer class:
OFFLINE - Stopped and not responding.
STARTING - In a transitional state and not yet responding.
ONLINE - Able to respond to management requests.
STOPPING - In a transitional state and no longer responding.
All connector servers are OFFLINE after their instantiation, so they must be started explicitly. Then, you must wait for a connector server to come ONLINE before it can respond to connections on its designated port.
// Explicitly start the RMI connector server // rmiConnector.start(); // waiting for it to leave starting state... while (rmiConnector.getState() == CommunicatorServer.STARTING) { try { Thread.sleep( 1000 ); } catch (InterruptedException e) { continue; } } |
Instead of blocking the application thread, you may register a listener for attribute change notifications concerning the State attribute of the connector server MBean. All connector servers implement this notification which contains both old and new values of the attribute (see "Attribute Change Notifications"). Listeners in the agent can then asynchronously detect when the state changes from STARTING to ONLINE.
During the STARTING state, the RMI connector server registers its RMI service name with the RMI registry on the local host for its designated port. If no registry exists, one is instantiated for the given port. Due to a limitation of the JDK software, creating a second registry in the same Java VM will fail. As a workaround, before starting an RMI connector server on a new, distinct port number in an agent, you must run the rmiregistry command from a terminal on the same host. This limitation is specific to the RMI connector: the HTTP protocols do not require a registry.
The stop method is used to take a connector server offline. The stop method is also called by the preDeregister method that all connector servers inherit. Stopping a connector server will interrupt all requests that are pending and close all connections that are active. When a connection is closed, all of its resources are cleaned up, including all notification listeners, and the connector client may be notified by a heartbeat notification (see "The Heartbeat Mechanism"). A connection that is closed can no longer be reopened, the connector client must establish a new connection when the connector server is restarted.
The setPort method that all connector servers inherit from the CommunicatorServer class allows you to change the port on which management requests will be expected. You can only change the port when the connector server is offline, so it must be explicitly stopped and then restarted. The same rule applies to the setServiceName method which is specific to the RMI connector server. These methods are also exposed in the MBean interface, along with start and stop, allowing a remote management application to configure the connector server through a different connection.
The manager application interacts with a connector client in order to access an agent through an established connection. Through its implementation of the RemoteMBeanServer interface, a connector client provides methods for handling the connection and for accessing the agent. This interface specifies nearly all of the same methods as the MBeanServer interface, meaning that an agent is fully manageable from a remote application.
Through the connector, the management application sends management requests to the MBeans located in a remote agent. Components of the management application access remote MBeans by calling the methods of the connector client for getting and setting attributes and invoking operations on the MBeans. The connector client then returns the result, providing a complete abstraction of the communication layer.
All connector clients implement the RemoteMBeanServer interface in order to expose the methods needed to access and manage the MBeans in a remote agent. This interface allows all management operations that would be possible directly in the agent application. In fact, the methods of the connector client for accessing MBeans remotely have exactly the same name and signature as their equivalent methods in the MBeanServer interface.
The following methods are defined identically in both the MBeanServer and the RemoteMBeanServer interfaces:
Table 8-1 The Set of Shared Methods[diams ] void | addNotificationListener(ObjectName name, NotificationListener listener, NotificationFilter filter, java.lang.Object handback) |
ObjectInstance | createMBean(*) - All four overloaded forms of this method |
[diams ] java.lang.Object | getAttribute(ObjectName name, java.lang.String attribute) |
[diams ] AttributeList | getAttributes(ObjectName name, java.lang.String[] attributes) |
java.lang.String | getDefaultDomain() |
java.lang.Integer | getMBeanCount() |
[diams ] MBeanInfo | getMBeanInfo(ObjectName name) |
ObjectInstance | getObjectInstance(ObjectName name) |
[diams ] java.lang.Object | invoke(ObjectName name, java.lang.String operationName, java.lang.Object[] params, java.lang.String[] signature) |
boolean | isInstanceOf(ObjectName name, java.lang.String className) |
boolean | isRegistered(ObjectName name) |
java.util.Set | queryMBeans(ObjectName name, QueryExp query) |
java.util.Set | queryNames(ObjectName name, QueryExp query) |
[diams ] void | removeNotificationListener(ObjectName name, NotificationListener listener) |
[diams ] void | setAttribute(ObjectName name, Attribute attribute) |
[diams ] AttributeList | setAttributes(ObjectName name, AttributeList attributes) |
[diams ] void | unregisterMBean(ObjectName name) |
[diams ] These methods are defined in the ProxyHandler interface; see the next topic: "Local and Remote Proxies".
Components of a management application which rely solely on this common subset of methods may be instantiated in either the agent or the manager application. Such components are location independent and may be reused either locally or remotely as managemement solutions evolve. This symmetry also allows the design of advanced management architectures where functionality may be deployed either in the agent or in the manager, depending on runtime conditions.
The other, unshared methods of the RemoteMBeanServer interface are used to establish and monitor the connection. In the following section, we will establish a connection and access MBeans directly through the connector client. In "The Heartbeat Mechanism", we will see how to monitor a connection and detect when it is lost.
The target of a connection is identified by a protocol-specific implementation of the ConnectorAddress interface. This object contains all the information that a connector client needs to establish a connection with the target agent. All address objects specify a host name and port number. An RMI address adds the required service name, and HTTP-based addresses have an optional authenication field (see "Password-Based Authentication"). In addition, the ConnectorType string identifies the protocol without needing to introspect the address object
In our example, the target of the connection is an active RMI connector server identified by an RmiConnectorAddress object. We use the default constructor to instantiate a default address object, but otherwise, these parameters can be specified in a constructor or through setter methods. The default values of the information contained in the RmiConnectorAddress object are the following:
The ConnectorType identifies the protocol that is used; its value is "SUN RMI" for the RmiConnectorAddress class.
The default RMI port is 1099, as given by the static variable RMI_CONNECTOR_PORT in the ServiceName class.
The Host is the name of the machine where the target agent is running; by default, its value is the localhost.
The Name attribute specifies the RMI registry service name of the adaptor server. Its default value is "name=RmiConnectorServer", which is the value of the RMI_CONNECTOR_SERVER static variable in the ServiceName class.
The RmiConnectorAddress object is used as the parameter to the connect method of the RmiConnectorClient instance. This method will try to establish the connection and will throw an exception if there is a communication or addressing error. Otherwise, when the connect method returns, the connector client will be ready to perform management operations on the designated agent.
echo("\t>> Instantiate the RMI connector client..."); connectorClient = new RmiConnectorClient(); echo("\t>> Instantiate a default RmiConnectorAddress object..."); RmiConnectorAddress address = new RmiConnectorAddress(); // display the default values echo("\t\tTYPE\t= " + address.getConnectorType()); echo("\t\tPORT\t= " + address.getPort()); echo("\t\tHOST\t= " + address.getHost()); echo("\t\tSERVER\t= " + address.getName()); echo("\t<< done <<"); echo("\t>> Connect the RMI connector client to the agent..."); try { connectorClient.connect( address ); } catch(Exception e) { echo("\t!!! RMI connector client could not connect to the agent !!!"); e.printStackTrace(); System.exit(1); } |
Once the connection to an agent is established, the management application can access that agent's MBeans through the RemoteMBeanServer interface of the connector client. Invoking these methods has exactly the same effect as invoking the equivalent methods directly on the MBean server instance.
It is possible to restrict access to certain methods of the MBean server when they are called through the connector client, but this is performed by a security mechanism in the connector server: see "Context Checking" for more details.
We use the createMBean method to instantiate and register an object from its class name. This class must already be available in the agent application's classpath, or you can use the createMBean method which takes the name of a class loader (see "M-Let Loading from a Manager (Java 2)" for more details).
private void doWithoutProxyExample(String mbeanName) { try { // build the MBean ObjectName instance // ObjectName mbeanObjectName = null; String domain = connectorClient.getDefaultDomain(); mbeanObjectName = new ObjectName( domain + ":type=" + mbeanName ); // create and register an MBean in the MBeanServer of the agent // echo("\nCurrent MBean count in the agent = "+ connectorClient.getMBeanCount()); echo("\n>>> CREATE the " + mbeanName + " MBean in the MBeanServer of the agent:"); String mbeanClassName = mbeanName; ObjectInstance mbeanObjectInstance = connectorClient.createMBean( mbeanClassName, mbeanObjectName ); echo("\tMBEAN CLASS NAME = " + mbeanObjectInstance.getClassName() ); echo("\tMBEAN OBJECT NAME = " + mbeanObjectInstance.getObjectName() ); echo("\nCurrent MBean count in the agent = "+ connectorClient.getMBeanCount() ); [...] // Retrieve MBeanInfo and access the MBean (see below) // unregister the MBean from the agent // echo("\n>>> UNREGISTERING the "+ mbeanName +" MBean"); connectorClient.unregisterMBean( mbeanObjectInstance.getObjectName() ); [...] } catch (Exception e) { e.printStackTrace(); } } |
The above sample shows the use of other calls to the remote agent, such as getDefaultDomain and getMBeanCount which have the same purpose as in an agent application.
Once you can access the object names for MBeans in the agent, you can know their management interface from their MBeanInfo object. The following code is actually called in between the MBean creation and deregistration shown in the previous code sample.
ObjectName mbeanObjectName = mbeanObjectInstance.getObjectName(); echo("\n>>> Getting the management information of the MBean"); MBeanInfo info = null; try { info = connectorClient.getMBeanInfo( mbeanObjectName ); } catch (Exception e) { echo("\t!!! Could not get MBeanInfo object for "+ mbeanObjectName ); e.printStackTrace(); return; } // display content of MBeanInfo object // echo("\nCLASSNAME: \t"+ info.getClassName()); echo("\nDESCRIPTION: \t"+ info.getDescription()); echo("\nATTRIBUTES"); MBeanAttributeInfo[] attrInfo = info.getAttributes(); if ( attrInfo.length>0 ) { for( int i=0; i<attrInfo.length; i++ ) { echo(" ** NAME: \t"+ attrInfo[i].getName()); echo(" DESCR: \t"+ attrInfo[i].getDescription()); echo(" TYPE: \t"+ attrInfo[i].getType() + "\tREAD: "+ attrInfo[i].isReadable() + "\tWRITE: "+ attrInfo[i].isWritable()); } } else echo(" ** No attributes **"); [...] |
It is then straightforward to perform management operations on MBeans through the connector client. As in an agent, we call the generic getters, setters and invoke methods with the object name of the MBean, the name of the attribute or operation, and any parameters. As in the methods of the MBean server, we need to use the Attribute and AttributeList classes to pass attributes as name-value pairs.
try { // Getting attribute values String State = (String) connectorClient.getAttribute( mbeanObjectName, "State" ); Integer NbChanges = (Integer) connectorClient.getAttribute( mbeanObjectName, "NbChanges" ); echo("\tState = \"" + State + "\""); echo("\tNbChanges = " + NbChanges); // Setting the "State" attribute Attribute stateAttr = new Attribute("State", "new state from client"); connectorClient.setAttribute(mbeanObjectName, stateAttr); // Invoking the "reset" operation Object[] params = new Object[0]; String[] signature = new String[0]; connectorClient.invoke(mbeanObjectName, "reset", params, signature); } catch (Exception e) { e.printStackTrace(); return; } |
All other MBean access methods are available in the same manner, such as bulk getters and setters, and the query methods.
In the first run of the example, the management application creates, manages and deregisters a standard MBean in the remote agent. However, standard and dynamic MBeans are designed to be managed through the same methods, both in the MBeanServer and in the RemoteMBeanServer interfaces.
As shown in Example 8-4, the subroutine of the example application takes only a single class name parameter. The first time this subroutine is called, the example passes the class name of a standard MBean, and the second time, that of a dynamic MBean. For the example to run, the two MBeans must have an identical management interface. By extension of this special case, we see that the connector client can manage dynamic MBeans through the same methods as it manages standard MBeans, without making any distinction between the two.
The examplesDir/SimpleClients directory contains all of the files for three sample managers, a base agent, and some MBeans to manage. In this topic, we run the ClientWithoutProxy application which demonstrates simple operations on MBeans through an RMI connector.
Compile all files in this directory with the javac command. For example, on the Solaris platform with the Korn shell, you would type:
$ cd examplesDir/SimpleClients/ $ javac -classpath classpath *.java |
We will not need all the files for this topic, but they will be used in the next topic, "MBean Proxy Objects". In this demonstration, we only need the BaseAgent and ClientWithoutProxy applications, as well as the standard and dynamic MBeans.
Launch the agent in another terminal window on the same host with the following command:
$ java -classpath classpath BaseAgent |
The agent creates an HTML protocol adaptor and an RMI connector server to which the client application will establish a connection, and then it waits for management operations.
Wait for the agent to be completely initialized, then launch the manager with the following command:
$ java -classpath classpath ClientWithoutProxy |
The client application creates the RMI connector client and establishes the connection to base agent.
Press <Enter> in the manager window to step through the example.
The management application instantiates both types of MBeans, looks at their metadata, and performs management operations on them. The results of each step are displayed in the terminal window.
At any time, you can view the agent through its HTML adaptor and interact with the MBeans created by the management application.
For example, immediately after the manager creates an MBean, you could modify its attributes and see this change reflected when the connector client access the new values.
Press <Enter> in both windows to stop the agent and manager applications.
The heartbeat mechanism monitors the connection between a manager and an agent and automates the cleanup procedure when the connection is lost. This allows both the manager and the agent to release resources that were allocated for maintaining the connection.
The mechanism is entirely contained in the connector client and connector server components, no additional objects are involved. In addition, connector clients send notifications that the manager application can receive to be aware of changes in the status of a connection.
All connector clients of the Java Dynamic Management Kit implement the HeartBeatClientHandler interface to provide a heartbeat mechanism. This means that agent-manager connections over RMI, HTTP, and HTTPS can be monitored and controlled in the same way. A manager application could even use the same notification handler for all connector clients where the heartbeat mechanism is activated.
To monitor the connection, the connector client sends periodic heartbeats (ping requests) to the connector server which acknowledges them by sending a reply (ping responses). If either heartbeat goes missing, the components of the connector will retry until either the connection is reestablished or the number of retries has been exhausted.
In a connector client, the methods specified by the HeartBeatClientHandler interface set the heartbeat period and the number of retries that will be attempted. You should determine these parameters empirically to implement the desired connection monitoring behavior, taking into account the network conditions and topology between the hosts of your manager and agent applications.
In the following code example, the management application configures the heartbeat mechanism before the connection to an agent is established.
// CREATE a new RMI connector client // echo("\tInstantiate the RMI connector client..."); connectorClient = new RmiConnectorClient(); // SET heartbeat period to 1 sec. Default value is 10 secs // echo("\tSet heartbeat period to 1 second..."); connectorClient.setHeartBeatPeriod(1000); // SET heartbeat number of retries to 2. Default value is 6 times // echo("\tSet heartbeat number of retries to 2 times..."); connectorClient.setHeartBeatRetries(2); |
Using the same methods, the heartbeat configuration may also be modified at any time, even after the connection has been established. By default, the heartbeat mechanism is activated in a connector with a 10 second heartbeat and 6 retries, meaning that a connection which cannot be reestablished within a minute will be assumed to be lost.
Setting the number of heartbeat retries to zero will cause a lost connection to be signalled immediately after the heartbeat fails. Setting the heartbeat period to zero will deactivate the mechanism and prevent any further connection failures from being detected.
No specific configuration is necessary on the agent-side connector server: it automatically responds to the heartbeat messages. These heartbeat messages contain the current heartbeat settings from the connector client which also configure the connector server. In this way, both client and server will apply the same retry policy, and when the configuration is updated in the connector client, it is immediately reflected in the connector server. The connector server may handle multiple connections from different management application, each with its specific heartbeat configuration.
The connector server will apply its retry policy when the next expected heartbeat message is not received within the heartbeat period. From that moment, the connector server will begin a timeout period which lasts 20% longer than the number of retries times the heartbeat period. This corresponds to the time during which the connector client will attempt to resend the heartbeat, with a safety margin to allow for communication delays. If no further heartbeat is received in that timeout, the connection is determined to be lost.
The heartbeat ping messages also contain a connection identifier so that connections are not erroneously reestablished. If a connector server is stopped, thereby closing all connections, and then restarted between two heartbeats or before the client's timeout period has elapsed the server will respond to heartbeats from a previous connection. However, the connector client will detect that the identifier has changed and will immediately declare that the connection is lost, regardless of the number of remaining retries.
During the timeout period, the notification push mechanism in the connector server will be suspended to avoid losing notifications (see "Notification Forwarding"). Similarly, while the connector client is retrying the heartbeat, it must suspend the notification pull mechanism if it is in effect.
When a connection is determined to be lost, both the connector client and server free any resources that were allocated for maintaining the connection. For example, the connector server will unregister all local listeners and delete the notification cache needed to forward notifications. Both components also return to a coherent, functional state, ready to establish or accept another connection.
The state of both components after a connection is lost is identical to the state which is reached after the disconnect method of the connector client is invoked. In fact, the disconnect method is invoked internally by the connector client when a connection is determined to be lost, and the equivalent, internal method is called in the connector server when its timeout elapses without recovering a heartbeat.
The connector client also sends notifications which signal any changes in the state of the connection. These notifications are instances of the HeartBeatNotification class. The HeartBeatClientHandler interface includes methods specifically for registering for heartbeat notifications. These methods are distinct from those of the NotificationRegistration interface that a connector client implements for transmitting agent-side notifications (see "Registering Manager-Side Listeners").
// Register this manager as a listener for heartbeat notifications // (the filter and handback objects are not used in this example) // echo("\tAdd this manager as a listener for heartbeat notifications..."); connectorClient.addHeartBeatNotificationListener(this, null, null); |
Instances of heartbeat notifications contain the connector address object from the connection that generated the event. This allows the notification handler to listen to any number of connectors and retrieve all relevant information about a specific connection when it triggers a notification. The HeartBeatNotification class defines constants to identify the possible notification type strings for heartbeat events:
CONNECTION_ESTABLISHED - Emitted when the connect method succeeds.
CONNECTION_RETRYING - After the heartbeat fails and if the number of retries is not zero, this notification type is emitted once when the first retry is sent.
CONNECTION_REESTABLISHED - Emitted if the heartbeat recovers during one of the retries.
CONNECTION_LOST - Emitted after the heartbeat and all retries, if any, have failed or when a heartbeat contains the wrong connection identifier, indicating that the connector server has been stopped and restarted.
CONNECTION_TERMINATED - Emitted when the disconnect method successfully terminates a connection and frees all the resources it used; therefore, this notification type is received after both a user-terminated connection and after a connection is lost.
Once they are established, connections can go through any number of retrying-reestablished cycles, and then be terminated by the user or determined to be lost and terminated automatically. When the heartbeat mechanism is deactivated by setting the heartbeat period to zero, only heartbeat notifications for normally established and normally terminated connections will continue to be sent. In that case, connections may be lost but they will not be detected nor signaled by a notification.
The following diagram shows the possible sequence of heartbeat notifications during a connection. Retries are enabled when the getHeartBeatRetries method returns an integer greater than zero.
The following example shows the source code for the notification handler method in our manager class. The handler prints out the notification type and the RMI address associated with the connector that generated the notification:
public void handleNotification(Notification notification, Object handback){ echo("\n>>> Notification has been received..."); echo("\tNotification type = " + notification.getType()); if (notification instanceof HeartBeatNotification) { ConnectorAddress notif_address = ((HeartBeatNotification)notification).getConnectorAddress(); if (notif_address instanceof RmiConnectorAddress) { RmiConnectorAddress rmi_address = (RmiConnectorAddress) notif_address; echo("\tNotification connector address:"); echo("\t\tTYPE = " + rmi_address.getConnectorType()); echo("\t\tHOST = " + rmi_address.getHost()); echo("\t\tPORT = " + rmi_address.getPort()); echo("\t\tSERVER = " + rmi_address.getName()); } } } |
In the agent application, the connector server does not emit any notifications about the state of its connections. The HTTP protocol-based connectors do provide a count of active clients, but there is no direct access to heartbeat information in an agent's connector servers.
The examplesDir/HeartBeat directory contains all of the files for the Agent and Client applications which demonstrate the heartbeat mechanism through an RMI connector.
Compile all files in this directory with the javac command. For example, on the Solaris platform with the Korn shell, you would type:
$ cd examplesDir/HeartBeat/ $ javac -classpath classpath *.java |
To demonstrate the various communication scenarios, we run the example three times: once to see a normal termination, once to see how the manager reacts to a lost connection, and once to see how the agent reacts to a lost connection.
Launch the agent on another host or in another terminal window with the following command:
$ java -classpath classpath Agent |
The agent only creates the RMI connector server to which the client application will establish a connection, and then it waits for management operations.
Wait for the agent to be completely initialized, then launch the manager with the following command, where hostname is the name of the machine running the agent. The RMI connector in this example uses port 1099 by default. If you launched the agent on the same machine, you can omit the hostname and the port number:
$ java -classpath classpath Client hostname 1099 |
The client application creates the RMI connector client, configures its heartbeat, and registers a notification listener, as seen in the code examples. When the connection is established, the listener outputs the notification information in the terminal window.
Press <Enter> in the manager window to call the disconnect method on the connector client and stop the Client application.
In the terminal window, the heartbeat notification listener outputs the information for the normal connection termination before the application ends.
Leave the agent application running for the next scenario.
Launch the Client application again with the same command as before:
$ java -classpath classpath Client hostname 1099 |
When the connection is established, type <Control-C> in the agent's window to stop the connector server and the agent application. This simulates a broken communication channel as seen by the connector client.
Less than a second later, when the next heartbeat fails, the heartbeat retrying notification is displayed in the manager's terminal window. Two seconds later, after both retries have failed, the lost connection and terminated connection notifications are displayed.
Press <Enter> in the manager window to exit the Client application.
Launch the agent in debug mode on another host or in another terminal window with the following command:
$ java -classpath classpath -DLEVEL_DEBUG Agent |
Wait for the agent to be completely initialized, then launch the Client application again with the same command as before:
$ java -classpath classpath Client hostname 1099 |
When the connection is established, you should see the periodic heartbeat messages in the debug output of the agent.
This time, type <Control-C> in the client's window to stop the connector client and the manager application. This simulates a broken communication channel as seen by the connector server in the agent.
After the heartbeat retry timeout elapses in the agent, you should see the lost connection message in the heartbeat debugging output of the agent.
Type <Control-C> in the agent window to stop the agent application and end the example.
In the previous topic, we saw how to access a remote agent and interact with its MBeans through connectors. The Java Dynamic Management Kit provides additional functionality which makes the remoteness of an agent and the communication layer even more transparent: proxy objects for registered MBeans.
A proxy is an object instance which represents an MBean, which mirrors the methods of the MBean, and whose methods are invoked directly by the caller. The proxy transmits requests to the MBean, through the MBean server, possibly through a connector, and returns any responses to the caller. Proxy objects can also register listeners for notifications that the MBean may emit.
The advantage of a proxy object is that it allows applications to have an instance of an object which represents an MBean, instead of accessing the MBean's management interface through methods of the MBean server or through a connector client. This can simplify both the conceptual design of a management system and the source code needed to implement that system.
While the concept of proxy objects remains unchanged from previous versions of the Java Dynamic Management Kit, the binding mechanisms have been modified in version 4.2.
The old proxy creation and binding methods were not thread-safe and are now deprecated. Callers are now responsible for instantiating and binding the proxy objects that they wish to use; see "The Proxy Interface" for details.
In addition, the new design allows proxies to be bound on both the agent and manager sides for a more symmetric usage.
The code samples in this topic are taken from the files in the SimpleClients example directory located in the main examplesDir (see "Directories and Classpath" in the preface).
Contents:
"The Proxy Mechanism" describes how proxy objects are implemented and the interfaces on which they rely.
"Standard MBean Proxies" shows how to generate proxy objects for standard MBeans and run the corresponding example.
"Generic Proxies" describes the proxy objects for dynamic MBeans and shows how to run the corresponding example.
Finally, "Proxies for Java DMK Components" explains how to use the pre-generated proxy objects provided with the product.
Proxy objects simplify the interactions between an application and the MBeans it wants to manage. The purpose of a proxy is to invoke the methods that access the attributes and operations of an MBean, through its MBean server. These method calls can be rather tedious to construct at every invocation, so the proxy performs this task for the caller.
For example to invoke an operation on an MBean, an application must call the invoke method of the MBean server and provide the MBean's object name, the operation name string, an array of parameter objects, and a signature array. The proxy is a class which codes this whole sequence, meaning that the application can call the reset method directly on the proxy instance. And because the code of a proxy class is deterministic, it can be generated automatically using the proxygen tool.
Conceptually, a proxy instance makes the MBean server and a protocol connector completely transparent. Except for MBean registration and connector connection phases, all management requests on MBeans can be fully served through proxies, with identical results. However, all functionality of the Java Dynamic Management Kit is available without using proxies, so their usage is never mandatory.
By definition, a proxy object has the same interface as its MBean: the proxy can be manipulated as if it were the MBean instance, except that all requests are transmitted through the MBean server to the actual MBean instance for processing. A standard MBean proxy exposes getters, setters, and operation methods. It may also register listeners for all notifications broadcast by their corresponding MBean. A dynamic MBean proxy, also known as a generic proxy, exposes generic methods which are identical to those of the DynamicMBean interface.
In addition, standard proxies are commonly called proxy MBeans, because they are themselves MBeans. They are generated with an MBean interface, and can therefore be registered as standard MBeans in an MBean server. This feature allows one agent to expose resources whose MBeans are actually located in another agent. An equivalent functionality is covered in the topic of "Cascading Agents" in the lesson on "Agent Services".
Proxies may also be bound to objects called handlers which necessarily implement the ProxyHandler interface. The methods of this interface are a subset of MBean server methods, as listed in Table 8-1 (see "The RemoteMBeanServer Interface"). These are the only methods that a proxy needs to in order to access its corresponding MBean and fulfill all management requests.
In the Java Dynamic Management Kit, the RemoteMBeanServer interface extends the ProxyHandler interface, meaning that proxy objects may be bound to any of the connector clients. These are called remote proxies, because they are instantiated in an application which is distant from the agent and its MBean instances.
As a new feature in version 4.2 of the product, the implementation of the MBeanServer interface also implements the ProxyHandler interface, so that proxy objects may be bound to the MBean server itself. These are called local proxies because they are located in the same application as their corresponding MBeans. Local proxies help preserve the management architecture by providing the simplicity of performing direct method calls on MBeans, while still routing all operations through the MBean server.
The symmetry of remote and local proxies complements the symmetry that allows management components to execute either in an agent or in a management application. Provided that all proxy classes are available, management components which use MBean proxies may be instantiated in an agent and rely on the MBean server or may be instantiated in a remote manager where they interact with a connector client. Except for communication delays, the results of all operations will be identical, and the same notifications will be received, whether obtained through a local proxy or a remote proxy (see "Adding a Listener Through the Connector").
The following diagram shows local proxies which are instantiated in an agent and bound to the MBean server, and the same classes instantiated as remote proxies in a management application and bound to the connector client. Management components located in either the agent or management application can interact with the local or remote proxies, respectively. Management components may also access the MBean server or the connector client directly, regardless of whether proxies are being used.
This diagram shows all possible relations between management components, proxies and their MBeans. Standard proxies can only represent a specific standard MBean, and generic proxies may represent any standard or dynamic MBean. In a typical management scenario, the management components will be located in only one application, and for simplicity, they will rarely instantiate more than one type of proxy.
Throughout the rest of this topic, we do not distinguish between local and remote proxies. A proxy object, either standard or generic, is used in exactly the same way regardless of whether it is instantiated locally or remotely.
In addition to the methods for accessing the attributes and operations of its MBean, all proxies implement the methods of the Proxy interface. This interface contains the methods for binding the proxy instance to the proxy handler which can fulfill its requests. The setServer method binds the proxy to the handler object. Setting the server to null effectively unbinds the proxy object. The result of the getServer method can indicate whether or not a proxy is bound and if so, it will return a reference to the handler object.
Because of the new design of the proxy mechanism, many methods in the Proxy interface are deprecated as of version 4.2 of the product. The functionality of the deprecated methods is preserved in the three non-deprecated methods, listed in the following table. See the Javadoc API of the Proxy interface for details.
Table 9-1 Non-Deprecated Methods of the Proxy InterfaceObjectInstance | getMBeanObjectInstance() |
ProxyHandler | getServer() |
void | setServer(ProxyHandler server) |
Standard proxies may also implement the NotificationBroadcasterProxy interface if their corresponding MBean is a notification proxy. This interface contains the same addNotificationListener and removeNotificationListener methods that the MBean implements from the NotificationBroadcaster interface.
Applications that use proxies therefore have two ways to detect notification broadcasters. The first way relies on the implementation of the NotificationBroadcasterProxy interface which can be detected in the proxy's class inheritance. The second and more standard way is to look at the notifications listed in the MBean's metadata obtained by the getMBeanInfo method either from the server or through the proxy.
Generic proxies do not implement the NotificationBroadcasterProxy interface, so callers must use the MBean metadata for detecting broadcasters. In addition, generic proxies cannot register notification listeners, callers must do this directly through the server.
A standard MBean proxy class is specific to its corresponding MBean class. Furthermore, a proxy instance is always bound to the same MBean instance, as determined by the object name passed to the proxy's constructor. Binding the proxy to its ProxyHandler object can be done through a constructor or set dynamically through the methods of the Proxy interface.
The methods of a standard proxy have exactly the same signature as those of the corresponding standard MBean. Their only task is to construct the complete management request, which necessarily includes the object name, and to transmit it to the MBean server or connector client. They also return any result directly to the caller.
Because the contents of all proxy methods are determined by the management interface of the MBean, the proxy classes can be generated automatically.
The proxygen tool provided with the Java Dynamic Management Kit takes the class files of an MBean and its MBean interface, and generates the Java source code files of its corresponding proxy object and proxy MBean interface. You then need to compile the two proxy files with the javac command and include the resulting class files in your application's classpath.
The proxygen command is fully documented in the Java Dynamic Management Kit 4.2 Tools Reference guide, and in the Javadoc API for the ProxyGen class. Its command line options allow you to generate read-only proxies where all setter methods are suppressed and to define a package name for your proxies. For the purpose of the examples, we generate the default proxies without any of these options: see the section on "Running the Standard Proxy Example".
The following code sample shows part of the code generated for the SimpleStandard MBean used in the SimpleClients examples.
public java.lang.String getState() throws InstanceNotFoundException, AttributeNotFoundException, ReflectionException, MBeanException { return ((java.lang.String)server.getAttribute( objectInstance.getObjectName(), "State")); } public void setState(java.lang.String value) throws InstanceNotFoundException, ReflectionException, AttributeNotFoundException,InvalidAttributeValueException, MBeanException { server.setAttribute(objectInstance.getObjectName(), new Attribute("State",value)); } public void reset() throws InstanceNotFoundException, ReflectionException, MBeanException { Object result; result= server.invoke(objectInstance.getObjectName(), "reset", null, null); } |
You are free to modify the generated code if you wish to customize the behavior of your proxies. However, customization is not recommended if your MBean interfaces are still evolving, because all modifications will need to be redone every time the proxies are generated.
Once the proxies are generated and available in your application's classpath, their usage is straightforward. For each of the proxy objects it wishes to use, the application needs to instantiate its proxy class and then bind it to a ProxyHandler object. The application is responsible for creating and binding all of the proxies that it wishes to use, and it must unbind and free them when they are no longer needed.
In previous versions of the Java Dynamic Management Kit, the connector client handled the creation of proxy instances and insured that only one proxy object could exist for each MBean. As of this version (4.2) of the product, connector clients no longer instantiate nor control proxy objects. The corresponding methods of the RemoteMBeanServer interface are now deprecated.
Similarly, the previous binding methods in the Proxy interface are deprecated in favor of the new setServer and getServer methods. This change is necessary so that proxies may be bound to any ProxyHandler object, to allow for both local and remote proxies.
All parameters for binding the proxy can be given in its constructor, which makes it very simple to instantiate and bind a proxy in one step.
String mbeanName = "SimpleStandard"; // build the MBean ObjectName instance ObjectName mbeanObjectName = null; String domain = connectorClient.getDefaultDomain(); mbeanObjectName = new ObjectName( domain + ":type=" + mbeanName ); // create the MBean in the MBeanServer of the agent String mbeanClassName = mbeanName; ObjectInstance mbeanObjectInstance = connectorClient.createMBean( mbeanClassName, mbeanObjectName ); // create and bind a proxy MBean on the client side // that corresponds to the MBean just created in the agent Proxy mbeanProxy = new SimpleStandardProxy( mbeanObjectInstance, connectorClient ); echo("\tPROXY CLASS NAME = " + mbeanProxy.getClass().getName() ); echo("\tMBEAN OBJECT NAME = " + mbeanProxy.getMBeanObjectInstance().getObjectName() ); echo("\tCONNECTOR CLIENT = " + mbeanProxy.getServer().getClass().getName() ); |
If the class name of your proxy is not known at compile time, you will have to instantiate its class dynamically. In the following code, we obtain the proxy class name which corresponds to an MBean, and we call its first constructor. This must be the constructor which takes an ObjectInstance identifying the MBean, and we must dynamically build the call to this constructor. We then call the setServer method to bind the new proxy instance.
// Get the class name of the MBean's proxy Class proxyClass = Class.forName( connectorClient.getClassForProxyMBean( mbeanObjectInstance )); // Find the constructor with takes an ObjectInstance parameter Class[] signature = new Class[1]; signature[0] = Class.forName("javax.management.ObjectInstance"); Constructor proxyConstr = proxyClass.getConstructor( signature ); // Call the constructor to instantiate the proxy object Object[] initargs = new Object[1]; initargs[0] = mbeanObjectInstance; Proxy proxy2 = (Proxy) proxyConstr.newInstance( initargs ); // Bind the proxy proxy2.setServer( connectorClient ); echo("\tPROXY CLASS NAME = " + proxy2.getClass().getName()); echo("\tMBEAN OBJECT NAME = " + proxy2.getMBeanObjectInstance().getObjectName()); echo("\tCONNECTOR CLIENT = " + proxy2.getServer().getClass().getName()); // We no longer need proxy2, so we unbind it proxy2.setServer( null ); |
Once a proxy is bound, you can access the attributes and operations of its MBean through direct calls to the proxy object, as shown in the following example.
try { // cast mbeanProxy to SimpleStandardProxy, so we can // call its MBean specific methods SimpleStandardProxy simpleStandardProxy = (SimpleStandardProxy) mbeanProxy; [...] // Change the "State" attribute simpleStandardProxy.setState("new state from client"); // Get and display the new attribute values echo("\tState = \"" + simpleStandardProxy.getState() + "\""); echo("\tNbChanges = " + simpleStandardProxy.getNbChanges()); // Invoke the "reset" operation simpleStandardProxy.reset(); [...] // We are done with the MBean, so we // unbind the proxy and unregister the MBean simpleStandardProxy.setServer( null ); connectorClient.unregisterMBean( mbeanObjectName ); } catch (Exception e) { echo("\t!!! Error accessing proxy for " + mbeanProxy.getMBeanObjectInstance().getObjectName() ); e.printStackTrace(); } |
The examplesDir/SimpleClients directory contains all of the files for the ClientMBeanProxy application which demonstrates the use of standard MBean proxies.
If you haven't done so already, compile all files in this directory with the javac command. For example, on the Solaris platform with the Korn shell, you would type:
$ cd examplesDir/SimpleClients/ $ javac -classpath classpath *.java |
Before running the example, you must also generate the proxy MBeans classes and compile them as well. From the same directory as above, type the following commands:
$ installDir/SUNWjdmk/jdmk4.2/JDKversion/proxygen SimpleStandard $ javac -classpath classpath SimpleStandardProxy.java |
Launch the base agent in a terminal window with the following command:
$ java -classpath classpath BaseAgent |
The agent creates the RMI connector server to which the client application will establish a connection, and then it waits for management operations.
Wait for the agent to be completely initialized, then, in another window on the same host, launch the management application with the following command:
$ java -classpath classpath ClientMBeanProxy |
Press <Enter> in the manager window to step through the example.
As seen in the code examples, the client application instantiates the proxy objects to access the MBean it has created in the base agent.
Press <Enter> one last time to exit the manager application, but leave the base agent running for the next example.
Because dynamic MBeans only expose their management at runtime, it is impossible to generate a specific proxy object for them. Instead, we use the GenericProxy object which can be bound to any dynamic MBean, and whose generic methods take the name of the attribute or operation being accessed. Therefore, to access a dynamic MBean through generic proxy you invoke exactly the same methods as those of the DynamicMBean interface.
Just as the MBean server's generic methods can access both standard and dynamic MBeans, generic proxies can also be bound to standard MBeans. You lose the specificity and simplicity of a standard proxy, but a generic proxy is always available in any Java Dynamic Management application, and it never needs regenerating.
The management application in this example shows how generic proxies can be used to access both standard and dynamic MBeans. The application contains the following subroutine which takes the class name of an MBean, creates that MBean in the agent, and instantiates a generic proxy to access the MBean.
In fact, the subroutine instantiates two generic proxies for the MBean, one using the GenericProxy class constructor which also binds the proxy, the other bound in a second, separate call to its setServer method. This demonstrates that it is possible to have two distinct proxy instances coexisting simultaneously for the same MBean.
private void doGenericProxyExample( String mbeanName ) { try { // build the MBean ObjectName instance ObjectName mbeanObjectName = null; String domain = connectorClient.getDefaultDomain(); mbeanObjectName = new ObjectName( domain + ":type=" + mbeanName); // create the MBean in the MBeanServer of the agent String mbeanClassName = mbeanName; ObjectInstance mbeanObjectInstance = connectorClient.createMBean( mbeanClassName, mbeanObjectName ); // create and bind a generic proxy instance for the MBean Proxy proxy = new GenericProxy( mbeanObjectInstance, connectorClient ); echo("\tPROXY CLASS NAME = " + proxy.getClass().getName()); echo("\tMBEAN OBJECT NAME = " + proxy.getMBeanObjectInstance().getObjectName()); echo("\tCONNECTOR CLIENT = " + proxy.getServer().getClass().getName()); // An alternate way is to first instantiate the generic proxy, // and then to bind it to the connector client: Proxy proxy2 = new GenericProxy( mbeanObjectInstance ); proxy2.setServer( connectorClient ); echo("\tPROXY CLASS NAME = " + proxy2.getClass().getName()); echo("\tMBEAN OBJECT NAME = " + proxy2.getMBeanObjectInstance().getObjectName()); echo("\tCONNECTOR CLIENT = " + proxy2.getServer().getClass().getName()); // we no longer need proxy2, so we unbind it proxy2.setServer(null); [...] // Accessing the MBean through its generic proxy (see below) // When done with the MBean, we unbind the proxy // and unregister the MBean // proxy.setServer(null); connectorClient.unregisterMBean( mbeanObjectName ); } catch (Exception e) { echo("\t!!! Error instantiating or binding proxy for " + mbeanName ); e.printStackTrace(); } } |
The standard and dynamic MBean classes used in this example have exactly the same management interface, and therefore, we can use the same code to access both of them. The manager application does this by calling the above subroutine twice, once with the class name of the standard MBean, once with that of the dynamic MBean:
manager.doGenericProxyExample("SimpleStandard"); manager.doGenericProxyExample("SimpleDynamic");
Because the two MBeans have the same behavior, they will produce the same results when accessed through their proxy. The only difference is that the dynamic MBean can expose a description of its management interface in its MBeanInfo object. As expected, accessing a standard MBean through a generic proxy also produces the same result as when it is accessed through a standard proxy (compare the following with Example 9-4).
try { // cast Proxy to GenericProxy GenericProxy genericProxy = (GenericProxy) proxy; // Get the MBean's metadata through the proxy MBeanInfo info = genericProxy.getMBeanInfo(); // display content of the MBeanInfo object echo("\nCLASSNAME: \t"+ info.getClassName() ); echo("\nDESCRIPTION: \t"+ info.getDescription() ); [...] // extract all attribute and operation info // Change the "State" attribute Attribute stateAttr = new Attribute( "State", "new state from client"); genericProxy.setAttribute( stateAttr ); // Get and display the new attribute values String state = (String) genericProxy.getAttribute("State"); Integer nbChanges = (Integer) genericProxy.getAttribute("NbChanges"); echo("\tState = \"" + state + "\""); echo("\tNbChanges = " + nbChanges); // Invoke the "reset" operation Object[] params = new Object[0]; String[] signature = new String[0]; genericProxy.invoke("reset", params, signature ); } catch (Exception e) { echo("\t!!! Error accessing proxy for " + proxy.getMBeanObjectInstance().getObjectName() ); e.printStackTrace(); } |
The above code listing shows how the generic methods are called with the names of attributes and operations, and how required parameters can be constructed.
The ClientGenericProxy application, also in the examplesDir/SimpleClients directory, demonstrates the use of generic proxies.
If you haven't done so already, compile all files in this directory with the javac command. For example, on the Solaris platform with the Korn shell, you would type:
$ cd examplesDir/SimpleClients/ $ javac -classpath classpath *.java |
Because generic proxies do not need to be generated, this example does not need the proxygen tool. The GenericProxy class is available in the usual classpath for the product's runtime libraries.
If it is not already running on your host, launch the base agent in a terminal window with the following command:
$ java -classpath classpath BaseAgent |
The agent creates the RMI connector server to which the client application will establish a connection, and then it waits for management operations.
Wait for the agent to be completely initialized, then launch the management application in another window on the same host:
$ java -classpath classpath ClientGenericProxy |
Press <Enter> in the manager window to step through the example.
As seen in the code examples, the client application instantiates generic proxy objects to access both a standard and dynamic MBean that it creates in the base agent. The only difference between the two is the user-provided information available in the dynamic MBean's metadata.
Press <Enter> in both windows to exit the base agent and manager applications.
Most components of the Java Dynamic Management Kit product are MBeans and can therefore also be managed through local or remote proxies. Nearly all are standard MBeans, so their corresponding standard proxies are provided with the product. The Java source code for all component proxy classes can be found in the JdmkProxyMBeans directory located in the main examplesDir (see "Directories and Classpath" in the preface).
The HTML protocol adaptor is implemented as a dynamic MBean and therefore cannot have a standard proxy. You must use a generic proxy if you wish to access the HTML adaptor through a proxy object.
Of course, all other Java DMK MBean components may also be accessed through generic proxies, although their standard proxies provide more abstraction of the MBean server and a greater simplification of your application's code.
The proxy classes have been generated by the proxygen tool with full read-write access of all attributes. See the chapter on the proxygen tool in the Java Dynamic Management Kit 4.2 Tools Reference guide.
Like all other classes, proxies may contain a package statement. The package for a component proxy class depends upon the package of the component:
The proxy classes for Java DMK components in the javax.management package and its javax.management.* subpackages do not have a package statement.
Their class files may be located in any directory which can then be added to your application's classpath.
The proxy classes for all other components belong to the same class as the component itself. For example, the proxy classes for the RmiConnectorServer component are declared in the com.sun.jdmk.comm package.
Their class files should be contained in the corresponding file hierarchy, whose root can be added to your application's classpath. Therefore, the class files of the RmiConnectorServerProxy should be located in a directory called packageRoot/com/sun/jdmk/comm/.
To use the standard proxies for the product components, you must first compile them with the javac command and then place the classes you need in the classpath of your agent or management application. If you compile the proxy classes individually, be sure to compile the proxy's MBean interface before its corresponding proxy class.
Because of the package statement in proxy classes, we recommend using the following commands:
$ cd examplesDir/JdmkProxyMBeans/ $ javac -d packageRoot -classpath classpath *ProxyMBean.java *Proxy.java |
In this command, the classpath must contain the current directory and the classpath of the Java DMK runtime libraries (usually in installDir/SUNWjdmk/jdmk4.2/JDKversion/lib/jdmkrt.jar). The -d option of the javac compiler creates the necessary directories in the given packageRoot for each class's package. The packageRoot is usually the current directory (.), or you may directly specify a target directory in your application's classpath.
In this topic, we expand the notification mechanism to the manager side, looking at how remote applications receive notifications. Notifications are forwarded to a manager through existing structures of the Java Dynamic Management architecture: MBeans, the MBean server, and the connectors. Notably, this implies that the notification mechanism is designed to forward only notifications from registered MBeans on the agent side to proper listeners on the manager side.
As with the other management operations, listening for notifications is nearly as simple to do in a management application as it is to do locally in an agent. The interface of the connector client hides all communication issues, so that listeners may be registered through the connector or directly with existing proxy MBeans.
The code samples in this topic are taken from the files in the Notification example directory located in the main examplesDir (see "Directories and Classpath" in the preface).
Contents:
"Registering Manager-Side Listeners" shows how similar the agent side and manager side notification mechanisms are.
"Push Mode" presents the simplest forwarding policy whereby the agent sends notifications to the manager as they occur.
"Pull Mode" presents an advanced forwarding policy which buffers notifications in the agent until the manager requests them.
"Running the Notification Forwarding Example" demonstrates the two forwarding mechanisms and how to set the parameters for the two strategies.
Like the other structures of the Java Dynamic Management Kit, the notification mechanism is designed to be homogeneous from the agent to the manager side. For this reason, notification objects and listener interfaces in manager applications are identical to those on the agent side.
The symmetry of the interfaces also means that code for listeners can easily be reused without modification in either agent or manager applications. Listeners in managers are similar to those in agents, and they could even be identical objects in some management solutions. However, chances are that manager-side listeners will want to receive different notifications and take different actions than their agent-side peers.
The notification broadcasters are MBeans registered in an agent's MBean server to which our management application will need to connect. Only notifications sent by registered MBeans can be forwarded to manager applications, and a manager-side listener can receive them only by registering through a connector client or a proxy object.
Other notification broadcasters may exist independently in the manager application, but listeners will need to register directly with these local broadcasters. Nothing prevents a listener object from registering with both a connector client or proxy for remote notifications and with a local broadcaster.
The code example below shows how the sample NotificationEmitter MBean will send notifications (the code for its MBean interface has been omitted). It extends the NotificationBroadcasterSupport class to reuse all of its listener registration facilities. It only contains one operation which can be called by our manager to trigger any number of notifications.
import javax.management.MBeanNotificationInfo; import javax.management.NotificationBroadcasterSupport; import javax.management.Notification; public class NotificationEmitter extends NotificationBroadcasterSupport implements NotificationEmitterMBean { // Just to make the inheritance explicit public NotificationEmitter() { super(); } // Provide details about the notification type and class that is sent public MBeanNotificationInfo[] getNotificationInfo() { MBeanNotificationInfo[] ntfInfoArray = new MBeanNotificationInfo[1]; String[] ntfTypes = new String[1]; ntfTypes[0] = myType; ntfInfoArray[0] = new MBeanNotificationInfo( ntfTypes, "javax.management.Notification", "Notifications sent by the NotificationEmitter"); return ntfInfoArray; } // The only operation: sends any number of notifications // whose sequence numbers go from 1 to "nb" public void sendNotifications( Integer nb ) { for (int i=1; i<=nb.intValue(); i++) { sendNotification(new Notification(myType, this, i)); } } private String myType = "notification.my_notification"; } |
Our MBean invents a notification type string and exposes this information through the getNotificationInfo method. To demonstrate the forwarding mechanism, we are more interested in the sequence number: this will allow us to identify the notifications as they are received in the manager.
This MBean demonstrates that the broadcaster has total control over the contents of its notifications. Constructors for the Notification object allow you to specify all of the fields, even ones such as the time stamp. In this example, we control the sequence number, and our chosen policy is to reset the sequence number to 1 with every call to the operation. Of course, you are free to choose the notification contents, including the time-stamping and sequence-numbering policies that fit your management solution.
Due to possible loss in the communication layer and the inherent indeterminism of thread execution, the notification model does not guarantee that remote notifications will be received nor that their sequence will be preserved. If notification order is critical to your application, your broadcaster should set the sequence numbers appropriately, and your listeners should sort the received notifications.
In our simple example, the Client class itself is the listener object. Usually, a listener would be a separate instance of a special listener class and depending on the complexity of the manager, there might be several classes of listeners, each for a specialized category of notifications.
public class Client implements NotificationListener { [...] // Constructor omitted // Implementation of the NotificationListener interface // public void handleNotification(Notification notif, Object handback) { System.out.println("Client: received a notification of type " + notif.getType() + "\nwith the sequence number " + notif.getSequenceNumber()); } [...] // main omitted } |
As explained in the notification mechanism "Overview", a listener on the agent side is typically an MBean which receives notifications about the status of other MBeans and then processes or exposes this information in some manner. Only if a key value or some management event is observed will this information be passed to a listening manager, probably by sending a different notification.
In this manner, the notification model reduces the communication that is necessary between agents and managers. Your management solution determines how much decisional power resides in the agent and when situations are escalated. These parameters will affect your design of the notification flow between broadcasters, listeners, agents, and managers.
The usual role of a manager-side listener is to process the important information in a notification and take the appropriate action. As we shall see, our notification example is much simpler. Our goal is not to construct a real-world example, but to demonstrate the mechanisms that are built into the Java Dynamic Management Kit.
By extension of the ClientNotificationHandler interface, the RemoteMBeanServer interface exposes methods for adding and removing listeners. The signatures of these methods are identical to those of the agent-side MBeanServer interface. The only difference is that they are implemented in the connector client classes which make the communication protocol transparent.
Our manager application uses the RMI protocol connector. After creating the connector client object, we use the methods of its RemoteMBeanServer interface to create our broadcaster MBean and then register as a listener to this MBean's notifications.
// Use RMI connector on port 8086 to communicate with the agent System.out.println(">>> Create an RMI connector client"); RmiConnectorClient connectorClient = new RmiConnectorClient(); // agentHost was read from the command line or defaulted to localhost RmiConnectorAddress rmiAddress = new RmiConnectorAddress( agentHost, 8086, com.sun.jdmk.ServiceName.RMI_CONNECTOR_SERVER); connectorClient.connect(rmiAddress); // Wait 1 second for connecting Thread.sleep(1000); // Create the MBean in the agent ObjectName mbean = new ObjectName ("Default:name=NotificationEmitter"); connectorClient.createMBean("NotificationEmitter", mbean); // Now add ourselves as the listener (no filter, no handback) connectorClient.addNotificationListener(mbean, this, null, null); |
You can see how similar this code is to the agent application by comparing it with the code example for "Adding a Listener Through the MBean Server".
If you have generated and instantiated proxy MBeans for your broadcaster MBeans, you can also register through the addNotificationListener method that they expose. When generating proxy classes with the proxygen tool, MBeans which implement the NotificationBroadcaster interface will have proxy classes which implement the NotificationBroadcasterProxy interface.
Again, the method signatures defined in a proxy MBean are identical to those of the MBeanServer or NotificationBroadcasterClient interfaces for adding or removing listeners: see the code example for "Adding a Listener Directly to an MBean". Listeners added through a proxy MBean will received the same notifications as listeners added to the same MBean through the interface of the connector client.
Following the Java programming model, the connector client limits its resource usage by only running one thread to notify all of its listeners. This thread calls all of the handler callback methods that have been added through this connector. Therefore, the callbacks should return quickly and use safe programming to avoid crashing the connector client.
Because the broadcaster and the listener are running on separate machines or in separate virtual machines on the same host, their notifications must be forwarded from one to the other. The mechanism for doing this is completely transparent to the users of the Java Dynamic Management Kit components.
Briefly, the connector client instructs the connector server to add its own agent-side listener to the designated broadcaster using the methods of the MBeans server. Then, the connector server implements a buffering cache mechanism to centralize notifications before serializing them to be forwarded to the connector client. By design, it is the connector client in the manager application that controls the buffering and forwarding mechanism for a connection.
The following diagram summarizes the notification forwarding mechanism and its actors. In particular, it shows how the connector server uses internal listener instances to register locally for MBean notifications, even if this mechanism is completely hidden from the user. The path of listener registration through calls to the addNotificationListener method of the various interfaces is paralleled by the propagation of notifications through calls to the listeners' handleNotification method.
Neither the broadcaster nor the listener need to implement any extra methods, or even be aware that the other party is remote. Only the designer needs to be aware of communication issues such as delays: you can't expect the listener to be invoked instantaneously after a remote broadcaster sends a notification.
The forwarding mechanism allows you to configure how and when notifications are forwarded. This allows you to optimize the communication strategy between your agents and managers. There are two basic modes for notification forwarding: push mode and pull mode. A notification in the connector server's cache is either pushed to the manager at the agent's initiative, or pulled by the manager at its own initiative.
The push mode for notification forwarding is the simplest because it implements the expected behavior of a notification. When a notification is sent from an MBean to its listener, it is immediately pushed to the manager-side where the listener's handler method is called. There is no delay in the caching, and if the communication layer is quick enough, the listener is invoked almost immediately after the notification is sent.
Push mode is the default forwarding policy of a newly instantiated connector client.
In our manager example, we explicitly set the connector client in push mode and then trigger the agent-side notifications.
System.out.println("\n>>> Set notification forward mode to PUSH."); connectorClient.setMode(ClientNotificationHandler.PUSH_MODE); System.out.println(">>> Have our MBean broadcast 10 notifications..."); params[0] = new Integer(10); signatures[0] = "java.lang.Integer"; connectorClient.invoke(mbean, "sendNotifications", params, signatures); System.out.println(">>> Done."); System.out.println(">>> Receiving notifications...\n"); // Nothing to do but wait while our handler receives the notifications Thread.sleep(2000); |
The connector client exposes the methods for controlling the agent's notification buffer. This caching buffer is not used in push mode, so these methods do not affect pushed notifications. The methods do however set internal variables that will be taken into account if and when pull mode is enabled. Future versions of the product may implement push-mode buffering to provide added functionality.
The advantage of push mode is that it works without any further intervention: notifications eventually reach their remote listeners. Push mode works when the communication layer and the listener's processing capacity are adapted to the notification emission rate, or more specifically to the potential emission rate. Since all notifications are immediately sent to the manager hosts, a burst of notifications will cause a burst of traffic that may or may not be adapted to the communication layer.
If your communication layer is likely to be saturated, either your design should control broadcasters to prevent bursts of notifications, or you should use the pull mode which has this control functionality built-in. The push mode is ideal if you have reliable and fast communication between your agents and your managers. You may also dynamically switch between modes, allowing a management application to fine-tune its communication policy depending on the number of notifications that must be handled.
In pull mode, notifications are not immediately sent to their remote listeners. Rather, they are stored in the connector server's internal buffer until the connector client requests that they be forwarded. Instead of being sent individually, the notifications are grouped to reduce the load on the communication layer. Pull mode has the following settings that let the manager define the notification forwarding policy:
A period for automatic pulling
The size of the agent-side notification buffer (also called the cache)
The policy for discarding notifications when this buffer is full
For a given connection, there is one cache for all listeners, not one cache per listener. This cache therefore has one buffering policy whose settings are controlled through the methods exposed by the connector client. The cache buffer contains an unpredictable mix of notifications in transit to all manager-side listeners added through a given connector client or through one of its bound proxy MBeans. The buffer operations such as pulling or overflowing apply to this mix of notifications, not to any single listener's notifications.
Pull mode forwarding is necessarily a compromise between receiving notifications in a timely manner, not saturating the communication layer, and not overflowing the buffer. Notifications are stored temporarily in the agent-side buffer, but the manager-side listeners still need to receive them. Pull mode includes automatic pulling that retrieves all buffered notifications regularly.
The frequency of the pull forwarding is controlled by the pull period expressed in milliseconds. By default, when pull mode is enabled, the manager will automatically begin pulling notifications once per second. Whether or not there are any notifications to receive depends upon events in the agent.
Our manager application sets a half-second pull period and then triggers the notification broadcaster.
System.out.println(">>> Set notification forward mode to PULL."); connectorClient.setMode(ClientNotificationHandler.PULL_MODE); // Retrieve buffered notifications from the agent twice per second System.out.println(">>> Set the forward period to 500 milliseconds."); connectorClient.setPeriod(500); System.out.println(">>> Have our MBean broadcast 20 notifications..."); params[0] = new Integer(20); signatures[0] = "java.lang.Integer"; connectorClient.invoke(mbean, "sendNotifications", params, signatures); System.out.println(">>> Done."); // Wait for the handler to process all notifications System.out.println(">>> Receiving notifications...\n"); Thread.sleep(2000); |
When notifications are pulled, all notifications in the agent-side buffer are forwarded to the manager and the registered listeners. It is not possible to set a limit on the number of notifications which are forwarded, except by limiting the size of the buffer (see "Agent-Side Buffering"). Even in a controlled example such as ours, the number of notifications in the agent-side buffer at each pull period is completely dependent upon the agent's execution paths, and therefore unpredictable from the manager-side.
You can disable automatic pulling by setting the pull period to zero. In this case, the connector client will not pull any notifications from the agent until instructed to do so. Use the getNotifications method of the connector client to pull all notifications when desired. This method will immediately forward all notifications in the agent-side buffer. Again, it is not possible to limit the number of notifications that are forwarded, except by limiting the buffer size.
In this example, we disable the automatic pulling and then trigger the notification broadcaster. The notifications will not be received until we request that the connector server pull them. Then, all of the notifications will be received at once.
System.out.println(">>> Use pull mode with period set to zero."); connectorClient.setMode(ClientNotificationHandler.PULL_MODE); connectorClient.setPeriod(0); System.out.println(">>> Have our MBean broadcast 30 notifications..."); params[0] = new Integer(30); signatures[0] = "java.lang.Integer"; connectorClient.invoke(mbean, "sendNotifications", params, signatures); System.out.println(">>> Done."); // Call getNotifications to pull all buffered notifications from the agent System.out.println("\n>>> Press <Enter> to pull the notifications."); System.in.read(); connectorClient.getNotifications(); // Wait for the handler to process all notifications Thread.sleep(100); |
In the rest of our example, we use the on-demand forwarding mechanism to control how many notifications are buffered on the agent-side and thereby test the different caching policies.
In pull mode, notifications are stored by the connector server in a buffer until they are pulled by the connector client. Any one of the pull operations, whether on-demand or periodic, empties this buffer, and it fills up again as new notifications are triggered.
By default, this buffer will grow to contain all notifications. The ClientNotificationHandler interface defines the static NO_CACHE_LIMIT field to represent an unlimited buffer size. If the notifications are allowed to accumulate indefinitely in the cache, this can lead either to an "out of memory" error in the agent application, a saturation of the communication layer, or an overload of the manager's listeners when the notifications are finally pulled.
To change the size of the agent's cache, call the connector client's setCacheSize method. The size of the cache is expressed as the number of notifications which can be stored in its buffer. When a cache buffer of limited size is full, new notifications will overflow and be lost. Therefore, you should also choose an overflow mode when using a limited cache size. The two overflow modes are defined by static fields of the ClientNotificationHandler interface:
DISCARD_OLD - The oldest notifications will be lost and the buffer will always be renewed with the latest notifications which have been triggered. This is the default value when a limit is first set for the cache size.
DISCARD_NEW - Once the notification buffer is full, any new notifications will be lost until the buffer is emptied by forwarding the messages. The buffer will always contain the first notifications triggered after the previous pull operation.
We demonstrate each of these modes in our sample manager, by first setting the cache size and the overflow mode, then by triggering more notifications than the cache buffer can hold.
System.out.println(">>> Use pull mode with period set to zero, " + "buffer size set to 10, and overflow mode set to DISCARD_OLD."); connectorClient.setMode(ClientNotificationHandler.PULL_MODE); connectorClient.setPeriod(0); connectorClient.setCacheSize(10, true); // see "Buffering Specifics" connectorClient.setOverflowMode(ClientNotificationHandler.DISCARD_OLD); System.out.println(">>> Have our MBean broadcast 30 notifications..."); params[0] = new Integer(30); signatures[0] = "java.lang.Integer"; connectorClient.invoke(mbean, "sendNotifications", params, signatures); System.out.println(">>> Done."); // Call getNotifications to pull all buffered notifications from the agent System.out.println("\n>>> Press <Enter> to get notifications."); System.in.read(); connectorClient.getNotifications(); // Wait for the handler to process the 10 notifications // These should be the 10 most recent notifications // (the greatest sequence numbers) Thread.sleep(100); System.out.println("\n>>> Press <Enter> to continue."); System.in.read(); // We should see that the 20 other notifications overflowed the agent buffer System.out.println(">>> Get overflow count = " + connectorClient.getOverflowCount()); |
The overflow count gives the total number of notifications that have been discarded because the buffer has overflowed. The number is cumulative from the first manger-side listener registration until all of the manager's listeners have been unregistered. The manager application can modify or reset this value by calling the setOverflowCount method.
In our example application, we repeat the actions above, in order to cause the buffer to overflow again, but this time using the DISCARD_NEW policy. Again, the buffer size is ten, and there are 30 notifications. In this mode, the first 10 sequence numbers will remain in the cache to be forwarded when the manager pulls them from the agent, and 20 more will have overflowed.
When the buffer is full and notifications need to be discarded, the time reference for applying the overflow mode is the order in which notifications have arrived in the buffer. Neither the time stamps nor the sequence numbers of the notifications are considered, since neither of these are necessarily absolute; even the sequence of notifications from the same broadcaster can be non-deterministic. And in any case, broadcasters are free to set both time stamps and sequence numbers as they see fit, or even to make them null.
The second parameter of the setCacheSize method is a boolean which determines whether or not the potential overflow of the cache is discarded when reducing the cache size. If the currently buffered notifications do not fit into the new cache size and this parameter is true, excess notifications are discarded according to the current overflow mode. The overflow count is also updated accordingly.
In the same situation with the parameter set to false, the cache will not be resized. You need to check the return value of the method when you set this parameter to false. If the cache cannot be resized because it would lead to discarded notifications, you need to empty the cache and try resizing the cache size again. To empty the cache, you can either pull the buffered notifications with the getNotifications method or discard them all by calling the connector client's clearCache method.
When the existing notifications fit within the new cache size or when increasing the cache size, the second parameter of setCacheSize has no effect.
Because several managers may connect through the same connector server object, it must handle the notifications for each separately. This implies that each connected manager has its own notification buffer and its own settings for controlling this cache. The overflow count is specific to each manager as well.
Here we have demonstrated each setting of the forwarding mechanism independently by controlling the notification broadcaster. In practice, periodic pulling, agent-side buffering and buffer overflow may all be happening at once. And you can call getNotifications at any time to do an on-demand pull of the notifications in the agent-side buffer. You should adjust the settings to fit the known or predicted behavior of your management architecture, depending upon communication constraints and your acceptable notification loss rate.
The caching policy is completely determined by the manager application. If notification loss is unacceptable, it is the manager's responsibility to configure the mechanism so that they are pulled as often as necessary. Also, the notification mechanism can be updated dynamically. For example, the manager can compute the notification emission rate and update any of the settings (buffer size, pull period, and overflow mode) to minimize the risk of a lost notification.
The examplesDir/Notification directory contains all of the files for the broadcaster MBean, the BaseAgent application, and our Client application which is itself the listener object.
Compile all files in this directory with the javac command. For example, on the Solaris platform with the Korn shell, you would type:
$ cd examplesDir/Notification/ $ javac -classpath classpath *.java |
To run the notification forwarding example, we use the BaseAgent application which contains an RMI connector server.
Launch the agent on another host or in another terminal window with the following command. Be sure that the classes for the NotificationEmitter MBean can be found in its classpath:
$ java -classpath classpath BaseAgent |
Wait for the agent to be completely initialized, then launch the manager in another window with the following command, where hostname is the name of the machine running the agent. If you launched the agent on the same machine, you can omit the hostname:
$ java -classpath classpath Client hostname |
When launched, the manager application first creates the NotificationEmitter MBean and then registers itself as a listener.
Press <Enter> when the application pauses to step through the various notification forwarding situations that we have seen in this topic.
Press <Enter> one last time in the manager window to exit the application.
Leave the agent application running if you wish interact with the example through the HTML adaptor of the BaseAgent.
Launch the manager in another window with the following command, where hostname is the name of the machine running the agent. If you launched the agent on the same machine, you can omit the hostname:
$ java -classpath classpath Client hostname |
Load the following URL in your browser and go to the MBean view of the NotificationEmitter MBean:
If you get an error, you may have to switch off proxies in your browser preference settings. Any browser on your local network can also connect to this agent using this URL.
When the manager application pauses for the first time, invoke the sendNotifications method from your browser with a small integer as the parameter.
You should see the listener handle your notifications in the manager's terminal window. Since the manager is still in push mode, they were forwarded immediately.
Press <Enter> in the manger window: the manager is now in pull mode with a pull period of 500 milliseconds. Through the MBean view, send 1000 notifications.
If your agent's host is slow enough, or your manager's host fast enough, you may be able to see the manager pause briefly after it has processed all notifications from one period and before the next ones are forwarded.
Press <Enter> in the manager window: the agent will now forward notifications by request. Before pressing <Enter> again, have the MBean send 15 notifications.
You should see the manager pull all of the notifications: the 30 triggered by the manager and the 15 we just triggered. They were all kept in the buffer, waiting for the manager's request to forward them. Remember that the sendNotifications operation resets the sequence numbering every time it is invoked.
Press <Enter> in the manager's window: the cache size will now be set to 10 notifications and the overflow mode to DISCARD_OLD. Before pressing <Enter> again, have the MBean send 15 more notifications.
Only the last ten of our notifications could fit into the cache buffer, all the rest, including those already triggered by the manager, overflowed and were discarded. Press <Enter> to see that they are tallied in the overflow count.
Press <Enter> in the manager's window: the cache size is still 10 notifications and the overflow mode will be set to DISCARD_NEW. Before pressing <Enter> again, have the MBean send only 5 more notifications.
The first ten of the manager-triggered notifications are received: all of the more recent notifications, including ours, overflowed the cache buffer and were lost. Press <Enter> to see that they are tallied in the overflow count: the 35 from the last step plus 25 more from this step, for a total of 60.
Press <Enter> in the manager's window one last time to stop the Client application. Press <Enter> in the other window to stop the agent application when you are finished running the example.
Whenever considering a distributed architecture, security issues are often an added factor in the complexity of the design. Not so with the Java Dynamic Management Kit, whose security features are built into the modularity of the components.
Management solutions can evolve from basic password-based protection all the way to secure connections using cryptography simply by switching protocol connectors or by adding filter components. The rest of the architecture is unchanged because it relies on the interface which is common to all connectors.
There are two categories of access-control: connection-level control through a password and request-level control through a context object. Context checkers work as filters between the connector server and the MBean server. The filter logic can be determined dynamically, based on the nature of the request and on a context object provided by the client.
Security in the communication layer is achieved through the cryptography of a Secure Socket Layer (SSL) and the HTTPS connector. Using other components of the Java platform, connectors can effectively make all open communication undecipherable.
The code samples in this topic are taken from the files in the Context example directory located in the main examplesDir (see "Directories and Classpath" in the preface).
Contents:
"Password-Based Authentication" shows how to provide connection-level access-control through the HTTP-based connectors.
"Context Checking" demonstrates the filter mechanism for fine-grained access control of incoming requests to an agent.
"The HTTPS Connector" explains how to use the security tools of the Java platform to implement cryptography on the data between agents and managers.
The simplest form of agent security is to accept management requests only if they contain a valid login identity and password. Agents recognize a given a list of login-password pairs, and managers must provide a matching login and password when they try to establish a connection.
In the Java Dynamic Management Kit, only the HTTP-based connectors support password-based authentication. The SNMP protocol adaptor also supports access control, but it is based on a different mechanism (see "Access Control Lists (ACL)").
By default, no authentication is enabled in the HTTP-based connectors, and any manager may establish a connection. The password checking behavior is enabled by defining the list of authorized login-password pairs.
You may define this authentication information either:
Through the constructor which takes an AuthInfo array parameter:HttpConnectorServer(int port, AuthInfo[] authInfoList)
Through the methods inherited from the GenericHttpConnectorServer class:addUserAuthenticationInfo(AuthInfo authinfo)removeUserAuthenticationInfo(AuthInfo authinfo)
In both cases, only the agent application has access to these methods, meaning that the agent controls the authentication mechanism. As soon as an AuthInfo object is added to the connector server through either method, all incoming requests must provide a recognized name and password. In our example, we read the authentication information from the command line and call the addUserAuthenticationInfo.
// Here we show the code for reading the // id-password pairs from the command line // int firstarg = 0; boolean doAuthentication = (args.length > firstarg); AuthInfo[] authInfoList; if (doAuthentication) { authInfoList = new AuthInfo[(args.length - firstarg) / 2]; for (int i = firstarg, j = 0; i < args.length; i += 2, j++) authInfoList[j] = new AuthInfo(args[i], args[i + 1]); } else authInfoList = null; [...] // instantiate and register an HTTP connector server // Define the authentication list // if (doAuthentication) { for (int i = 0; i < authInfoList.length; i++) http.addUserAuthenticationInfo(authInfoList[i]); } |
On the manager-side, identifiers and passwords are given in the address object, since authentication applies when the connection is established.
// login and password were read from the command line // AuthInfo authInfo = null; if (login != null) { authInfo = new AuthInfo( login, password ); } // agentHost and agentPort are read from the command // line or take on default values // HttpConnectorAddress addr = new HttpConnectorAddress( agentHost, agentPort, authInfo ); final RemoteMBeanServer connector = (RemoteMBeanServer) new HttpConnectorClient(); connector.connect( addr ); |
The connector is identified by the one AuthInfo object it uses to instantiate the connector address. If the agent has authentication enabled, both the login and the password much match one of the AuthInfo objects in the agent. If the agent does not perform authentication, providing a login and password has no effect because all connections are accepted.
If the authentication fails, the call to the connect method will return an exception. Normally, the client's code should catch this exception to handle this error case.
As demonstrated by the code examples, the authentication mechanism is very simple to configure. It prevents unauthorized access with very little overhead.
The HTML adaptor provides a similar authentication mechanism, where the list of accepted identities is given to the server object. In the case of the HTML protocol, the web browser is the management application which must provide a login and password. The behavior is browser-dependent, but the browser will usually ask to user to type this login and password in a dialog box.
The examplesDir/Context directory contains the applications which demonstrate the use of password authentication through the HTTP connector.
Compile all files in this directory with the javac command. For example, on the Solaris platform with the Korn shell, you would type:
$ cd examplesDir/Context/ $ javac -classpath classpath *.java |
Launch the agent in a terminal window, and specify a list of login-password pairs, as in the following command:
$ java -classpath classpath ContextAgent jack jill billy bob |
Wait for the agent to be completely initialized, then launch the manager in another window with the following command:
$ java -classpath classpath ContextClient -ident andy bob |
The client application will try to establish a connection with the login andy and the password bob. The authentication mechanism will refuse the connection, an you will see the com.sun.jdmk.comm.UnauthorizedSecurityException raised by the connector server.
Launch the manager again, this time with a valid identity:
$ java -classpath classpath ContextClient -ident jack jill |
The connection is established and you see the output from management operation in both windows.
Leave both applications running for the next example.
Context checking is a more advanced security mechanism that can perform selective filtering of incoming requests. The context is an arbitrary object provided by the client and used by the server to decide whether or not to allow the request.
Filtering and context checking are performed in between the communicator server and the MBean server. The mechanism relies on two objects called the MBeanServerForwarder and the MBeanServerChecker.
The MBeanServerForwarder allows the principle of stackable MBean servers. An MBeanServerForwarder implements the MBeanServer interface and one extra method called setMBeanServer. Its function is to receive requests and forward them to the designated MBean server.
The setMBeanServer method of a communicator server object allows you to specify the MBean server which fulfills its requests. By chaining one or more MBeanServerForwarder objects between a communicator server and the actual MBean server, the agent application creates a stack of objects which may process the requests before they reach the MBean server.
The MBeanServerChecker is an extension of the forwarder which forces each request to call a -checker method. By extending the MBeanServerChecker class and providing an implementation of the checker methods, you can define a policy for filtering requests before they reach the MBean server. As shown in the following table, checker methods apply to groups of MBeanServer methods.
Table 11-1 Filter Method Granularity for Context CheckingFilter Method | MBean Server Operations Filtered |
---|---|
checkAny |
Every method of the MBeanServer interface |
checkCreate |
All forms of the create and registerMBean methods |
checkDelete |
The unregisterMBean method |
checkInstantiate |
All forms of the instantiate method |
checkInvoke |
The invoke method which handles all operation invocations |
checkNotification |
Both addNotificationListener and removeNotificationListener |
checkQuery |
Both queryMBeans and queryNames |
checkRead |
All methods which access but do not change the state of the agent: getAttribute, getAttributes, getObjectInstance, isRegistered, getMBeanCount, getDefaultDomain, getMBeanInfo, and isInstanceOf |
checkWrite |
The setAttribute and setAttributes methods |
As a request passes through a stack of MBean servers, the checker methods are called to determine if the request is allowed. In order to identify the manager that issued a request, the checker may access the operation context of the request.
The operation context, or just context, is an object defined by the manager who seeks access through a context checker. It usually contains some description of the manager's identity. The only restriction on the context object is that it must implement the OperationContext interface. The context object is passed from the connector client to the connector server and is then associated with the execution of a request. Conceptually, this object is stored in the user accessible context of the thread which executes the request.
All methods in the MBeanServerChecker class may access the context object by calling the protected getOperationContext method. The methods of the context checker then implement some policy to filter requests based on the context object, the nature of the request, and the data provided in the request, such as the attribute or operation name.
The following diagram shows the paths of two requests through a stack of MBean server implementations, one of which is stopped by the context checker because it doesn't provide the correct context.
Only connectors fully support the context mechanism. Their connector clients expose the methods that allow the manager to specify the context object. Existing protocol adaptors have no way to specify a context. Their requests may be filtered and checked, but their context object will always be null.
This functionality may still be used to implement a filtering policy, but without a context object, straightforward manager identification is not possible. However, a proprietary protocol adaptor could define some mapping to determine a context object that could be accepted by the filters.
An agent wanting to implement context checking first needs to extend the MBeanServerChecker class. This class retrieves the context object and decides whether any given operation is allowed.
import javax.management.MBeanServer; import javax.management.ObjectName; import javax.management.QueryExp; import com.sun.jdmk.MBeanServerChecker; import com.sun.jdmk.OperationContext; public class ContextChecker extends MBeanServerChecker { // Constructor public ContextChecker(MBeanServer mbs) { super(mbs); } // Implementation of the abstract methods of the // MBeanServerChecker class: for each of the specific // checks, we just print out a trace of being called. [...] protected void checkWrite( String methodName, ObjectName objectName) { System.out.println("checkWrite(\"" + methodName + "\", " + objectName + ")"); } protected void checkQuery( String methodName, ObjectName name, QueryExp query) { System.out.println("checkQuery(\"" + methodName + "\", " + name + ", " + query + ")"); } [...] /** * This is where we implement the check that requires every * operation to be called with an OperationContext whose * toString() method returns the string "nice". */ protected void checkAny( String methodName, ObjectName objectName ) { System.out.println("checkAny(\"" + methodName + "\", " + objectName); OperationContext context = getOperationContext(); System.out.println(" OperationContext: " + context); if (context == null || !context.toString().equals("nice")) { RuntimeException ex = new SecurityException(" Bad context: " + context); ex.printStackTrace(); throw ex; } } } |
Then the agent application then needs to instantiate its context checker and stack them in between the communicator servers and the MBean server. Each communicator server would have its own stack, although filters and context checkers may be shared. The agent performs the stacking inside a synchronized block because other threads may try to do stacking simultaneously.
// Create MBeanServer // MBeanServer server = MBeanServerFactory.createMBeanServer(); /* Create context checker. The argument to the constructor is * our MBean server to which all requests will be forwarded */ ContextChecker contextChecker = new ContextChecker( server ); [...] // Create HTTP connector server /* Add the context checker to this HTTP connector server. * We point it at the context checker which already points * to the actual MBean server. * It is good policy to check that we are not sidetracking * an existing stack of MBean servers before setting ours. */ synchronized (http) { if (http.getMBeanServer() != server) { System.err.println("After registering connector MBean, " + "http.getMBeanServer() != " + "our MBeanServer"); System.exit(1); } http.setMBeanServer(contextChecker); } |
Finally, the manager operation defines a context object class and then provides a context object instance through its connector client.
/* In this example, the agent checks the OperationContext of each operation by examining its toString() method, so we define a simple implementation of OperationContext whose toString() is a constant string supplied in the constructor */ class StringOperationContext implements OperationContext, Cloneable { private String s; StringOperationContext(String s) { this.s = s; } public String toString() { return s; } public Object clone() throws CloneNotSupportedException { return super.clone(); } } // the contextName must be provided on the command line OperationContext context = new StringOperationContext(contextName); [...] // The context is set for all requests issued through // the connector client; it may be changed at any time connector.setOperationContext(context); |
The ContextClient and ContextAgent applications in he examplesDir/Context directory also demonstrate the use of stackable MBean servers and context checking through the HTTP connector.
If you have not done so already, compile all files in this directory with the javac command. For example, on the Solaris platform with the Korn shell, you would type:
$ cd examplesDir/Context/ $ javac -classpath classpath *.java |
If the agent and client applications are not already running from the previous example, type the following commands in separate windows:
$ java -classpath classpath ContextAgent |
$ java -classpath classpath ContextClient |
The classpath should include the current directory (.) for both applications because they rely on classes that were compiled in this directory.
Press <Enter> in the client application to trigger another set of requests.
The agent window displays the output of the ContextChecker class. We can see that the checkAny method verifies the "nice" context of every request and that the other checkers just print out their name, providing a trace of the request.
Stop both applications by typing <Control-C> in each of the windows.Restart both applications, but specify a different context string for the client:
$ java -classpath classpath ContextAgent |
$ java -classpath classpath ContextClient -context BadToTheBone |
This time we see the result of a context that is not recognized. The agent raises a java.lang.SecurityException which is propagated to the client who then exits.
Press <Control-C> in the agent window to stop the ContextAgent application.
The HTTPS connector provides data encryption and certificate-based security through a Secure Socket Layer (SSL). An implementation of secure sockets is only available for the Java 2 platform. However, secure sockets are not part of the Java 2 SDK (Software Development Kit), and their libraries must be installed separately.
The Java Secure Socket Extension (JSSE) 1.0 provides a compatible implementation of secure sockets for the Java 2 platform. For optimal performance of the HTTPS connector, it is recommended that you use the Java 2 SDK, Standard Edition, v1.2.2, and the JSSE 1.01.
The web site for the JSSE is http://java.sun.com/products/jsse. This site provides links for downloading the software and the documentation. For further information and details regarding the use of the secure sockets, please refer to the JSSE documentation.
The HTTPS connector exposes the same interfaces as all other connectors and has exactly the same behavior. The development of management application which relies on the HTTPS connector is no different from that of any other Java Dynamic Management manager. See "Connector Clients" for details about programming with the RemoteMBeanServer API.
Where the HTTPS connector differs is that it relies on the security mechanisms built into the Java language and extended by JSSE. In order to use these libraries and communicate securely, you must configure your application environment to meet all security requirement. The cost of security is establishing all of the structures that guarantee the trust between two communicating parties.
This section covers the steps that are required to establish a secure connection between your agent and manager applications. These instructions do not guarantee total security, they just explain the programmatic steps needed to ensure data security between two distant Java applications.
These steps assume that each of your manager and agent applications runs on a separate machine, and that each machine has its own installation of the Java SDK (not a shared network installation).
You should install both the Java 2 SDK, Standard Edition, v1.2.2, and the JSSE 1.01 products on all hosts that will use the HTTPS connector client or connector server components.
In this procedure, the directories where you have installed these products are named JAVAhome and JSSEhome, respectively. These names are used on all hosts where the products are installed, even though their value is specific to each host.
For each of your SDK/JSSE installations, copy the three jar files (jsse.jar, jcert.jar, and jnet.jar) of the JSSE reference implementation into the extensions directory of your Java runtime environment.
For example, on the Solaris platform you would type:
$ cp JSSEhome/lib/jsse.jar JAVAhome/jre/lib/ext/ $ cp JSSEhome/lib/jcert.jar JAVAhome/jre/lib/ext/ $ cp JSSEhome/lib/jnet.jar JAVAhome/jre/lib/ext/ |
Or you could add these jar files to your environment's classpath, as follows (for the Korn shell):
$ export CLASSPATH=${CLASSPATH}:JSSEhome/lib/jsse.jar:\ JSSEhome/lib/jcert.jar:JSSEhome/lib/jnet.jar |
The JSSE follows the same "provider" architecture found in the Java Cryptography Architecture (JCA) which is provided in the Java 2 platform as the Java Cryptography Extension (JCE). In order to use JSSE you must install this provider either statically or dynamically. Again, you must do this for all of your SDK/JSSE installations.
To install the provider statically you must edit the security properties file (JAVAhome/lib/security/java.security). Edit this file as follows, the boldface text is the part you must add:
security.provider.1=sun.security.provider.Sun security.provider.2=com.sun.net.ssl.internal.ssl.Provider
The first line of this file depends upon your SDK platform and should not be changed.
To install the provider dynamically in your Java application, you should call the addProvider method of the java.security.Security class. The line in your source code would look like this:
Security.addProvider( new com.sun.net.ssl.internal.ssl.Provider() );
This step must be repeated on all agent and manager host machines.
Generate a key pair (a public key and associated private key). Wrap the public key into an X.509 v1 self-signed certificate, which is stored as a single-element certificate chain. This certificate chain and the private key are stored in a new keystore entry identified by alias.
In the following command, the -dname parameters designates the X.500 Distinguished Name for the host where you are generating the certificates. The commonName field must be the machine's hostname.
$ keytool -genkey -alias alias -keyalg RSA -keysize 1024 -sigalg MD5withRSA -dname "CN=commonName, OU=orgUnit, O=org, L=location, S=state, C=country" -keypass passPhrase -storetype jks -keystore yourHome/.keystore -storepass passPhrase |
This step must be repeated on all agent and manager host machines.
Read the certificate that is associated with your alias from the keystore, and store it in a hostCertFile:
$ keytool -export -alias alias -file hostCertFile -storetype jks -keystore yourHome/.keystore -storepass passPhrase -rfc |
When you are done with this step you will have a certificate for each of your host machines.
This step must be repeated on both the agent and manager host machines, for all pairs of agent-managers in your management architecture.
In this step, agent and manager pairs must exchange their certificates. The manager should import the agent's hostCertFile and the agent should import the manager's hostCertFile. If a manager has two agents, it will import two certificates, and each agent will import a copy of the manager's certificate.
Import the certificate into the file containing the trusted Certificate Authorities (CA) certificates. This will add our self-signed certificate as a trusted CA certificate to the cacerts file so that the server and the client will be able to authenticate each other.
$ keytool -import -alias alias -file hostCertFile -noprompt -trustcacerts -storetype jks -keystore JAVAhome/jre/lib/security/cacerts -storepass changeit |
This command modifies the JAVAhome/jre/lib/security/cacerts which will affect all applications running on that installation. If you do not want to modify this file, you could create a file named jssecacerts and use it instead. The default location of this file is either JAVAhome/lib/security/jssecacerts or if that does not exist, then JAVAhome/lib/security/cacerts.
Launch your agent applications with the following properties:
$ java -Djavax.net.ssl.keyStore=yourHome/.keystore -Djavax.net.ssl.keyStoreType=jks -Djavax.net.ssl.keyStorePassword=passPhrase AgentClass |
If you are using the notification push mechanism, add the following property definition to the above command line:
-Djava.protocol.handler.pkgs=com.sun.net.ssl.internal.www.protocol
Launch your management applications with the following properties:
$ java -Djavax.net.ssl.keyStore=yourHome/.keystore -Djavax.net.ssl.keyStoreType=jks -Djavax.net.ssl.keyStorePassword=passPhrase -Djava.protocol.handler.pkgs=com.sun.net.ssl.internal.www.protocol ManagerClass |