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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class DiscountService { | |
private AtomicInteger globalDiscount = new AtomicInteger(12); | |
public int calculateDiscount() { | |
return globalDiscount.get() * 2; | |
} | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Configuration | |
@EnableMBeanExport | |
@ComponentScan("pl.mjedynak") | |
public class AppConfig { | |
@Bean | |
public DiscountService discountService() { | |
return new DiscountService(); | |
} | |
@Bean | |
public MBeanServerFactoryBean mbeanServer() { | |
return new MBeanServerFactoryBean(); | |
} | |
} |
We can run the application with:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class App { | |
public static void main(String[] args) throws IOException { | |
new AnnotationConfigApplicationContext(AppConfig.class); | |
System.in.read(); | |
} | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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; | |
} |
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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.