Files
gh-giuseppe-trisciuoglio-de…/skills/spring-boot-actuator/references/jmx.md
2025-11-29 18:28:34 +08:00

18 KiB

JMX with Spring Boot Actuator

Java Management Extensions (JMX) provide a standard mechanism to monitor and manage applications. By default, this feature is not enabled. You can turn it on by setting the spring.jmx.enabled configuration property to true. Spring Boot exposes the most suitable MBeanServer as a bean with an ID of mbeanServer. Any of your beans that are annotated with Spring JMX annotations (@ManagedResource, @ManagedAttribute, or @ManagedOperation) are exposed to it.

If your platform provides a standard MBeanServer, Spring Boot uses that and defaults to the VM MBeanServer, if necessary. If all that fails, a new MBeanServer is created.

Note

spring.jmx.enabled affects only the management beans provided by Spring. Enabling management beans provided by other libraries (for example Log4j2 or Quartz) is independent.

Basic JMX Configuration

Enabling JMX

spring:
  jmx:
    enabled: true
    default-domain: com.example.myapp

management:
  endpoints:
    jmx:
      exposure:
        include: "*"
  endpoint:
    jmx:
      enabled: true

Custom MBean Server Configuration

@Configuration
public class JmxConfiguration {

    @Bean
    @Primary
    public MBeanServer mbeanServer() {
        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
        return server;
    }

    @Bean
    public JmxMetricsExporter jmxMetricsExporter(MeterRegistry meterRegistry) {
        return new JmxMetricsExporter(meterRegistry);
    }
}

Creating Custom MBeans

Using @ManagedResource Annotation

@Component
@ManagedResource(
    objectName = "com.example:type=ApplicationMetrics,name=UserService",
    description = "User Service Management Bean"
)
public class UserServiceMBean {

    private final UserService userService;
    private long totalUsers = 0;
    private long activeUsers = 0;

    public UserServiceMBean(UserService userService) {
        this.userService = userService;
    }

    @ManagedAttribute(description = "Total number of users")
    public long getTotalUsers() {
        return userService.getTotalUserCount();
    }

    @ManagedAttribute(description = "Number of active users")
    public long getActiveUsers() {
        return userService.getActiveUserCount();
    }

    @ManagedAttribute(description = "Cache hit ratio")
    public double getCacheHitRatio() {
        return userService.getCacheHitRatio();
    }

    @ManagedOperation(description = "Clear user cache")
    public void clearCache() {
        userService.clearCache();
    }

    @ManagedOperation(description = "Refresh user statistics")
    public String refreshStatistics() {
        userService.refreshStatistics();
        return "Statistics refreshed at " + Instant.now();
    }

    @ManagedOperation(description = "Get user by ID")
    @ManagedOperationParameters({
        @ManagedOperationParameter(name = "userId", description = "User ID")
    })
    public String getUserInfo(Long userId) {
        User user = userService.findById(userId);
        return user != null ? user.toString() : "User not found";
    }
}

Implementing MBean Interface

public interface ApplicationConfigMBean {
    String getEnvironment();
    void setLogLevel(String loggerName, String level);
    boolean isMaintenanceMode();
    void setMaintenanceMode(boolean maintenanceMode);
    void reloadConfiguration();
    Map<String, String> getSystemProperties();
}

@Component
public class ApplicationConfig implements ApplicationConfigMBean {

    private final Environment environment;
    private final LoggingSystem loggingSystem;
    private boolean maintenanceMode = false;

    public ApplicationConfig(Environment environment, LoggingSystem loggingSystem) {
        this.environment = environment;
        this.loggingSystem = loggingSystem;
    }

    @Override
    public String getEnvironment() {
        return String.join(",", environment.getActiveProfiles());
    }

    @Override
    public void setLogLevel(String loggerName, String level) {
        LogLevel logLevel = level != null ? LogLevel.valueOf(level.toUpperCase()) : null;
        loggingSystem.setLogLevel(loggerName, logLevel);
    }

    @Override
    public boolean isMaintenanceMode() {
        return maintenanceMode;
    }

