Java Management Extensions (JMX) in Java

JMX is a Java technology that provides tools for managing and monitoring applications, system objects, devices, and service-oriented networks.

1. JMX Architecture Overview

// JMX Architecture Components:
// 1. Instrumentation Level (MBeans)
// 2. Agent Level (MBeanServer)
// 3. Distributed Services Level (Connectors/Adaptors)
public class JMXArchitecture {
public static void main(String[] args) {
System.out.println("JMX Architecture:");
System.out.println("1. Managed Beans (MBeans) - The resources to manage");
System.out.println("2. MBean Server - The core component that registers MBeans");
System.out.println("3. Connectors - How external clients connect (RMI, JMXMP)");
System.out.println("4. Protocol Adaptors - HTML, SNMP, etc.");
}
}

2. Standard MBeans

MBean Interface

// ServerMonitorMBean.java - MBean interface
public interface ServerMonitorMBean {
// Attributes
int getActiveSessions();
long getUptime();
String getStatus();
void setMaxSessions(int maxSessions);
int getMaxSessions();
// Operations
void start();
void stop();
void restart();
String performHealthCheck();
// Notifications
void addNotificationListener(NotificationListener listener, 
NotificationFilter filter, Object handback);
void removeNotificationListener(NotificationListener listener);
}

MBean Implementation

// ServerMonitor.java - MBean implementation
public class ServerMonitor implements ServerMonitorMBean {
private int activeSessions = 0;
private int maxSessions = 1000;
private long startTime;
private boolean running = false;
private final NotificationBroadcasterSupport broadcaster;
public ServerMonitor() {
this.startTime = System.currentTimeMillis();
this.broadcaster = new NotificationBroadcasterSupport();
}
// Attribute getters
@Override
public int getActiveSessions() {
return activeSessions;
}
@Override
public long getUptime() {
return System.currentTimeMillis() - startTime;
}
@Override
public String getStatus() {
return running ? "RUNNING" : "STOPPED";
}
@Override
public int getMaxSessions() {
return maxSessions;
}
// Attribute setter
@Override
public void setMaxSessions(int maxSessions) {
if (maxSessions < 1) {
throw new IllegalArgumentException("Max sessions must be positive");
}
this.maxSessions = maxSessions;
// Send notification when configuration changes
Notification notification = new Notification(
"server.config.changed",
this,
System.currentTimeMillis(),
"Max sessions changed to: " + maxSessions
);
broadcaster.sendNotification(notification);
}
// Operations
@Override
public void start() {
this.running = true;
this.startTime = System.currentTimeMillis();
Notification notification = new Notification(
"server.started",
this,
System.currentTimeMillis(),
"Server started successfully"
);
broadcaster.sendNotification(notification);
}
@Override
public void stop() {
this.running = false;
this.activeSessions = 0;
Notification notification = new Notification(
"server.stopped",
this,
System.currentTimeMillis(),
"Server stopped"
);
broadcaster.sendNotification(notification);
}
@Override
public void restart() {
stop();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
start();
}
@Override
public String performHealthCheck() {
double memoryUsage = (Runtime.getRuntime().totalMemory() - 
Runtime.getRuntime().freeMemory()) / 
(1024.0 * 1024.0);
return String.format("Health Check: Sessions=%d/%d, Memory=%.2fMB, Uptime=%dms",
activeSessions, maxSessions, memoryUsage, getUptime());
}
// Business methods (not exposed via JMX)
public void incrementSessions() {
if (activeSessions < maxSessions) {
activeSessions++;
} else {
// Send notification when session limit reached
Notification notification = new Notification(
"server.sessions.limit",
this,
System.currentTimeMillis(),
"Session limit reached: " + activeSessions
);
broadcaster.sendNotification(notification);
}
}
public void decrementSessions() {
if (activeSessions > 0) {
activeSessions--;
}
}
// Notification support
@Override
public void addNotificationListener(NotificationListener listener, 
NotificationFilter filter, Object handback) {
broadcaster.addNotificationListener(listener, filter, handback);
}
@Override
public void removeNotificationListener(NotificationListener listener) {
broadcaster.removeNotificationListener(listener);
}
}

3. MXBeans (Modern Approach)

