Often it might be necessary to expose a Plain Old Java Object as a JMX MBean for monitoring and management. Exposing a Plain Old Java Object as a JMX MBean is quite easy using the Spring framework's support. In this tutorial, we provide examples on how to do this, along with some tips and pitfalls to avoid. Check out the tutorial at the following URL:

​JavaCodeGeeks: Exposing a POJO as a JMX MBean with Spring​

I needed to monitor a command-line java application using Spring 2.5 on an IBM JVM 1.5 running on a server. Monitoring would be performed with a jconsole on Sun JVM 1.6 as the JMX client on my PC. All the following XML snippets are from the corresponding Spring application-context.xml.


Turning a POJO into an MBean


JMX makes it possible to expose getters, setters and operations taking primitives or ​​complex data types​​ as parameters (though types other than a few special ones require the client to have the classes). You tell Spring to expose a POJO as an MBean as follows:



​01​

​<​​​​bean​​ ​​id​​​​=​​​​"myMBean"​


​02​

​class​​​​=​​​​"my.package.JobPerformanceStats"​


​03​

​factory-method​​​​=​​​​"instance"​​ ​​/> ​


​04​

 


​05​

​<​​​​bean​​ ​​class​​​​=​​​​"org.springframework.jmx.export.MBeanExporter"​​ ​​lazy-init​​​​=​​​​"false"​​​​> ​


​06​

​<​​​​property​​ ​​name​​​​=​​​​"beans"​​​​> ​


​07​

​<​​​​map​​​​> ​


​08​

​<​​​​entry​​ ​​key​​​​=​​​​"bean:name=MyMBeanName"​​ ​​value-ref​​​​=​​​​"myMBean"​​​​/> ​


​09​

​</​​​​map​​​​> ​


​10​

​</​​​​property​​​​> ​


​11​

​</​​​​bean​​​​>​


First you declare an instance of the POJO class – the myMBean (for other reasons I’ve an old-fashioned singleton and use JobPerformanceStats.instance() to access the bean). Next you declare an ​​MBeanExporter​​ with lazy-init=”false” and tell it about your bean. (There are also other ways to do it, including auto-discovery.) The bean will be then visible under its key, i.e. “bean:name=MyMBeanName”, which JConsole displays as “MyMBeanName”.


Notice that the MBeanExporter only works under JVM 1.5+ because it uses the new java.lang.management package. Under JDK 1.4, Spring would fail with the following errror:


java.lang.NoClassDefFoundError: javax/management/MBeanServerFactory

at org.springframework.jmx.support.MBeanServerFactoryBean.createMBeanServer


By default it will expose all public methods and attributes. You can change that in various ways, for example with the help of an interface.


If you are not running in a container that already provides an MBean server, which is my case here, you must tell Spring to start one:




​view source​


​print​​​​?​


​1​

​<​​​​bean​​ ​​class​​​​=​​​​"org.springframework.jmx.support.MBeanServerFactoryBean"​​​​/>​


Enabling remote access


To make the MBean accessible from another machine, you must expose it to the world by declaring a ConnectorServerFactoryBean configured with an appropriate communication mechanism.


Remote access over JMXMP


By default the ​​ConnectorServerFactoryBean​​ exposes MBeans over the JMX Messaging Protocol (JMXMP) with the address


service:jmx:jmxmp://localhost:9875


​1​

​<​​​​bean​​ ​​class​​​​=​​​​"org.springframework.jmx.support.ConnectorServerFactoryBean"​​ ​​/>​


However this protocol ​​isn’t supported out of the box​​ and you must include jmxremote_optional.jar, a part of ​​OpenDMK​​, on the classpath of both the MBean application and the jconsole client to avoid the following exception:


org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘org.springframework.jmx.support.ConnectorServerFactoryBean#0? defined in class path resource [application-context.xml]: Invocation of init method failed; nested exception is java.net.MalformedURLException: Unsupported protocol: jmxmp


Remote access over RMI


Alternatively you can expose the MBeans over RMI, which has no additional dependencies:




​view source​


​print​​​​?​


​01​

​<!-- ​


​02​

​Now expose the server for remote access via RMI ​


​03​

​Local access:   service:jmx:rmi://localhost/jndi/rmi://localhost:10099/myconnector ​


​04​

​Remote access:  service:jmx:rmi:///jndi/rmi://your.host:10099/myconnector ​


​05​

​or service:jmx:rmi://localhost/jndi/rmi://localhost:10099/myconnector ​


​06​

​-->​


​07​

​<​​​​bean​


​08​

