Testing JMX between two Web Applications using Maven

The problem: you have two web applications and each is developed inside a separate Maven module. You need to communicate from one web application to the other and you don't want to implement a service but use JMX instead. This is a scenario we are having here at the moment. The first web application (application A) contains the game server logic of our new game. The second web application (application B) contains a debug tool which we will not deploy into production. I have selected JMX for the communication, mainly because I didn't wanted to add another technology and we are already using JMX in the first application. Both web application are Spring powered.

First here is a nice Spring feature, which completely hides the JMX complexity for the client application behind a proxy.


<bean id="clientConnector" class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean">
<property name="serviceUrl" value="[SERVICE_URL]"/>
</bean>

<bean id="gameplayRecordable" class="org.springframework.jmx.access.MBeanProxyFactoryBean">
<property name="objectName" value="[MBEAN]" />
<property name="proxyInterface" value="any.java.Interface" />
<property name="server" ref="clientConnector" />
</bean>


First you define a client connector which connects you to the RMI server port of the other web application. Then you define a MBeanProxyFactoryBean using this client connector. The objectName must be the name of your MBean inside the MBean container. If you are not sure about the name, use jconsole to connect to the process of the first web application and look it up. Another important property of the MBeanProxyFactoryBean is the proxyInterface. This is an interface that the proxy will implement. The proxy will try to map each method call on that interface in application B to a JMX call in application A. I can really recommend to share the same Interface in both applications as it makes stuff really simple.

This was simple so far. Now lets say you want to write an integration test to automatically test the whole shebang. The test should start up a JMX enabled Jetty from Maven. This Jetty instance should explode the war file of application A (hosting the MBean you want to invoke). Once Jetty is up, the test should execute, connect to application A via the MBeanProxyFactoryBean and validate the results. First lets enable remote JMX access in the configuration of the maven-jetty-plugin:


<profiles>
<profile>
<id>itest</id>
<build>
<plugins>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>${version.jetty.plugin}</version>
<configuration>
<stopKey>stop_key</stopKey>
<stopPort>9999</stopPort>
<contextPath>/</contextPath>
<webApp>
${settings.localRepository}/com/package/../../your.war
</webApp>
<jettyConfig>
${basedir}/src/test/etc/jetty-jmx.xml</jettyConfig>
</configuration>
<executions>
<execution>
<id>start-jetty</id>
<phase>pre-integration-test</phase>
<goals>
<goal>deploy-war</goal>
</goals>
<configuration>
<daemon>true</daemon>
</configuration>
</execution>
<execution>
<id>stop-jetty</id>
<phase>prepare-package</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>


As you can see, this plugin configuration is done in a Maven profile as we are defining this for application B which also has its own Jetty configuration. The important piece is the jettyConfig element which points to a jetty-jmx.xml file. To get this file, download the Jetty container that has the same version as your maven-jetty-plugin. For instance is you use version 6.1.26 of the maven-jetty-plugin, make sure you download jetty-6.1.26 from the codehaus download page. If you are using the the new jetty-maven-plugin and Jetty 7 or 8, you need to download the Jetty container from Eclipse. The configuration is the same for the maven-jetty-plugin and the jetty-maven-plugin. Just make sure you download the jetty-jmx.xml file from the right Jetty container as they are different. You don't need to specify any additional system properties like -Dcom.sun.management.jmxremote, -Dcom.sun.management.jmxremote.ssl, -Dcom.sun.management.jmxremote.authenticate or -Dcom.sun.management.jmxremote.port.

Once the jetty-jmx.xml is downloaded, put in somewhere inside your Maven module where it does not get packaged into the module artifact. In the example above you can see that we have the jetty-jmx.xml file in src/test/etc but any other location will do. Open the file and enable remote JMX access via RMI. In the Jetty 6 based jetty-jmx.xml file these elements should be commented in:


<Call id="rmiRegistry" class="java.rmi.registry.LocateRegistry" name="createRegistry">
<Arg type="int">2099</Arg>
</Call>

<Call id="jmxConnectorServer" class="javax.management.remote.JMXConnectorServerFactory"
name="newJMXConnectorServer">
<Arg>
<New class="javax.management.remote.JMXServiceURL">
<Arg>
service:jmx:rmi://localhost:17264/jndi/rmi://localhost:2099/jmxrmi
</Arg>
</New>
</Arg>
<Arg/>
<Arg>
<Ref id="MBeanServer"/>
</Arg>
<Call name="start"/>
</Call>


Note that we changed the port to 17264. You might want to use the default port instead. In the Jetty 7 based jetty-jmx.xml file these elements should be commented in:


