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
- MBeans: Managed Beans - the resources you want to monitor/manage
- MBean Server: The core registry for MBeans
- JMX Connectors: How clients connect to the MBean server
- Protocol Adaptors: Expose MBeans through different protocols
- Notifications: Event mechanism in JMX
- 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.