​class​​​​=​​​​"org.springframework.jmx.support.ConnectorServerFactoryBean"​


​09​

​depends-on​​​​=​​​​"rmiRegistry"​​​​> ​


​10​

​<​​​​property​​ ​​name​​​​=​​​​"objectName"​​ ​​value​​​​=​​​​"connector:name=rmi"​​ ​​/> ​


​11​

​<​​​​property​​ ​​name​​​​=​​​​"serviceUrl"​


​12​

​value​​​​=​​​​"service:jmx:rmi://localhost/jndi/rmi://localhost:10099/myconnector"​​ ​​/> ​


​13​

​</​​​​bean​​​​> ​


​14​

 


​15​

​<​​​​bean​​ ​​id​​​​=​​​​"rmiRegistry"​


​16​

​class​​​​=​​​​"org.springframework.remoting.rmi.RmiRegistryFactoryBean"​​​​> ​


​17​

​<​​​​property​​ ​​name​​​​=​​​​"port"​​ ​​value​​​​=​​​​"10099"​​ ​​/> ​


​18​

​</​​​​bean​​​​>​


However there are also some catches you must avoid:


1. You must start an RMI registry so that the connector can register the MBean there; it won’t start one for you

2. You must make sure that the registry is started before the connector tries to use either by declaring it before the connector or by making this dependency explicit with the depends-on attribute


If you don’t set it up correctly you will get an exception like this one:


org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘org.springframework.jmx.support.ConnectorServerFactoryBean#0′ defined in class path resource [application-context.xml]: Invocation of init method failed; nested exception is java.io.IOException: Cannot bind to URL [rmi://localhost:10099/jmxrmi]: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: localhost; nested exception is: java.net.ConnectException: Connection refused: connect].


Local MBean server accessed over an SSH tunnel


For increased security you may want to prefer not to expose your MBeans to remote access by making them accessible only from the local machine (127.0.0.1) and use na SSH tunnel so that a remote JConsole can access them as a local application. This is certainly possible but may be difficult because normally JMX goes over RMI, which ​​uses two ports​​: one for the RMI Registry and another one for the actual service (MBean server here), which is usually chosen randomly at the runtime, and you’d need to tunnel both. Fortunately, ​​Spring makes it possible to configure both the ports​​:


​01​

​<​​​​bean​


​02​

​class​​​​=​​​​"org.springframework.jmx.support.ConnectorServerFactoryBean"​


​03​

​depends-on​​​​=​​​​"rmiRegistry"​​​​> ​


​04​

​<​​​​property​​ ​​name​​​​=​​​​"objectName"​​ ​​value​​​​=​​​​"connector:name=rmi"​​ ​​/> ​


​05​

​<​​​​property​​ ​​name​​​​=​​​​"serviceUrl"​


​06​

​value​​​​=​​​​"service:jmx:rmi://127.0.0.1:STUBPORT/jndi/rmi://localhost:REGISTRYPORT/myconnector"​​ ​​/> ​


​07​

​</​​​​bean​​​​> ​


​08​

 


​09​

​<​​​​bean​​ ​​id​​​​=​​​​"rmiRegistry"​


​10​

​class​​​​=​​​​"org.springframework.remoting.rmi.RmiRegistryFactoryBean"​​​​> ​


​11​

​<​​​​property​​ ​​name​​​​=​​​​"port"​​ ​​value​​​​=​​​​"REGISTRYPORT"​​ ​​/> ​


​12​

​</​​​​bean​​​​>​


Replace STUBPORT and REGISTRYPORT with suitable numbers and tunnel those two. Notice that the REGISTRYPORT number is same in the connector’s serviceUrl and in the RMI registry’s port attribute.

WARNING: The configuration above actually doesn’t prevent direct access from a remote application. To really force the RMI registry to only listen for connection from the local host we would likely need to set – under Sun JVM without Spring – the system property com.sun.management.jmxremote. Additionally, to force the registry to use the IP 120.0.0.1, we’d need to set java.rmi.server.hostname=localhost (applies to Spring too). See this ​​discussion about forcing local access​​. I’m not sure how to achieve the same result with Spring while still preserving the ability to specify both the RMI ports. Check also the JavaDoc for Spring’s ​​RmiServiceExporter​​.


Related posts and docs:

Connecting with Jconsole


Fire up JConsole and type the appropriate remote address, for example


service:jmx:rmi:///jndi/rmi://your.server.com:10099/myconnector


if connecting to an application on the remote machine your.server.com accessible via RMI.


Regarding the connection URL, if you have a a connector with the serviceUrl of