    @Override
    public void setMaintenanceMode(boolean maintenanceMode) {
        this.maintenanceMode = maintenanceMode;
        // Publish event or notify other components
    }

    @Override
    public void reloadConfiguration() {
        // Implement configuration reload logic
        // This could refresh @ConfigurationProperties beans
    }

    @Override
    public Map<String, String> getSystemProperties() {
        return System.getProperties().entrySet().stream()
            .collect(Collectors.toMap(
                e -> String.valueOf(e.getKey()),
                e -> String.valueOf(e.getValue())
            ));
    }

    @PostConstruct
    public void registerMBean() {
        try {
            MBeanServer server = ManagementFactory.getPlatformMBeanServer();
            ObjectName objectName = new ObjectName("com.example:type=ApplicationConfig");
            server.registerMBean(this, objectName);
        } catch (Exception e) {
            throw new RuntimeException("Failed to register MBean", e);
        }
    }
}

Application Metrics via JMX

Custom Metrics MBean

@Component
@ManagedResource(
    objectName = "com.example:type=Performance,name=ApplicationMetrics",
    description = "Application Performance Metrics"
)
public class ApplicationMetricsMBean {

    private final MeterRegistry meterRegistry;
    private final Counter requestCounter;
    private final Timer responseTimer;
    private final Gauge activeConnections;

    public ApplicationMetricsMBean(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.requestCounter = Counter.builder("application.requests.total")
            .description("Total number of requests")
            .register(meterRegistry);
        this.responseTimer = Timer.builder("application.response.time")
            .description("Response time")
            .register(meterRegistry);
        this.activeConnections = Gauge.builder("application.connections.active")
            .description("Active connections")
            .register(meterRegistry, this, ApplicationMetricsMBean::getActiveConnectionsCount);
    }

    @ManagedAttribute(description = "Total requests processed")
    public long getTotalRequests() {
        return (long) requestCounter.count();
    }

    @ManagedAttribute(description = "Average response time in milliseconds")
    public double getAverageResponseTime() {
        return responseTimer.mean(TimeUnit.MILLISECONDS);
    }

    @ManagedAttribute(description = "95th percentile response time")
    public double getResponse95thPercentile() {
        return responseTimer.percentile(0.95, TimeUnit.MILLISECONDS);
    }

    @ManagedAttribute(description = "Current active connections")
    public long getActiveConnections() {
        return getActiveConnectionsCount();
    }

    @ManagedAttribute(description = "JVM memory usage percentage")
    public double getMemoryUsagePercentage() {
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
        return (double) heapUsage.getUsed() / heapUsage.getMax() * 100;
    }

    @ManagedOperation(description = "Reset request counter")
    public void resetRequestCounter() {
        // Note: Micrometer counters cannot be reset, this would require custom implementation
        // or using a different metric type
    }

    private long getActiveConnectionsCount() {
        // Implementation to get actual active connections
        return 42; // Placeholder
    }
}

Database Connection Pool MBean

@Component
@ManagedResource(
    objectName = "com.example:type=Database,name=ConnectionPool",
    description = "Database Connection Pool Metrics"
)
public class DatabaseConnectionPoolMBean {

    private final DataSource dataSource;