<Call name="createRegistry" class="java.rmi.registry.LocateRegistry">
<Arg type="java.lang.Integer">1099</Arg>
<Call name="sleep" class="java.lang.Thread">
<Arg type="java.lang.Integer">1000</Arg>
</Call>
</Call>

<New id="ConnectorServer" class="org.eclipse.jetty.jmx.ConnectorServer">
<Arg>
<New class="javax.management.remote.JMXServiceURL">
<Arg type="java.lang.String">rmi</Arg>
<Arg type="java.lang.String" />
<Arg type="java.lang.Integer">0</Arg>
<Arg type="java.lang.String">/jndi/rmi://localhost:1099/jettyjmx</Arg>
</New>
</Arg>
<Arg>org.eclipse.jetty:name=rmiconnectorserver</Arg>
<Call name="start" />
</New>


To test the setup, run mvn -Pitest jetty:run and start jconsole. In jconsole you do not connect to a local process. Select Remote Process and enter the service URL. This URL can be copied from the jetty-jmx.xml file if you are using Jetty 6 (i.e. service:jmx:rmi://localhost:17264/jndi/rmi://localhost:2099/jmxrmi). If you are using Jetty 7 and the jetty-maven-plugin, there will be a info statement on the command line when Maven starts the Jetty container from where you can copy the service URL. Finally to execute the integration test, we use the maven-failsafe-plugin like this:


<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.9</version>
<configuration>
<includes>
<include>**/com/package/integration/*.java</include>
</includes>
</configuration>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
<execution>
<id>verify</id> to specify any additional system properties like -Dcom.sun.management.jmxremote, -Dcom.sun.management.jmxremote.ssl, -Dcom.sun.management.jmxremote.authenticate or -Dcom.sun.management.jmxremote.port.

Once the jetty-jmx.xml is downloaded, put in somewhere inside your Maven module where it does not get packaged into the module artifact. In the example above you can see that we have the jetty-jmx.xml file in src/test/etc but any other location will do. Open the file and enable remote JMX access via RMI. In the Jetty 6 based jetty-jmx.xml file these elements should be commented in:


<Call id="rmiRegistry" class="java.rmi.registry.LocateRegistry" name="createRegistry">
<Arg type="int">2099</Arg>
</Call>

<Call id="jmxConnectorServer" class="javax.management.remote.JMXConnectorServerFactory"
name="newJMXConnectorServer">
<Arg>
<New class="javax.management.remote.JMXServiceURL">
<Arg>service:jmx:rmi://localhost:17264/jndi/rmi://localhost:2099/jmxrmi</Arg>
</New>
</Arg>
<Arg/>
<Arg>
<Ref id="MBeanServer"/>
</Arg>
<Call name="start"/>
</Call>


Note that we changed the port to 17264. You might want to use the default port instead. In the Jetty 7 based jetty-jmx.xml file these elements should be commented in:


<Call name="createRegistry" class="java.rmi.registry.LocateRegistry">
<Arg type="java.lang.Integer">1099</Arg>
<Call name="sleep" class="java.lang.Thread">
<Arg type="java.lang.Integer">1000</Arg>
</Call>
</Call>

<New id="ConnectorServer" class="org.eclipse.jetty.jmx.ConnectorServer">
<Arg>
<New class="javax.management.remote.JMXServiceURL">
<Arg type="java.lang.String">rmi</Arg>
<Arg type="java.lang.String" />
<Arg type="java.lang.Integer">0</Arg>
<Arg type="java.lang.String">/jndi/rmi://localhost:1099/jettyjmx</Arg>
</New>
</Arg>
<Arg>org.eclipse.jetty:name=rmiconnectorserver</Arg>
<Call name="start" />
</New>


To test the setup, run mvn -Pitest jetty:run and start jconsole. In jconsole you do not connect to a local process. Select Remote Process and enter the service URL. This URL can be copied from the jetty-jmx.xml file if you are using Jetty 6 (i.e. service:jmx:rmi://localhost:17264/jndi/rmi://localhost:2099/jmxrmi). If you are using Jetty 7 and the jetty-maven-plugin, there will be a info statement on the command line when Maven starts the Jetty container from where you can copy the service URL. Finally to execute the integration test, we use the maven-failsafe-plugin like this:


<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.9</version>
<configuration>
<includes>
<include>**/com/package/integration/*.java</include>
</includes>
</configuration>
<executions>
<execution>
<id>integration-test</id>
<goals>
<goal>integration-test</goal>
</goals>
</execution>
<execution>
<id>verify</id>
<goals>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>

<goals>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>