Thursday, 29 August 2013

Changing application behavior at runtime with JMX

Sometimes we need to be able to change the behavior of our application without a restart.
JMX, apart from its monitoring capabilities, is a perfect solution for this.
Spring provides great JMX that will ease our task.

Let's start with a simple service, which behavior we will change at runtime.

public class DiscountService {
private AtomicInteger globalDiscount = new AtomicInteger(12);
public int calculateDiscount() {
return globalDiscount.get() * 2;
}
}
DiscountService calculates discount based on a globalDiscount - it's value is harcoded for simplicity purposes, it would probably be read from some configuration file or database in more realistic example.

First of all, in order to expose methods to manage globalDiscount we need to add @ManagedResource annotation to our class and add the methods with @ManagedOperation annotation.
We could also use @ManagedAttribute if we would treat these methods as simple getter and setter for globalDiscount.

Class with needed methods and annotations would look like:

@ManagedResource
public class DiscountService {
private AtomicInteger globalDiscount = new AtomicInteger(12);
public int calculateDiscount() {
return globalDiscount.get() * 2;
}
@ManagedOperation
public int checkGlobalDiscount() {
return globalDiscount.get();
}
@ManagedOperation
public void modifyGlobalDiscount(int newDiscount) {
globalDiscount.set(newDiscount);
}
}

In Spring configuration we just need to define the bean for DiscountService and enabling exporting MBeans with MBean server.

@Configuration
@EnableMBeanExport
@ComponentScan("pl.mjedynak")
public class AppConfig {
@Bean
public DiscountService discountService() {
return new DiscountService();
}
@Bean
public MBeanServerFactoryBean mbeanServer() {
return new MBeanServerFactoryBean();
}
}
view raw AppConfig.java hosted with ❤ by GitHub

We can run the application with:

public class App {
public static void main(String[] args) throws IOException {
new AnnotationConfigApplicationContext(AppConfig.class);
System.in.read();
}
}
view raw App.java hosted with ❤ by GitHub

Now we're ready to manage our service with jconsole:


Our service is exposed locally, but if we want to be able to connect to it remotely, we will need to add following beans to the Spring configuration:

@Bean
public RmiRegistryFactoryBean registry() {
return new RmiRegistryFactoryBean();
}
@Bean
@DependsOn("registry")
public ConnectorServerFactoryBean connectorServer() throws MalformedObjectNameException {
ConnectorServerFactoryBean connectorServerFactoryBean = new ConnectorServerFactoryBean();
connectorServerFactoryBean.setObjectName("connector:name=rmi");
connectorServerFactoryBean.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/connector");
return connectorServerFactoryBean;
}
view raw beans.java hosted with ❤ by GitHub

Service is now exposed via RMI.
We can invoke the exposed methods programmatically, which allows us to write some scripts and manage services without using jconsole.

Let's write an integration test to check that it works correctly.
We will need a Spring config for the test with the RMI client.

@Configuration
public class AppJmxIntegrationTestConfig {
@Bean
public MBeanServerConnectionFactoryBean clientConnector() throws MalformedURLException {
MBeanServerConnectionFactoryBean mBeanServerConnectionFactoryBean = new MBeanServerConnectionFactoryBean();
mBeanServerConnectionFactoryBean.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/connector");
return mBeanServerConnectionFactoryBean;
}
}

The test will increment the value of globalDiscount.
Take a closer look at exposed methods invocation, which is very cumbersome, especially if the method has parameters.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AppJmxIntegrationTestConfig.class})
public class AppJmxIntegrationTest {
private static final String CHECK_GLOBAL_DISCOUNT_METHOD_NAME = "checkGlobalDiscount";
private static final String MODIFY_GLOBAL_DISCOUNT_METHOD_NAME = "modifyGlobalDiscount";
@Autowired
private MBeanServerConnectionFactoryBean clientConnector;
@Test
public void shouldInvokeOperations() throws Exception {
// given
MBeanServerConnection connection = clientConnector.getObject();
ObjectName objectName = new ObjectName("pl.mjedynak:name=discountService,type=DiscountService");
// when
Integer oldDiscount = (Integer) connection.invoke(objectName, CHECK_GLOBAL_DISCOUNT_METHOD_NAME, null, null);
Integer newDiscount = ++oldDiscount;
connection.invoke(objectName, MODIFY_GLOBAL_DISCOUNT_METHOD_NAME, new Object[]{newDiscount}, new String[]{int.class.getName()});
Integer currentDiscount = (Integer) connection.invoke(objectName, CHECK_GLOBAL_DISCOUNT_METHOD_NAME, null, null);
// then
assertThat(currentDiscount, is(newDiscount));
}
}

The whole project can be found at github.