    public DatabaseConnectionPoolMBean(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @ManagedAttribute(description = "Active connections")
    public int getActiveConnections() {
        if (dataSource instanceof HikariDataSource) {
            return ((HikariDataSource) dataSource).getHikariPoolMXBean().getActiveConnections();
        }
        return -1; // Not supported
    }

    @ManagedAttribute(description = "Idle connections")
    public int getIdleConnections() {
        if (dataSource instanceof HikariDataSource) {
            return ((HikariDataSource) dataSource).getHikariPoolMXBean().getIdleConnections();
        }
        return -1; // Not supported
    }

    @ManagedAttribute(description = "Total connections")
    public int getTotalConnections() {
        if (dataSource instanceof HikariDataSource) {
            return ((HikariDataSource) dataSource).getHikariPoolMXBean().getTotalConnections();
        }
        return -1; // Not supported
    }

    @ManagedAttribute(description = "Threads awaiting connection")
    public int getThreadsAwaitingConnection() {
        if (dataSource instanceof HikariDataSource) {
            return ((HikariDataSource) dataSource).getHikariPoolMXBean().getThreadsAwaitingConnection();
        }
        return -1; // Not supported
    }

    @ManagedOperation(description = "Suspend connection pool")
    public void suspendPool() {
        if (dataSource instanceof HikariDataSource) {
            ((HikariDataSource) dataSource).getHikariPoolMXBean().suspendPool();
        }
    }

    @ManagedOperation(description = "Resume connection pool")
    public void resumePool() {
        if (dataSource instanceof HikariDataSource) {
            ((HikariDataSource) dataSource).getHikariPoolMXBean().resumePool();
        }
    }
}

Security and JMX

Securing JMX Access

spring:
  jmx:
    enabled: true

management:
  endpoints:
    jmx:
      exposure:
        include: "health,info,metrics"
        exclude: "env,configprops"  # Exclude sensitive endpoints

# JMX-specific security
com.sun.management.jmxremote.port: 9999
com.sun.management.jmxremote.authenticate: true
com.sun.management.jmxremote.ssl: false
com.sun.management.jmxremote.access.file: /path/to/jmxremote.access
com.sun.management.jmxremote.password.file: /path/to/jmxremote.password

Custom JMX Security

@Configuration
public class JmxSecurityConfiguration {

    @Bean
    public JMXConnectorServer jmxConnectorServer() throws Exception {
        JMXServiceURL url = new JMXServiceURL("service:jmx:rmi://localhost:9999");
        
        Map<String, Object> environment = new HashMap<>();
        environment.put(JMXConnectorServer.AUTHENTICATOR, new CustomJMXAuthenticator());
        
        JMXConnectorServer server = JMXConnectorServerFactory.newJMXConnectorServer(
            url, environment, ManagementFactory.getPlatformMBeanServer());
        
        server.start();
        return server;
    }

    private static class CustomJMXAuthenticator implements JMXAuthenticator {
        @Override
        public Subject authenticate(Object credentials) {
            if (!(credentials instanceof String[])) {
                throw new SecurityException("Credentials must be String[]");
            }
            
            String[] creds = (String[]) credentials;
            if (creds.length != 2) {
                throw new SecurityException("Credentials must contain username and password");
            }
            
            String username = creds[0];
            String password = creds[1];
            
            // Implement your authentication logic
            if ("admin".equals(username) && "password".equals(password)) {
                return new Subject();
            }
            
            throw new SecurityException("Authentication failed");
        }
    }
}

Monitoring and Alerting with JMX

Health Check MBean

@Component
@ManagedResource(
    objectName = "com.example:type=Health,name=ApplicationHealth",
    description = "Application Health Monitoring"
)
public class ApplicationHealthMBean {

    private final HealthEndpoint healthEndpoint;
    private final List<String> healthIssues = new ArrayList<>();

    public ApplicationHealthMBean(HealthEndpoint healthEndpoint) {
        this.healthEndpoint = healthEndpoint;
    }

    @ManagedAttribute(description = "Overall application health status")
    public String getHealthStatus() {
        HealthComponent health = healthEndpoint.health();
        return health.getStatus().getCode();
    }

    @ManagedAttribute(description = "Detailed health information")
    public String getHealthDetails() {
        HealthComponent health = healthEndpoint.health();
        return health.toString();
    }

    @ManagedAttribute(description = "Database health status")
    public String getDatabaseHealth() {
        HealthComponent health = healthEndpoint.healthForPath("db");
        return health != null ? health.getStatus().getCode() : "UNKNOWN";
    }

    @ManagedAttribute(description = "Current health issues")
    public String[] getHealthIssues() {
        return healthIssues.toArray(new String[0]);
    }

    @ManagedOperation(description = "Refresh health status")
    public void refreshHealth() {
        HealthComponent health = healthEndpoint.health();
        healthIssues.clear();
        
        if (health instanceof CompositeHealthComponent) {
            CompositeHealthComponent composite = (CompositeHealthComponent) health;
            composite.getComponents().forEach((name, component) -> {
                if (!Status.UP.equals(component.getStatus())) {
                    healthIssues.add(name + ": " + component.getStatus().getCode());
                }
            });
        }
    }