// DatabaseStatsMXBean.java - MXBean interface (uses open types)
public interface DatabaseStatsMXBean {
@ManagedAttribute
int getConnectionCount();
@ManagedAttribute
long getTotalQueries();
@ManagedAttribute
DatabaseStats getStats();
@ManagedOperation
void resetStatistics();
@ManagedOperation
QueryStats getQueryStats(String queryType);
}
// DatabaseStats.java - Composite data type
public class DatabaseStats {
private final int activeConnections;
private final long totalQueries;
private final double avgResponseTime;
private final Date lastReset;
public DatabaseStats(int activeConnections, long totalQueries, 
double avgResponseTime, Date lastReset) {
this.activeConnections = activeConnections;
this.totalQueries = totalQueries;
this.avgResponseTime = avgResponseTime;
this.lastReset = lastReset;
}
// Getters
public int getActiveConnections() { return activeConnections; }
public long getTotalQueries() { return totalQueries; }
public double getAvgResponseTime() { return avgResponseTime; }
public Date getLastReset() { return lastReset; }
}
// QueryStats.java - Another composite type
public class QueryStats {
private final String queryType;
private final long count;
private final double avgDuration;
public QueryStats(String queryType, long count, double avgDuration) {
this.queryType = queryType;
this.count = count;
this.avgDuration = avgDuration;
}
// Getters
public String getQueryType() { return queryType; }
public long getCount() { return count; }
public double getAvgDuration() { return avgDuration; }
}
// DatabaseStatsImpl.java - MXBean implementation
public class DatabaseStatsImpl implements DatabaseStatsMXBean {
private int connectionCount = 0;
private long totalQueries = 0;
private long lastResetTime = System.currentTimeMillis();
private final Map<String, Long> queryCounts = new ConcurrentHashMap<>();
private final Map<String, Double> queryDurations = new ConcurrentHashMap<>();
@Override
public int getConnectionCount() {
return connectionCount;
}
@Override
public long getTotalQueries() {
return totalQueries;
}
@Override
public DatabaseStats getStats() {
double avgResponseTime = calculateAverageResponseTime();
return new DatabaseStats(connectionCount, totalQueries, 
avgResponseTime, new Date(lastResetTime));
}
@Override
public QueryStats getQueryStats(String queryType) {
long count = queryCounts.getOrDefault(queryType, 0L);
double avgDuration = queryDurations.getOrDefault(queryType, 0.0);
return new QueryStats(queryType, count, avgDuration);
}
@Override
public void resetStatistics() {
connectionCount = 0;
totalQueries = 0;
lastResetTime = System.currentTimeMillis();
queryCounts.clear();
queryDurations.clear();
}
// Business methods
public void connectionOpened() {
connectionCount++;
}
public void connectionClosed() {
if (connectionCount > 0) {
connectionCount--;
}
}
public void queryExecuted(String queryType, long duration) {
totalQueries++;
queryCounts.merge(queryType, 1L, Long::sum);
queryDurations.merge(queryType, (double)duration, 
(old, newVal) -> (old + newVal) / 2);
}
private double calculateAverageResponseTime() {
return queryDurations.values().stream()
.mapToDouble(Double::doubleValue)
.average()
.orElse(0.0);
}
}

4. JMX Agent Setup and Registration