service:jmx:rmi://myhost:9999/jndi/rmi://localhost:10099/myconnector


then, from a client, you can use either


service:jmx:rmi://myhost:9999/jndi/rmi://your.server.com:10099/myconnector


or simply


service:jmx:rmi:///jndi/rmi://your.server.com:10099/myconnector


because, according to the JMX 1.2 Remote API specification (page 90):


… the hostname and port number

# (myhost:9999 in the examples) are not used by the client and, if

# present, are essentially comments. The connector server address

# is actually stored in the serialized stub (/stub/ form) or in the

# directory entry (/jndi/ form).


IBM JVM, JConsole and JMX configuration


The IBM JVM 5 SDK guide indicates that the ​​IBM SDK also contains JConsole​​ and recognizes ​​the same JMX-related system properties​​, namely com.sun.management.jmxremote.* (though “com.sun.management.jmxremote” itself isn’t mentioned).

Notice that the IBM JConsole is a bit different, for instance it’s missing the Local tab, which is replaced by specifying the command-line option connection=localhost (search the SDK guide for “JConsole monitoring tool Local tab”).


Further improvements


JVM 1.5: Exposing the MemoryMXBean


Since Java 5.0 there is a couple of useful platform MBeans that provide information about the JVM, including also the java.lang.management.MemoryMXBean, that let you see the heap usage, invoke GC etc.


You can make it available to JConsole and other JMX agents as follows (though there must be an easier way):


​01​

​<​​​​bean​​ ​​class​​​​=​​​​"org.springframework.jmx.export.MBeanExporter"​​ ​​lazy-init​​​​=​​​​"false"​​​​> ​


​02​

​<​​​​property​​ ​​name​​​​=​​​​"beans"​​​​> ​


​03​

​<​​​​map​​​​> ​


​04​

​<​​​​entry​​ ​​key​​​​=​​​​"bean:name=Memory2"​​ ​​value-ref​​​​=​​​​"memProxy"​​​​/> ​


​05​

​<!-- other exported beans may follow ... -->​


​06​

​</​​​​map​​​​> ​


​07​

​</​​​​property​​​​> ​


​08​

​</​​​​bean​​​​> ​


​09​

 


​10​

​<​​​​bean​​ ​​id​​​​=​​​​"memProxy"​


​11​

​class​​​​=​​​​"java.lang.management.ManagementFactory"​


​12​

​factory-method​​​​=​​​​"getMemoryMXBean"​


​13​

​/>​


Update: There indeed seems to be a ​​better way of exposing the platform MBeans directly​​ by replacing the Spring’s MBeanServerFactoryBean with java.lang.management.ManagementFactory using its factory-method getPlatformMBeanServer. Of course this requires JVM 1.5+.


Improving security with password authentication


Access to your MBeans over RMI may be protected with a password. According to a discussion, ​​authentication is configured on the server connector​​:


​01​

​<​​​​bean​


​02​

​class​​​​=​​​​"org.springframework.jmx.support.ConnectorServerFactoryBean"​


​03​

​depends-on​​​​=​​​​"rmiRegistry"​​​​> ​


​04​

​<​​​​property​​ ​​name​​​​=​​​​"objectName"​​ ​​value​​​​=​​​​"connector:name=rmi"​​ ​​/> ​


​05​

​<​​​​property​​ ​​name​​​​=​​​​"serviceUrl"​


​06​

​value​​​​=​​​​"service:jmx:rmi://localhost/jndi/rmi://localhost:10099/myconnector"​​ ​​/> ​


​07​

​<​​​​property​​ ​​name​​​​=​​​​"environment"​​​​> ​


​08​

​<!-- the following is only valid when the sun jmx implementation is used -->​


​09​

​<​​​​map​​​​> ​


​10​

​<​​​​entry​​ ​​key​​​​=​​​​"jmx.remote.x.password.file"​​ ​​value​​​​=​​​​"etc/security/jmxremote.password"​​​​/> ​


​11​

​<​​​​entry​​ ​​key​​​​=​​​​"jmx.remote.x.access.file"​​ ​​value​​​​=​​​​"etc/security/jmxremote.access"​​​​/> ​


​12​

​</​​​​map​​​​> ​


​13​

​</​​​​property​​​​> ​


​14​

​</​​​​bean​​​​>​


The passwd and access files follow the templates that can be found in the JDK/jre/lib/management folder.


Summary


Exposing a POJO as a MBean with Spring is easy, just don’t forget to start an MBean server and a connector. For JMXMP, include the jmxmp_impl. jar on the classpath and for RMI make sure to start a RMI registry before the connector.oo