    @PostConstruct
    public void init() {
        refreshHealth();
    }
}

Notification MBean

@Component
@ManagedResource(
    objectName = "com.example:type=Notifications,name=AlertManager",
    description = "Application Alert Management"
)
public class AlertManagerMBean extends NotificationBroadcasterSupport {

    private final AtomicLong sequenceNumber = new AtomicLong(0);
    private boolean alertsEnabled = true;

    @ManagedAttribute(description = "Are alerts enabled")
    public boolean isAlertsEnabled() {
        return alertsEnabled;
    }

    @ManagedAttribute(description = "Enable or disable alerts")
    public void setAlertsEnabled(boolean alertsEnabled) {
        this.alertsEnabled = alertsEnabled;
    }

    @ManagedOperation(description = "Send test alert")
    public void sendTestAlert() {
        sendAlert("TEST", "Test alert from JMX", "INFO");
    }

    public void sendAlert(String type, String message, String severity) {
        if (!alertsEnabled) {
            return;
        }

        Notification notification = new Notification(
            type,
            this,
            sequenceNumber.incrementAndGet(),
            System.currentTimeMillis(),
            message
        );
        
        notification.setUserData(Map.of(
            "severity", severity,
            "timestamp", Instant.now().toString()
        ));

        sendNotification(notification);
    }

    @Override
    public MBeanNotificationInfo[] getNotificationInfo() {
        return new MBeanNotificationInfo[]{
            new MBeanNotificationInfo(
                new String[]{"HEALTH", "PERFORMANCE", "SECURITY", "TEST"},
                Notification.class.getName(),
                "Application alerts and notifications"
            )
        };
    }
}

Best Practices

  1. Naming Convention: Use consistent ObjectName patterns
  2. Security: Always secure JMX access in production
  3. Performance: Be mindful of expensive operations in MBean methods
  4. Documentation: Provide clear descriptions for attributes and operations
  5. Error Handling: Handle exceptions gracefully in MBean operations
  6. Resource Management: Properly manage resources in MBean operations
  7. Monitoring: Monitor JMX itself for availability and performance

Production JMX Configuration

# Production JMX configuration
spring:
  jmx:
    enabled: true
    default-domain: "com.mycompany.myapp"

management:
  endpoints:
    jmx:
      exposure:
        include: "health,info,metrics"
        exclude: "env,configprops,beans"
  endpoint:
    jmx:
      enabled: true

# JVM JMX settings (set as JVM arguments)
# -Dcom.sun.management.jmxremote=true
# -Dcom.sun.management.jmxremote.port=9999
# -Dcom.sun.management.jmxremote.authenticate=true
# -Dcom.sun.management.jmxremote.ssl=true
# -Dcom.sun.management.jmxremote.access.file=/etc/jmx/jmxremote.access
# -Dcom.sun.management.jmxremote.password.file=/etc/jmx/jmxremote.password

JMX Client Example

public class JmxClient {

    public static void main(String[] args) throws Exception {
        String url = "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi";
        JMXServiceURL serviceURL = new JMXServiceURL(url);
        
        Map<String, Object> environment = new HashMap<>();
        environment.put(JMXConnector.CREDENTIALS, new String[]{"admin", "password"});
        
        try (JMXConnector connector = JMXConnectorFactory.connect(serviceURL, environment)) {
            MBeanServerConnection connection = connector.getMBeanServerConnection();
            
            // Get application health
            ObjectName healthName = new ObjectName("com.example:type=Health,name=ApplicationHealth");
            String healthStatus = (String) connection.getAttribute(healthName, "HealthStatus");
            System.out.println("Health Status: " + healthStatus);
            
            // Invoke operation
            connection.invoke(healthName, "refreshHealth", null, null);
            
            // Listen for notifications
            ObjectName alertName = new ObjectName("com.example:type=Notifications,name=AlertManager");
            connection.addNotificationListener(alertName, 
                (notification, handback) -> {
                    System.out.println("Alert: " + notification.getMessage());
                }, null, null);
        }
    }
}