// JMXAgent.java - Setting up JMX agent and registering MBeans
public class JMXAgent {
private MBeanServer mBeanServer;
private ObjectName serverMonitorName;
private ObjectName databaseStatsName;
private JMXConnectorServer connectorServer;
public void start() throws Exception {
System.out.println("Starting JMX Agent...");
// Get the platform MBeanServer
mBeanServer = ManagementFactory.getPlatformMBeanServer();
// Register MBeans
registerServerMonitor();
registerDatabaseStats();
// Start RMI connector
startRMIConnector();
System.out.println("JMX Agent started successfully");
System.out.println("Connect using: service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi");
}
private void registerServerMonitor() throws Exception {
ServerMonitor serverMonitor = new ServerMonitor();
serverMonitorName = new ObjectName("com.example:type=ServerMonitor,name=ApplicationServer");
mBeanServer.registerMBean(serverMonitor, serverMonitorName);
System.out.println("Registered ServerMonitor MBean: " + serverMonitorName);
}
private void registerDatabaseStats() throws Exception {
DatabaseStatsImpl dbStats = new DatabaseStatsImpl();
databaseStatsName = new ObjectName("com.example:type=DatabaseStats,name=MainDatabase");
mBeanServer.registerMBean(dbStats, databaseStatsName);
System.out.println("Registered DatabaseStats MXBean: " + databaseStatsName);
}
private void startRMIConnector() throws Exception {
// Create RMI connector server
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi");
connectorServer = JMXConnectorServerFactory.newJMXConnectorServer(url, null, mBeanServer);
connectorServer.start();
System.out.println("JMX Connector Server started on port 9999");
}
public void stop() throws Exception {
if (connectorServer != null) {
connectorServer.stop();
System.out.println("JMX Connector Server stopped");
}
if (mBeanServer != null) {
if (serverMonitorName != null) {
mBeanServer.unregisterMBean(serverMonitorName);
}
if (databaseStatsName != null) {
mBeanServer.unregisterMBean(databaseStatsName);
}
}
System.out.println("JMX Agent stopped");
}
public static void main(String[] args) throws Exception {
JMXAgent agent = new JMXAgent();
agent.start();
// Keep the agent running
System.out.println("Press Enter to stop the JMX Agent...");
System.in.read();
agent.stop();
}
}

5. Dynamic MBeans

// DynamicServiceMonitor.java - Dynamic MBean implementation
public class DynamicServiceMonitor implements DynamicMBean {
private final Map<String, Object> attributes = new HashMap<>();
private final Map<String, MBeanOperationInfo> operations = new HashMap<>();
private final MBeanInfo mBeanInfo;
public DynamicServiceMonitor() {
initializeAttributes();
initializeOperations();
this.mBeanInfo = createMBeanInfo();
}
private void initializeAttributes() {
attributes.put("RequestCount", 0L);
attributes.put("ErrorCount", 0L);
attributes.put("AverageResponseTime", 0.0);
attributes.put("Active", true);
attributes.put("ServiceName", "DynamicService");
}
private void initializeOperations() {
operations.put("reset", new MBeanOperationInfo(
"reset",
"Reset all statistics",
new MBeanParameterInfo[0],
"void",
MBeanOperationInfo.ACTION
));
operations.put("getDetailedStats", new MBeanOperationInfo(
"getDetailedStats",
"Get detailed statistics",
new MBeanParameterInfo[0],
"java.lang.String",
MBeanOperationInfo.INFO
));
}
private MBeanInfo createMBeanInfo() {
List<MBeanAttributeInfo> attributeInfos = new ArrayList<>();
attributeInfos.add(new MBeanAttributeInfo(
"RequestCount", "long", "Total number of requests", 
true, false, false
));
attributeInfos.add(new MBeanAttributeInfo(
"ErrorCount", "long", "Total number of errors", 
true, false, false
));
attributeInfos.add(new MBeanAttributeInfo(
"AverageResponseTime", "double", "Average response time in ms", 
true, false, false
));
attributeInfos.add(new MBeanAttributeInfo(
"Active", "boolean", "Whether service is active", 
true, true, false
));
attributeInfos.add(new MBeanAttributeInfo(
"ServiceName", "java.lang.String", "Name of the service", 
true, false, false
));
return new MBeanInfo(
this.getClass().getName(),
"Dynamic Service Monitor",
attributeInfos.toArray(new MBeanAttributeInfo[0]),
new MBeanConstructorInfo[0],
operations.values().toArray(new MBeanOperationInfo[0]),
new MBeanNotificationInfo[0]
);
}
@Override
public Object getAttribute(String attribute) throws AttributeNotFoundException {
if (!attributes.containsKey(attribute)) {
throw new AttributeNotFoundException("Attribute not found: " + attribute);
}
return attributes.get(attribute);
}
@Override
public void setAttribute(Attribute attribute) throws InvalidAttributeValueException {
String name = attribute.getName();
Object value = attribute.getValue();
if (!attributes.containsKey(name)) {
throw new InvalidAttributeValueException("Attribute not found: " + name);
}
// Type validation
Object currentValue = attributes.get(name);
if (currentValue != null && !currentValue.getClass().isInstance(value)) {
throw new InvalidAttributeValueException(
"Invalid type for attribute " + name + ". Expected: " + 
currentValue.getClass().getSimpleName()
);
}
attributes.put(name, value);
}
@Override
public AttributeList getAttributes(String[] attributes) {
AttributeList result = new AttributeList();
for (String attribute : attributes) {
try {
result.add(new Attribute(attribute, getAttribute(attribute)));
} catch (AttributeNotFoundException e) {
// Skip missing attributes
}
}
return result;
}
@Override
public AttributeList setAttributes(AttributeList attributes) {
AttributeList result = new AttributeList();
for (Object attributeObj : attributes) {
Attribute attribute = (Attribute) attributeObj;
try {
setAttribute(attribute);
result.add(new Attribute(attribute.getName(), getAttribute(attribute.getName())));
} catch (Exception e) {
// Skip failed attributes
}
}
return result;
}
@Override
public Object invoke(String actionName, Object[] params, String[] signature) {
switch (actionName) {
case "reset":
attributes.put("RequestCount", 0L);
attributes.put("ErrorCount", 0L);
attributes.put("AverageResponseTime", 0.0);
return null;
case "getDetailedStats":
return String.format(
"Service: %s, Requests: %d, Errors: %d, Avg Response: %.2fms, Active: %s",
attributes.get("ServiceName"),
attributes.get("RequestCount"),
attributes.get("ErrorCount"),
attributes.get("AverageResponseTime"),
attributes.get("Active")
);
default:
throw new IllegalArgumentException("Unknown operation: " + actionName);
}
}
@Override
public MBeanInfo getMBeanInfo() {
return mBeanInfo;
}
// Business methods
public void recordRequest(long responseTime) {
long requestCount = (Long) attributes.get("RequestCount");
double avgResponseTime = (Double) attributes.get("AverageResponseTime");
attributes.put("RequestCount", requestCount + 1);
attributes.put("AverageResponseTime", 
(avgResponseTime * requestCount + responseTime) / (requestCount + 1));
}
public void recordError() {
long errorCount = (Long) attributes.get("ErrorCount");
attributes.put("ErrorCount", errorCount + 1);
}
}

6. JMX Client Application

// JMXClient.java - Client to connect and manage MBeans
public class JMXClient {
private JMXConnector connector;
private MBeanServerConnection connection;
public void connect(String url) throws IOException {
JMXServiceURL jmxUrl = new JMXServiceURL(url);
connector = JMXConnectorFactory.connect(jmxUrl);
connection = connector.getMBeanServerConnection();
System.out.println("Connected to JMX agent: " + url);
}
public void disconnect() throws IOException {
if (connector != null) {
connector.close();
System.out.println("Disconnected from JMX agent");
}
}
public void listAllMBeans() throws Exception {
Set<ObjectName> names = connection.queryNames(null, null);
System.out.println("\n=== Registered MBeans ===");
for (ObjectName name : names) {
System.out.println("MBean: " + name);
MBeanInfo info = connection.getMBeanInfo(name);
System.out.println("  Description: " + info.getDescription());
// Print attributes
for (MBeanAttributeInfo attr : info.getAttributes()) {
if (attr.isReadable()) {
try {
Object value = connection.getAttribute(name, attr.getName());
System.out.println("    " + attr.getName() + ": " + value);
} catch (Exception e) {
System.out.println("    " + attr.getName() + ": <unavailable>");
}
}
}
// Print operations
for (MBeanOperationInfo op : info.getOperations()) {
System.out.println("    Operation: " + op.getName() + 
" - " + op.getDescription());
}
System.out.println();
}
}
public void monitorServer() throws Exception {
ObjectName serverName = new ObjectName("com.example:type=ServerMonitor,name=ApplicationServer");
// Get attributes
Integer activeSessions = (Integer) connection.getAttribute(serverName, "ActiveSessions");
Long uptime = (Long) connection.getAttribute(serverName, "Uptime");
String status = (String) connection.getAttribute(serverName, "Status");
System.out.println("Server Status:");
System.out.println("  Active Sessions: " + activeSessions);
System.out.println("  Uptime: " + uptime + "ms");
System.out.println("  Status: " + status);
// Invoke operation
String healthCheck = (String) connection.invoke(serverName, "performHealthCheck", null, null);
System.out.println("Health Check: " + healthCheck);
}
public void manageDatabaseStats() throws Exception {
ObjectName dbName = new ObjectName("com.example:type=DatabaseStats,name=MainDatabase");
// Get composite attribute
CompositeData stats = (CompositeData) connection.getAttribute(dbName, "Stats");
System.out.println("Database Statistics:");
System.out.println("  Active Connections: " + stats.get("activeConnections"));
System.out.println("  Total Queries: " + stats.get("totalQueries"));
System.out.println("  Avg Response Time: " + stats.get("avgResponseTime"));
System.out.println("  Last Reset: " + stats.get("lastReset"));
// Invoke operation with parameters
Object[] params = {"SELECT"};
String[] signature = {"java.lang.String"};
CompositeData queryStats = (CompositeData) connection.invoke(dbName, "getQueryStats", params, signature);
System.out.println("SELECT Query Stats:");
System.out.println("  Count: " + queryStats.get("count"));
System.out.println("  Avg Duration: " + queryStats.get("avgDuration"));
}
public void setServerMaxSessions(int maxSessions) throws Exception {
ObjectName serverName = new ObjectName("com.example:type=ServerMonitor,name=ApplicationServer");
Attribute attribute = new Attribute("MaxSessions", maxSessions);
connection.setAttribute(serverName, attribute);
System.out.println("Set MaxSessions to: " + maxSessions);
}
public static void main(String[] args) {
JMXClient client = new JMXClient();
try {
client.connect("service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi");
// List all MBeans
client.listAllMBeans();
// Monitor server
client.monitorServer();
// Manage database stats
client.manageDatabaseStats();
// Set configuration
client.setServerMaxSessions(1500);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
client.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

7. JMX Notifications

// NotificationListenerExample.java - Handling JMX notifications
public class NotificationListenerExample implements NotificationListener {
@Override
public void handleNotification(Notification notification, Object handback) {
System.out.println("\n=== JMX Notification Received ===");
System.out.println("Type: " + notification.getType());
System.out.println("Message: " + notification.getMessage());
System.out.println("Sequence Number: " + notification.getSequenceNumber());
System.out.println("Timestamp: " + new Date(notification.getTimeStamp()));
System.out.println("Source: " + notification.getSource());
System.out.println("User Data: " + notification.getUserData());
System.out.println("================================\n");
}
public void registerListener(MBeanServerConnection connection) throws Exception {
ObjectName serverName = new ObjectName("com.example:type=ServerMonitor,name=ApplicationServer");
connection.addNotificationListener(serverName, this, null, null);
System.out.println("Registered notification listener for: " + serverName);
}
}
// Using the notification listener in client
public class NotificationClient {
public static void main(String[] args) throws Exception {
JMXClient client = new JMXClient();
NotificationListenerExample listener = new NotificationListenerExample();
client.connect("service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi");
listener.registerListener(client.connection);
// Keep running to receive notifications
System.out.println("Waiting for notifications... Press Enter to exit.");
System.in.read();
client.disconnect();
}
}

8. Spring-based JMX Configuration

// SpringJMXConfig.java - Spring-based JMX configuration
@Configuration
@EnableMBeanExport
public class SpringJMXConfig {
@Bean
public ServerMonitor serverMonitor() {
return new ServerMonitor();
}
@Bean
public DatabaseStatsImpl databaseStats() {
return new DatabaseStatsImpl();
}
@Bean
public DynamicServiceMonitor dynamicServiceMonitor() {
return new DynamicServiceMonitor();
}
}
// SpringBoot JMX Application
@SpringBootApplication
public class SpringJMXApplication {
@Autowired
private ServerMonitor serverMonitor;
@Autowired
private DatabaseStatsImpl databaseStats;
@PostConstruct
public void init() {
// Simulate some activity
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
serverMonitor.incrementSessions();
databaseStats.queryExecuted("SELECT", 50L);
}, 0, 5, TimeUnit.SECONDS);
}
public static void main(String[] args) {
SpringApplication.run(SpringJMXApplication.class, args);
}
}

Key JMX Concepts

  1. MBeans: Managed Beans - the resources you want to monitor/manage
  2. MBean Server: The core registry for MBeans
  3. JMX Connectors: How clients connect to the MBean server
  4. Protocol Adaptors: Expose MBeans through different protocols
  5. Notifications: Event mechanism in JMX
  6. MXBeans: Modern approach with better type handling

JMX provides a standardized way to monitor and manage Java applications, making it essential for production systems and enterprise applications.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper