docs
This documentation has been enhanced for AI coding agents with comprehensive examples, complete API signatures, thread safety notes, error handling patterns, and production-ready usage patterns.
Package: org.springframework.boot.admin
Module: org.springframework.boot:spring-boot
Since: 1.3.0
JMX (Java Management Extensions) administration support for managing and monitoring Spring Boot applications remotely through MBeans. This enables operational control including application readiness checks, environment inspection, and graceful shutdown via JMX clients like JConsole, VisualVM, or programmatic access.
Spring Boot's JMX admin support provides a standardized MBean interface for remote application management. The system exposes application state, configuration properties, and lifecycle controls through JMX, enabling integration with enterprise monitoring and management tools.
MBean contract for remote management of a running Spring Boot application.
package org.springframework.boot.admin;
/**
* MBean contract to control and monitor a running SpringApplication over JMX.
* Intended for programmatic control and monitoring of Spring Boot applications.
*
* @since 1.3.0
*/
public interface SpringApplicationAdminMXBean {
/**
* Check if the application has fully started and is ready to handle requests.
* Returns true after ApplicationReadyEvent has been published.
*
* @return true if the application is ready, false during startup or if not ready
*/
boolean isReady();
/**
* Check if the application runs in an embedded web container.
* Returns true for applications with embedded Tomcat, Jetty, or Undertow.
* Returns false for WAR deployments or non-web applications.
*
* @return true if running as embedded web application
*/
boolean isEmbeddedWebApplication();
/**
* Return the value of the specified key from the application Environment.
* Retrieves properties from all property sources in the Environment.
*
* @param key the property key (never null)
* @return the property value as a String, or null if not found
* @throws IllegalArgumentException if key is null
*/
@Nullable String getProperty(String key);
/**
* Shutdown the application gracefully.
* Closes the ApplicationContext and runs registered shutdown hooks.
* This is a non-blocking call - the JVM will continue shutdown asynchronously.
*
* Thread Safety: Safe to call from any thread
*
* @throws IllegalStateException if the application is already shutting down
*/
void shutdown();
}Key Methods:
isReady(): Returns false during startup, true after ApplicationReadyEventisEmbeddedWebApplication(): Detects embedded web server presencegetProperty(String): Accesses any Environment propertyshutdown(): Initiates graceful application shutdownThread Safety: All methods are thread-safe and can be called concurrently from multiple JMX clients.
Registers the admin MBean with the platform MBean server, making it available to JMX clients.
package org.springframework.boot.admin;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.EnvironmentAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.DisposableBean;
/**
* Register a SpringApplicationAdminMXBean implementation to the platform MBeanServer.
* Automatically unregisters the MBean when the ApplicationContext is destroyed.
*
* Implements multiple lifecycle interfaces:
* - ApplicationContextAware: Receives the ApplicationContext reference
* - EnvironmentAware: Receives the Environment reference
* - ApplicationListener: Listens for application lifecycle events
* - InitializingBean: Registers MBean after properties are set
* - DisposableBean: Unregisters MBean on shutdown
*
* @since 1.3.0
*/
public class SpringApplicationAdminMXBeanRegistrar
implements ApplicationContextAware, GenericApplicationListener,
EnvironmentAware, InitializingBean, DisposableBean {
/**
* Create a new SpringApplicationAdminMXBeanRegistrar.
*
* @param name the ObjectName for the MBean (e.g., "org.springframework.boot:type=Admin,name=SpringApplication")
* @throws MalformedObjectNameException if the name is not a valid ObjectName
* @throws IllegalArgumentException if name is null
*/
public SpringApplicationAdminMXBeanRegistrar(String name) throws MalformedObjectNameException {
// Implementation registers MBean with given name
}
/**
* Determine whether the given event type is supported by this listener.
* Supports ApplicationReadyEvent to track when the application is ready.
*
* @param eventType the event type (class)
* @return true if the event type is ApplicationReadyEvent
* @since 1.3.0
*/
@Override
public boolean supportsEventType(ResolvableType eventType) {
// Returns true for ApplicationReadyEvent
}
/**
* Determine whether the given source type is supported by this listener.
* Supports all source types.
*
* @param sourceType the source type, or null if no source
* @return true for all source types
* @since 1.3.0
*/
@Override
public boolean supportsSourceType(@Nullable Class<?> sourceType) {
// Returns true for all source types
}
/**
* Handle an application event.
* Listens for ApplicationReadyEvent to mark the application as ready.
*
* @param event the event to respond to
* @since 1.3.0
*/
@Override
public void onApplicationEvent(ApplicationEvent event) {
// Handles ApplicationReadyEvent to set ready flag
}
/**
* Get the order value of this listener.
* Returns HIGHEST_PRECEDENCE to ensure early event processing.
*
* @return Ordered.HIGHEST_PRECEDENCE
* @since 1.3.0
*/
@Override
public int getOrder() {
// Returns Ordered.HIGHEST_PRECEDENCE
}
@Override
public void afterPropertiesSet() throws Exception {
// Registers the MBean with the platform MBean server
}
@Override
public void destroy() throws Exception {
// Unregisters the MBean from the platform MBean server
}
}Lifecycle:
setApplicationContext() - receives ApplicationContextsetEnvironment() - receives EnvironmentafterPropertiesSet() - registers MBeandestroy() - unregisters MBean on shutdownEnable JMX admin support in your Spring Boot application:
package com.example.config;
import org.springframework.boot.admin.SpringApplicationAdminMXBeanRegistrar;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.management.MalformedObjectNameException;
/**
* Configuration for JMX administration support.
*/
@Configuration
public class JmxAdminConfig {
/**
* Register the Spring Application Admin MBean.
*
* @return the MBean registrar
* @throws MalformedObjectNameException if ObjectName is invalid (should never happen with valid string)
*/
@Bean
public SpringApplicationAdminMXBeanRegistrar springApplicationAdminRegistrar()
throws MalformedObjectNameException {
return new SpringApplicationAdminMXBeanRegistrar(
"org.springframework.boot:type=Admin,name=SpringApplication"
);
}
}Alternative: Property-Based Configuration
# application.properties
# Enable JMX admin support
spring.application.admin.enabled=true
# Optional: Customize the JMX object name
spring.application.admin.jmx-name=org.springframework.boot:type=Admin,name=SpringApplication# application.yml
spring:
application:
admin:
enabled: true
jmx-name: "org.springframework.boot:type=Admin,name=SpringApplication"Access the admin MBean programmatically from Java code:
package com.example.management;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.io.IOException;
/**
* Client for programmatic access to Spring Boot Admin MBean.
*/
public class JmxAdminClient implements AutoCloseable {
private final JMXConnector connector;
private final MBeanServerConnection connection;
private final ObjectName adminMBean;
/**
* Connect to JMX server and locate admin MBean.
*
* @param jmxUrl the JMX service URL (e.g., "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi")
* @param objectName the admin MBean ObjectName
* @throws IOException if connection fails
* @throws Exception if MBean lookup fails
*/
public JmxAdminClient(String jmxUrl, String objectName) throws Exception {
// Connect to JMX server
JMXServiceURL url = new JMXServiceURL(jmxUrl);
this.connector = JMXConnectorFactory.connect(url);
this.connection = connector.getMBeanServerConnection();
this.adminMBean = new ObjectName(objectName);
}
/**
* Check if the application is ready.
*
* @return true if application is ready
* @throws Exception if JMX operation fails
*/
public boolean isApplicationReady() throws Exception {
return (Boolean) connection.getAttribute(adminMBean, "Ready");
}
/**
* Check if application is an embedded web application.
*
* @return true if embedded web application
* @throws Exception if JMX operation fails
*/
public boolean isEmbeddedWebApplication() throws Exception {
return (Boolean) connection.getAttribute(adminMBean, "EmbeddedWebApplication");
}
/**
* Get an environment property value.
*
* @param propertyName the property name
* @return the property value, or null if not found
* @throws Exception if JMX operation fails
*/
public String getProperty(String propertyName) throws Exception {
return (String) connection.invoke(
adminMBean,
"getProperty",
new Object[]{propertyName},
new String[]{"java.lang.String"}
);
}
/**
* Shutdown the application remotely.
*
* @throws Exception if JMX operation fails
*/
public void shutdownApplication() throws Exception {
connection.invoke(
adminMBean,
"shutdown",
new Object[]{},
new String[]{}
);
}
@Override
public void close() throws IOException {
if (connector != null) {
connector.close();
}
}
/**
* Example usage.
*/
public static void main(String[] args) {
String jmxUrl = "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi";
String mbeanName = "org.springframework.boot:type=Admin,name=SpringApplication";
try (JmxAdminClient client = new JmxAdminClient(jmxUrl, mbeanName)) {
// Check application status
boolean ready = client.isApplicationReady();
System.out.println("Application ready: " + ready);
boolean embeddedWeb = client.isEmbeddedWebApplication();
System.out.println("Embedded web app: " + embeddedWeb);
// Query configuration
String serverPort = client.getProperty("server.port");
System.out.println("Server port: " + serverPort);
String activeProfiles = client.getProperty("spring.profiles.active");
System.out.println("Active profiles: " + activeProfiles);
// Optionally shutdown (commented out for safety)
// client.shutdownApplication();
} catch (Exception e) {
System.err.println("JMX operation failed: " + e.getMessage());
e.printStackTrace();
}
}
}Monitor application readiness state during and after startup:
package com.example.monitoring;
import org.springframework.boot.admin.SpringApplicationAdminMXBean;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* Monitors application readiness via JMX admin MBean.
*/
@Component
public class ReadinessMonitor {
private final SpringApplicationAdminMXBean adminMBean;
/**
* Constructor injection of admin MBean (if configured as a bean).
*
* @param adminMBean the admin MBean instance
*/
public ReadinessMonitor(SpringApplicationAdminMXBean adminMBean) {
this.adminMBean = adminMBean;
}
/**
* Periodically check application readiness.
* Runs every 30 seconds.
*/
@Scheduled(fixedRate = 30000)
public void checkReadiness() {
boolean ready = adminMBean.isReady();
if (!ready) {
System.err.println("WARNING: Application is not ready");
// Trigger alerts, update health check endpoints, etc.
} else {
System.out.println("Application status: READY");
}
}
/**
* Check readiness on-demand.
*
* @return true if application is ready
*/
public boolean isReady() {
return adminMBean.isReady();
}
}Query and validate environment properties remotely:
package com.example.management;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import java.util.HashMap;
import java.util.Map;
/**
* Inspector for remote configuration properties via JMX.
*/
public class ConfigurationInspector {
private final MBeanServerConnection connection;
private final ObjectName adminMBean;
public ConfigurationInspector(MBeanServerConnection connection, ObjectName adminMBean) {
this.connection = connection;
this.adminMBean = adminMBean;
}
/**
* Retrieve multiple properties in a single call sequence.
*
* @param propertyNames the property names to retrieve
* @return map of property name to value (null values for missing properties)
* @throws Exception if JMX operations fail
*/
public Map<String, String> getProperties(String... propertyNames) throws Exception {
Map<String, String> result = new HashMap<>();
for (String propertyName : propertyNames) {
String value = (String) connection.invoke(
adminMBean,
"getProperty",
new Object[]{propertyName},
new String[]{"java.lang.String"}
);
result.put(propertyName, value);
}
return result;
}
/**
* Dump all common Spring Boot properties.
*
* @return map of property names to values
* @throws Exception if JMX operations fail
*/
public Map<String, String> dumpCommonProperties() throws Exception {
String[] commonProperties = {
"server.port",
"server.address",
"spring.application.name",
"spring.profiles.active",
"spring.profiles.default",
"spring.datasource.url",
"spring.datasource.driver-class-name",
"logging.level.root",
"management.endpoints.web.base-path"
};
return getProperties(commonProperties);
}
/**
* Example usage.
*/
public static void main(String[] args) throws Exception {
// Connect to JMX (connection code omitted for brevity)
MBeanServerConnection connection = null; // Initialize properly
ObjectName adminMBean = new ObjectName(
"org.springframework.boot:type=Admin,name=SpringApplication"
);
ConfigurationInspector inspector = new ConfigurationInspector(connection, adminMBean);
// Inspect configuration
Map<String, String> config = inspector.dumpCommonProperties();
config.forEach((key, value) ->
System.out.println(key + " = " + value)
);
}
}Coordinate graceful shutdown with external systems:
package com.example.management;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
/**
* Coordinates graceful application shutdown via JMX.
*/
public class ShutdownCoordinator {
private final MBeanServerConnection connection;
private final ObjectName adminMBean;
public ShutdownCoordinator(MBeanServerConnection connection, ObjectName adminMBean) {
this.connection = connection;
this.adminMBean = adminMBean;
}
/**
* Initiate graceful shutdown with pre-shutdown tasks.
*
* @throws Exception if shutdown fails
*/
public void gracefulShutdown() throws Exception {
System.out.println("Initiating graceful shutdown...");
// 1. Check if application is ready before shutdown
Boolean ready = (Boolean) connection.getAttribute(adminMBean, "Ready");
if (!ready) {
System.out.println("WARNING: Application not ready, proceeding anyway");
}
// 2. Perform pre-shutdown tasks
System.out.println("Performing pre-shutdown tasks...");
deregisterFromServiceDiscovery();
waitForInFlightRequests();
// 3. Trigger JMX shutdown
System.out.println("Triggering application shutdown via JMX...");
connection.invoke(
adminMBean,
"shutdown",
new Object[]{},
new String[]{}
);
System.out.println("Shutdown initiated successfully");
}
/**
* Deregister from service discovery before shutdown.
*/
private void deregisterFromServiceDiscovery() {
// Implementation: deregister from Consul, Eureka, etc.
System.out.println("Deregistering from service discovery");
}
/**
* Wait for in-flight requests to complete.
*/
private void waitForInFlightRequests() {
// Implementation: wait for active requests to finish
System.out.println("Waiting for in-flight requests (max 30s)");
try {
Thread.sleep(5000); // Wait up to 5 seconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}Secure JMX access in production environments:
# JVM arguments for JMX security
# Add to java command line or JAVA_OPTS
# Enable JMX authentication
-Dcom.sun.management.jmxremote.authenticate=true
-Dcom.sun.management.jmxremote.password.file=/path/to/jmxremote.password
-Dcom.sun.management.jmxremote.access.file=/path/to/jmxremote.access
# Enable SSL for JMX connections
-Dcom.sun.management.jmxremote.ssl=true
-Dcom.sun.management.jmxremote.registry.ssl=true
# SSL keystore configuration
-Djavax.net.ssl.keyStore=/path/to/keystore.jks
-Djavax.net.ssl.keyStorePassword=keystorePassword
-Djavax.net.ssl.trustStore=/path/to/truststore.jks
-Djavax.net.ssl.trustStorePassword=truststorePasswordJMX Password File (jmxremote.password):
# Format: username password
monitorRole monitorPassword
controlRole controlPasswordJMX Access File (jmxremote.access):
# Format: username access_level
monitorRole readonly
controlRole readwriteFile Permissions: Both files must have 600 permissions (read/write for owner only):
chmod 600 jmxremote.password jmxremote.accessConfigure JMX remote access:
# JVM arguments for JMX network access
# JMX port configuration
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.rmi.port=9999
# Disable local-only restriction (allow remote connections)
-Dcom.sun.management.jmxremote.local.only=false
# Set hostname for RMI (important for Docker/cloud environments)
-Djava.rmi.server.hostname=your-hostname-or-ip
# Disable authentication (development only - NOT for production)
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=falseDocker Configuration:
# Dockerfile with JMX enabled
FROM openjdk:17-jdk-slim
# Expose JMX port
EXPOSE 9999
# Set JMX options
ENV JAVA_OPTS="-Dcom.sun.management.jmxremote \
-Dcom.sun.management.jmxremote.port=9999 \
-Dcom.sun.management.jmxremote.rmi.port=9999 \
-Dcom.sun.management.jmxremote.local.only=false \
-Dcom.sun.management.jmxremote.authenticate=false \
-Dcom.sun.management.jmxremote.ssl=false \
-Djava.rmi.server.hostname=localhost"
COPY target/app.jar /app.jar
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"]Combine JMX admin with Spring Boot Actuator health checks:
package com.example.health;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.admin.SpringApplicationAdminMXBean;
import org.springframework.stereotype.Component;
/**
* Health indicator based on JMX admin MBean readiness.
*/
@Component
public class JmxAdminHealthIndicator implements HealthIndicator {
private final SpringApplicationAdminMXBean adminMBean;
public JmxAdminHealthIndicator(SpringApplicationAdminMXBean adminMBean) {
this.adminMBean = adminMBean;
}
@Override
public Health health() {
try {
boolean ready = adminMBean.isReady();
boolean embeddedWeb = adminMBean.isEmbeddedWebApplication();
if (ready) {
return Health.up()
.withDetail("ready", true)
.withDetail("embeddedWeb", embeddedWeb)
.withDetail("jmx", "enabled")
.build();
} else {
return Health.down()
.withDetail("ready", false)
.withDetail("reason", "Application not ready")
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.withException(e)
.build();
}
}
}Use JMX admin for Kubernetes liveness/readiness probes:
package com.example.k8s;
import org.springframework.boot.admin.SpringApplicationAdminMXBean;
import org.springframework.boot.availability.ApplicationAvailability;
import org.springframework.boot.availability.LivenessState;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Kubernetes probe endpoints backed by JMX admin MBean.
*/
@RestController
public class KubernetesProbeController {
private final SpringApplicationAdminMXBean adminMBean;
private final ApplicationAvailability availability;
public KubernetesProbeController(
SpringApplicationAdminMXBean adminMBean,
ApplicationAvailability availability) {
this.adminMBean = adminMBean;
this.availability = availability;
}
/**
* Liveness probe endpoint.
* Returns 200 if application is alive, 503 if broken.
*
* @return "UP" or "DOWN"
*/
@GetMapping("/actuator/health/liveness")
public String liveness() {
LivenessState livenessState = availability.getLivenessState();
return (livenessState == LivenessState.CORRECT) ? "UP" : "DOWN";
}
/**
* Readiness probe endpoint.
* Returns 200 if application is ready, 503 if not ready.
*
* @return "UP" or "DOWN"
*/
@GetMapping("/actuator/health/readiness")
public String readiness() {
boolean ready = adminMBean.isReady();
ReadinessState readinessState = availability.getReadinessState();
return (ready && readinessState == ReadinessState.ACCEPTING_TRAFFIC)
? "UP"
: "DOWN";
}
}Kubernetes Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-boot-app
spec:
template:
spec:
containers:
- name: app
image: my-spring-boot-app:latest
ports:
- containerPort: 8080
name: http
- containerPort: 9999
name: jmx
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
env:
- name: JAVA_OPTS
value: "-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999"Handle JMX connection failures gracefully:
package com.example.management;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* Resilient JMX client with connection retry logic.
*/
public class ResilientJmxClient {
private static final int MAX_RETRIES = 3;
private static final long RETRY_DELAY_MS = 1000;
/**
* Connect to JMX with retry logic.
*
* @param jmxUrl the JMX service URL
* @return the JMX connector
* @throws IOException if all connection attempts fail
*/
public JMXConnector connectWithRetry(String jmxUrl) throws IOException {
IOException lastException = null;
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
JMXServiceURL url = new JMXServiceURL(jmxUrl);
Map<String, Object> env = new HashMap<>();
// Set connection timeout
env.put("jmx.remote.x.client.connection.check.period", 5000L);
env.put("jmx.remote.x.request.timeout", 10000L);
return JMXConnectorFactory.connect(url, env);
} catch (IOException e) {
lastException = e;
System.err.printf("JMX connection attempt %d/%d failed: %s%n",
attempt, MAX_RETRIES, e.getMessage());
if (attempt < MAX_RETRIES) {
try {
Thread.sleep(RETRY_DELAY_MS * attempt); // Exponential backoff
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("Connection interrupted", ie);
}
}
}
}
throw new IOException("Failed to connect after " + MAX_RETRIES + " attempts", lastException);
}
}Handle MBean operation failures:
package com.example.management;
import javax.management.*;
import java.io.IOException;
/**
* Safe wrapper for JMX operations with error handling.
*/
public class SafeJmxOperations {
private final MBeanServerConnection connection;
private final ObjectName adminMBean;
public SafeJmxOperations(MBeanServerConnection connection, ObjectName adminMBean) {
this.connection = connection;
this.adminMBean = adminMBean;
}
/**
* Safely get property with default value on error.
*
* @param propertyName the property name
* @param defaultValue the default value if property not found or error occurs
* @return the property value or default
*/
public String getPropertySafe(String propertyName, String defaultValue) {
try {
String value = (String) connection.invoke(
adminMBean,
"getProperty",
new Object[]{propertyName},
new String[]{"java.lang.String"}
);
return (value != null) ? value : defaultValue;
} catch (InstanceNotFoundException e) {
System.err.println("Admin MBean not found: " + e.getMessage());
return defaultValue;
} catch (MBeanException e) {
System.err.println("MBean operation failed: " + e.getMessage());
return defaultValue;
} catch (ReflectionException e) {
System.err.println("Method invocation failed: " + e.getMessage());
return defaultValue;
} catch (IOException e) {
System.err.println("JMX communication error: " + e.getMessage());
return defaultValue;
}
}
/**
* Safely check readiness with default on error.
*
* @param defaultValue the default value on error
* @return the readiness state or default
*/
public boolean isReadySafe(boolean defaultValue) {
try {
return (Boolean) connection.getAttribute(adminMBean, "Ready");
} catch (Exception e) {
System.err.println("Failed to check readiness: " + e.getMessage());
return defaultValue;
}
}
}All JMX admin components are thread-safe:
SpringApplicationAdminMXBean methods can be called concurrentlySpringApplicationAdminMXBeanRegistrar handles concurrent lifecycle events safelyConcurrency Notes:
shutdown() is idempotent - multiple calls are safe but only first takes effectgetProperty() reads are lock-free and highly concurrentisReady() and isEmbeddedWebApplication() are volatile readsComplete health check system using JMX admin for monitoring:
package com.example.monitoring;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Automated health monitoring via JMX with alerting.
*/
public class JmxHealthMonitor implements AutoCloseable {
private final String jmxUrl;
private final String mbeanName;
private final ScheduledExecutorService scheduler;
private JMXConnector connector;
private MBeanServerConnection connection;
private ObjectName adminMBean;
private HealthStatus lastStatus = HealthStatus.UNKNOWN;
public JmxHealthMonitor(String jmxUrl, String mbeanName) {
this.jmxUrl = jmxUrl;
this.mbeanName = mbeanName;
this.scheduler = Executors.newSingleThreadScheduledExecutor();
}
public void startMonitoring(Duration interval) {
scheduler.scheduleAtFixedRate(
this::performHealthCheck,
0,
interval.toMillis(),
TimeUnit.MILLISECONDS
);
}
private void performHealthCheck() {
try {
ensureConnected();
boolean ready = (Boolean) connection.getAttribute(adminMBean, "Ready");
boolean embeddedWeb = (Boolean) connection.getAttribute(
adminMBean, "EmbeddedWebApplication"
);
String serverPort = (String) connection.invoke(
adminMBean,
"getProperty",
new Object[]{"server.port"},
new String[]{"java.lang.String"}
);
HealthStatus newStatus = ready ? HealthStatus.HEALTHY : HealthStatus.UNHEALTHY;
if (newStatus != lastStatus) {
handleStatusChange(lastStatus, newStatus);
lastStatus = newStatus;
}
reportMetrics(ready, embeddedWeb, serverPort);
} catch (Exception e) {
System.err.println("Health check failed: " + e.getMessage());
handleStatusChange(lastStatus, HealthStatus.UNREACHABLE);
lastStatus = HealthStatus.UNREACHABLE;
}
}
private void ensureConnected() throws Exception {
if (connector == null || connection == null) {
connect();
}
}
private void connect() throws Exception {
JMXServiceURL url = new JMXServiceURL(jmxUrl);
Map<String, Object> env = new HashMap<>();
env.put("jmx.remote.x.request.timeout", 10000L);
this.connector = JMXConnectorFactory.connect(url, env);
this.connection = connector.getMBeanServerConnection();
this.adminMBean = new ObjectName(mbeanName);
}
private void handleStatusChange(HealthStatus oldStatus, HealthStatus newStatus) {
System.out.printf("[%s] Application status changed: %s -> %s%n",
Instant.now(), oldStatus, newStatus);
if (newStatus == HealthStatus.UNHEALTHY || newStatus == HealthStatus.UNREACHABLE) {
sendAlert(newStatus);
}
}
private void sendAlert(HealthStatus status) {
// Send alert to monitoring system (PagerDuty, Slack, etc.)
System.err.println("ALERT: Application health is " + status);
}
private void reportMetrics(boolean ready, boolean embeddedWeb, String port) {
// Report to metrics system (Prometheus, CloudWatch, etc.)
System.out.printf("Metrics: ready=%s, embeddedWeb=%s, port=%s%n",
ready, embeddedWeb, port);
}
@Override
public void close() {
scheduler.shutdown();
if (connector != null) {
try {
connector.close();
} catch (IOException e) {
System.err.println("Error closing JMX connector: " + e.getMessage());
}
}
}
enum HealthStatus {
HEALTHY, UNHEALTHY, UNREACHABLE, UNKNOWN
}
public static void main(String[] args) {
String jmxUrl = "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi";
String mbeanName = "org.springframework.boot:type=Admin,name=SpringApplication";
try (JmxHealthMonitor monitor = new JmxHealthMonitor(jmxUrl, mbeanName)) {
monitor.startMonitoring(Duration.ofSeconds(30));
Thread.sleep(Duration.ofMinutes(10).toMillis());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}JMX-based coordination for zero-downtime deployments:
package com.example.deployment;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* Coordinates rolling deployment using JMX admin.
*/
public class RollingDeploymentCoordinator {
private final List<InstanceConnection> instances;
private final Duration drainTimeout = Duration.ofSeconds(30);
public RollingDeploymentCoordinator(List<String> jmxUrls) {
this.instances = jmxUrls.stream()
.map(url -> new InstanceConnection(url,
"org.springframework.boot:type=Admin,name=SpringApplication"))
.toList();
}
public void performRollingDeployment() {
System.out.println("Starting rolling deployment of " + instances.size() + " instances");
for (int i = 0; i < instances.size(); i++) {
InstanceConnection instance = instances.get(i);
System.out.printf("Deploying instance %d/%d: %s%n",
i + 1, instances.size(), instance.jmxUrl);
try {
shutdownInstance(instance);
waitForNewVersion(instance);
verifyInstance(instance);
} catch (Exception e) {
System.err.println("Deployment failed at instance " + instance.jmxUrl);
rollback(i);
throw new RuntimeException("Deployment failed", e);
}
}
System.out.println("Rolling deployment completed successfully");
}
private void shutdownInstance(InstanceConnection instance) throws Exception {
instance.connect();
// Verify instance is ready before shutdown
Boolean ready = (Boolean) instance.connection.getAttribute(
instance.adminMBean, "Ready"
);
if (!ready) {
throw new IllegalStateException("Instance not ready before shutdown");
}
System.out.println("Triggering graceful shutdown...");
instance.connection.invoke(
instance.adminMBean,
"shutdown",
new Object[]{},
new String[]{}
);
// Wait for shutdown to complete
Thread.sleep(drainTimeout.toMillis());
instance.close();
}
private void waitForNewVersion(InstanceConnection instance) throws Exception {
System.out.println("Waiting for new version to start...");
int maxAttempts = 60; // 5 minutes with 5-second intervals
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
instance.connect();
Boolean ready = (Boolean) instance.connection.getAttribute(
instance.adminMBean, "Ready"
);
if (ready) {
System.out.println("New version is ready");
return;
}
} catch (Exception e) {
// Instance not yet available, retry
}
Thread.sleep(5000);
}
throw new RuntimeException("New version did not become ready in time");
}
private void verifyInstance(InstanceConnection instance) throws Exception {
// Verify critical properties
String version = (String) instance.connection.invoke(
instance.adminMBean,
"getProperty",
new Object[]{"application.version"},
new String[]{"java.lang.String"}
);
System.out.println("Verified new version: " + version);
}
private void rollback(int failedIndex) {
System.err.println("Rolling back deployment");
// Implement rollback logic
}
static class InstanceConnection implements AutoCloseable {
final String jmxUrl;
final String mbeanName;
JMXConnector connector;
MBeanServerConnection connection;
ObjectName adminMBean;
InstanceConnection(String jmxUrl, String mbeanName) {
this.jmxUrl = jmxUrl;
this.mbeanName = mbeanName;
}
void connect() throws Exception {
if (connector != null) {
return;
}
JMXServiceURL url = new JMXServiceURL(jmxUrl);
this.connector = JMXConnectorFactory.connect(url);
this.connection = connector.getMBeanServerConnection();
this.adminMBean = new ObjectName(mbeanName);
}
@Override
public void close() {
if (connector != null) {
try {
connector.close();
} catch (Exception e) {
System.err.println("Error closing connector: " + e.getMessage());
}
}
}
}
}Monitor and reload configuration changes via JMX:
package com.example.config;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
/**
* Monitors configuration changes via JMX and triggers reloads.
*/
public class DynamicConfigMonitor implements AutoCloseable {
private final String jmxUrl;
private final String mbeanName;
private final Map<String, String> lastKnownConfig = new ConcurrentHashMap<>();
private final Map<String, Consumer<String>> changeListeners = new HashMap<>();
private JMXConnector connector;
private MBeanServerConnection connection;
private ObjectName adminMBean;
private volatile boolean running = true;
public DynamicConfigMonitor(String jmxUrl, String mbeanName) {
this.jmxUrl = jmxUrl;
this.mbeanName = mbeanName;
}
public void watchProperty(String propertyName, Consumer<String> onChange) {
changeListeners.put(propertyName, onChange);
}
public void startMonitoring() throws Exception {
connect();
// Initialize with current values
for (String propertyName : changeListeners.keySet()) {
String value = getProperty(propertyName);
lastKnownConfig.put(propertyName, value);
}
// Start monitoring loop
new Thread(this::monitorLoop, "config-monitor").start();
}
private void connect() throws Exception {
JMXServiceURL url = new JMXServiceURL(jmxUrl);
this.connector = JMXConnectorFactory.connect(url);
this.connection = connector.getMBeanServerConnection();
this.adminMBean = new ObjectName(mbeanName);
}
private void monitorLoop() {
while (running) {
try {
checkForChanges();
Thread.sleep(5000); // Check every 5 seconds
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
System.err.println("Error checking config: " + e.getMessage());
}
}
}
private void checkForChanges() throws Exception {
for (Map.Entry<String, Consumer<String>> entry : changeListeners.entrySet()) {
String propertyName = entry.getKey();
String currentValue = getProperty(propertyName);
String lastValue = lastKnownConfig.get(propertyName);
if (!equals(currentValue, lastValue)) {
System.out.printf("Property '%s' changed: %s -> %s%n",
propertyName, lastValue, currentValue);
lastKnownConfig.put(propertyName, currentValue);
entry.getValue().accept(currentValue);
}
}
}
private String getProperty(String propertyName) throws Exception {
return (String) connection.invoke(
adminMBean,
"getProperty",
new Object[]{propertyName},
new String[]{"java.lang.String"}
);
}
private boolean equals(String a, String b) {
return (a == null) ? (b == null) : a.equals(b);
}
@Override
public void close() {
running = false;
if (connector != null) {
try {
connector.close();
} catch (Exception e) {
System.err.println("Error closing connector: " + e.getMessage());
}
}
}
public static void main(String[] args) throws Exception {
String jmxUrl = "service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi";
String mbeanName = "org.springframework.boot:type=Admin,name=SpringApplication";
try (DynamicConfigMonitor monitor = new DynamicConfigMonitor(jmxUrl, mbeanName)) {
monitor.watchProperty("logging.level.root", newLevel ->
System.out.println("Logging level changed to: " + newLevel)
);
monitor.watchProperty("server.port", newPort ->
System.out.println("Server port changed to: " + newPort)
);
monitor.startMonitoring();
Thread.sleep(Duration.ofMinutes(10).toMillis());
}
}
}Centralized management of multiple application instances:
package com.example.dashboard;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
/**
* Dashboard for managing multiple application instances via JMX.
*/
public class MultiInstanceDashboard {
private final List<InstanceManager> instances = new ArrayList<>();
private final Map<String, InstanceStatus> statusCache = new ConcurrentHashMap<>();
public void addInstance(String instanceId, String jmxUrl) {
InstanceManager manager = new InstanceManager(instanceId, jmxUrl,
"org.springframework.boot:type=Admin,name=SpringApplication");
instances.add(manager);
}
public void refreshAllStatuses() {
System.out.println("Refreshing status for " + instances.size() + " instances...");
List<CompletableFuture<Void>> futures = instances.stream()
.map(instance -> CompletableFuture.runAsync(() -> refreshInstance(instance)))
.toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
printDashboard();
}
private void refreshInstance(InstanceManager instance) {
try {
instance.connect();
Boolean ready = (Boolean) instance.connection.getAttribute(
instance.adminMBean, "Ready"
);
Boolean embeddedWeb = (Boolean) instance.connection.getAttribute(
instance.adminMBean, "EmbeddedWebApplication"
);
String version = instance.getProperty("application.version");
String activeProfiles = instance.getProperty("spring.profiles.active");
String serverPort = instance.getProperty("server.port");
InstanceStatus status = new InstanceStatus(
instance.instanceId,
ready,
embeddedWeb,
version,
activeProfiles,
serverPort,
Instant.now()
);
statusCache.put(instance.instanceId, status);
} catch (Exception e) {
System.err.printf("Failed to refresh %s: %s%n",
instance.instanceId, e.getMessage());
statusCache.put(instance.instanceId, InstanceStatus.failed(instance.instanceId));
}
}
private void printDashboard() {
System.out.println("\n=== Application Instance Dashboard ===");
System.out.printf("%-20s %-10s %-10s %-15s %-20s %-10s%n",
"Instance", "Ready", "Web", "Version", "Profiles", "Port");
System.out.println("-".repeat(95));
statusCache.values().forEach(status -> {
System.out.printf("%-20s %-10s %-10s %-15s %-20s %-10s%n",
status.instanceId,
status.ready != null ? status.ready : "ERROR",
status.embeddedWeb != null ? status.embeddedWeb : "ERROR",
status.version != null ? status.version : "N/A",
status.activeProfiles != null ? status.activeProfiles : "N/A",
status.serverPort != null ? status.serverPort : "N/A"
);
});
}
public void shutdownAll() {
System.out.println("Shutting down all instances...");
instances.forEach(instance -> {
try {
instance.shutdown();
System.out.println("Shutdown initiated for " + instance.instanceId);
} catch (Exception e) {
System.err.println("Failed to shutdown " + instance.instanceId);
}
});
}
static class InstanceManager {
final String instanceId;
final String jmxUrl;
final String mbeanName;
JMXConnector connector;
MBeanServerConnection connection;
ObjectName adminMBean;
InstanceManager(String instanceId, String jmxUrl, String mbeanName) {
this.instanceId = instanceId;
this.jmxUrl = jmxUrl;
this.mbeanName = mbeanName;
}
void connect() throws Exception {
if (connector == null) {
JMXServiceURL url = new JMXServiceURL(jmxUrl);
this.connector = JMXConnectorFactory.connect(url);
this.connection = connector.getMBeanServerConnection();
this.adminMBean = new ObjectName(mbeanName);
}
}
String getProperty(String propertyName) throws Exception {
return (String) connection.invoke(
adminMBean,
"getProperty",
new Object[]{propertyName},
new String[]{"java.lang.String"}
);
}
void shutdown() throws Exception {
connect();
connection.invoke(adminMBean, "shutdown", new Object[]{}, new String[]{});
}
}
record InstanceStatus(
String instanceId,
Boolean ready,
Boolean embeddedWeb,
String version,
String activeProfiles,
String serverPort,
Instant lastCheck
) {
static InstanceStatus failed(String instanceId) {
return new InstanceStatus(instanceId, null, null, null, null, null, Instant.now());
}
}
}Implement circuit breaker pattern using JMX health state:
package com.example.resilience;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* Circuit breaker that monitors application health via JMX.
*/
public class JmxCircuitBreaker implements AutoCloseable {
private final String jmxUrl;
private final String mbeanName;
private final int failureThreshold;
private final Duration timeout;
private final AtomicInteger consecutiveFailures = new AtomicInteger(0);
private final AtomicReference<CircuitState> state = new AtomicReference<>(CircuitState.CLOSED);
private volatile Instant openedAt;
private JMXConnector connector;
private MBeanServerConnection connection;
private ObjectName adminMBean;
public JmxCircuitBreaker(String jmxUrl, String mbeanName,
int failureThreshold, Duration timeout) {
this.jmxUrl = jmxUrl;
this.mbeanName = mbeanName;
this.failureThreshold = failureThreshold;
this.timeout = timeout;
}
public boolean allowRequest() {
CircuitState currentState = state.get();
switch (currentState) {
case CLOSED:
return true;
case OPEN:
if (shouldAttemptReset()) {
System.out.println("Circuit breaker entering HALF_OPEN state");
state.set(CircuitState.HALF_OPEN);
return true;
}
return false;
case HALF_OPEN:
return true;
default:
return false;
}
}
public void recordSuccess() {
consecutiveFailures.set(0);
if (state.get() == CircuitState.HALF_OPEN) {
System.out.println("Circuit breaker closing after successful request");
state.set(CircuitState.CLOSED);
}
}
public void recordFailure() {
int failures = consecutiveFailures.incrementAndGet();
if (failures >= failureThreshold && state.compareAndSet(CircuitState.CLOSED, CircuitState.OPEN)) {
openedAt = Instant.now();
System.err.printf("Circuit breaker OPENED after %d failures%n", failures);
}
if (state.get() == CircuitState.HALF_OPEN) {
System.err.println("Circuit breaker reopening after HALF_OPEN failure");
state.set(CircuitState.OPEN);
openedAt = Instant.now();
}
}
public boolean checkHealth() {
if (!allowRequest()) {
return false;
}
try {
connect();
Boolean ready = (Boolean) connection.getAttribute(adminMBean, "Ready");
if (ready) {
recordSuccess();
return true;
} else {
recordFailure();
return false;
}
} catch (Exception e) {
System.err.println("Health check failed: " + e.getMessage());
recordFailure();
return false;
}
}
private boolean shouldAttemptReset() {
return openedAt != null &&
Duration.between(openedAt, Instant.now()).compareTo(timeout) > 0;
}
private void connect() throws Exception {
if (connector == null) {
JMXServiceURL url = new JMXServiceURL(jmxUrl);
this.connector = JMXConnectorFactory.connect(url);
this.connection = connector.getMBeanServerConnection();
this.adminMBean = new ObjectName(mbeanName);
}
}
@Override
public void close() {
if (connector != null) {
try {
connector.close();
} catch (Exception e) {
System.err.println("Error closing connector: " + e.getMessage());
}
}
}
enum CircuitState {
CLOSED, OPEN, HALF_OPEN
}
}java.rmi.server.hostname for remote accessisReady() for health checks and load balancer integrationshutdown() instead of kill signals when possibleProblem: JMX works locally but fails for remote connections
Error:
java.rmi.ConnectException: Connection refused to host: 127.0.0.1Solution:
# Set the public hostname/IP for RMI
-Djava.rmi.server.hostname=<public-ip-or-hostname>
# Ensure both ports match
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.rmi.port=9999Rationale: RMI by default binds to localhost. Remote clients receive localhost address in RMI stub and fail to connect. Setting hostname ensures clients receive the correct address.
Problem: Not closing JMX connectors causes resource leaks
Error:
java.io.IOException: Too many open filesSolution:
// Always use try-with-resources
try (JMXConnector connector = JMXConnectorFactory.connect(url)) {
MBeanServerConnection connection = connector.getMBeanServerConnection();
// Use connection
} // Automatically closed
// Or manual cleanup
JMXConnector connector = null;
try {
connector = JMXConnectorFactory.connect(url);
// Use connector
} finally {
if (connector != null) {
connector.close();
}
}Rationale: Each JMX connection opens network sockets and threads. Failure to close connectors exhausts file descriptors and causes application instability.
Problem: JMX operations hang indefinitely when application unresponsive
Error: Application hangs with no error message
Solution:
Map<String, Object> env = new HashMap<>();
env.put("jmx.remote.x.request.timeout", 10000L); // 10 seconds
env.put("jmx.remote.x.client.connection.check.period", 5000L);
JMXConnector connector = JMXConnectorFactory.connect(url, env);Rationale: Without timeouts, monitoring tools can hang forever waiting for unresponsive applications, causing cascading failures in monitoring infrastructure.
Problem: Exposing JMX without authentication allows unauthorized access
Error: Security breach, unauthorized shutdown or configuration changes
Solution:
# Enable authentication
-Dcom.sun.management.jmxremote.authenticate=true
-Dcom.sun.management.jmxremote.password.file=/path/to/jmxremote.password
-Dcom.sun.management.jmxremote.access.file=/path/to/jmxremote.access
# Enable SSL
-Dcom.sun.management.jmxremote.ssl=true
-Dcom.sun.management.jmxremote.registry.ssl=trueRationale: Unauthenticated JMX allows anyone with network access to inspect configuration, shutdown the application, or modify runtime behavior.
Problem: Using wrong ObjectName string causes MBean not found
Error:
javax.management.InstanceNotFoundException: org.springframework.boot:type=AdminSolution:
// Correct ObjectName (must match exactly)
String correctName = "org.springframework.boot:type=Admin,name=SpringApplication";
ObjectName mbean = new ObjectName(correctName);
// Verify MBean exists
if (!connection.isRegistered(mbean)) {
System.err.println("MBean not registered. Check spring.application.admin.enabled=true");
}Rationale: ObjectName must exactly match the registered MBean name including all properties. Even minor differences cause lookup failures.
Problem: Shutting down application without checking readiness state
Error: In-flight requests aborted, data loss
Solution:
// Check readiness before shutdown
Boolean ready = (Boolean) connection.getAttribute(adminMBean, "Ready");
if (!ready) {
System.out.println("WARNING: Application not ready, shutting down anyway");
}
// Trigger shutdown
connection.invoke(adminMBean, "shutdown", new Object[]{}, new String[]{});
// Wait for graceful shutdown
Thread.sleep(30000); // Give 30 seconds for cleanupRationale: Shutting down during startup or while not ready can cause incomplete initialization cleanup or data corruption.
Problem: JMX port open but RMI callback port blocked by firewall
Error:
java.rmi.ConnectException: Connection refusedSolution:
# Use same port for both JMX and RMI to simplify firewall rules
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.rmi.port=9999
# Open firewall for port 9999 TCPRationale: JMX uses two ports - one for initial connection and one for RMI callbacks. Using the same port for both simplifies firewall configuration and prevents callback failures.
Problem: Not handling connection failures gracefully
Error: Monitoring system reports false positives, alerts on transient issues
Solution:
public boolean checkHealthWithRetry(int maxRetries) {
IOException lastException = null;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
Boolean ready = (Boolean) connection.getAttribute(adminMBean, "Ready");
return ready;
} catch (IOException e) {
lastException = e;
if (attempt < maxRetries) {
Thread.sleep(1000 * attempt); // Exponential backoff
}
} catch (Exception e) {
throw new RuntimeException("Unexpected error", e);
}
}
throw new IOException("Failed after " + maxRetries + " attempts", lastException);
}Rationale: Network issues, GC pauses, and application restarts cause transient connection failures. Retry logic with exponential backoff prevents false alerts.
Problem: JMX doesn't work in containerized environments
Error:
Connection refused to host: <container-id>Solution:
# Dockerfile
ENV JAVA_OPTS="-Djava.rmi.server.hostname=0.0.0.0 \
-Dcom.sun.management.jmxremote.port=9999 \
-Dcom.sun.management.jmxremote.rmi.port=9999 \
-Dcom.sun.management.jmxremote.local.only=false"
# Kubernetes Service
apiVersion: v1
kind: Service
metadata:
name: app-jmx
spec:
type: NodePort
ports:
- port: 9999
targetPort: 9999
nodePort: 30999Rationale: Containers use internal hostnames that aren't routable from outside. Setting hostname to 0.0.0.0 and proper port forwarding enables external JMX access.
Problem: Assuming admin MBean is always available
Error:
javax.management.InstanceNotFoundExceptionSolution:
public boolean isMBeanAvailable() {
try {
return connection.isRegistered(adminMBean);
} catch (IOException e) {
return false;
}
}
public void waitForMBean(Duration timeout) throws Exception {
Instant deadline = Instant.now().plus(timeout);
while (Instant.now().isBefore(deadline)) {
if (isMBeanAvailable()) {
return;
}
Thread.sleep(1000);
}
throw new TimeoutException("MBean not available after " + timeout);
}Rationale: Admin MBean is only registered after application context initialization. Attempting to access it too early during startup causes errors. Always check availability first.
Symptoms: JMX connection works locally but fails remotely
Solutions:
# Set the RMI hostname
-Djava.rmi.server.hostname=<public-ip-or-hostname>
# Ensure both ports are the same
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.rmi.port=9999
# Disable local-only mode
-Dcom.sun.management.jmxremote.local.only=falseSolution: Use host network or port forwarding
# Run with host network
docker run --network host myapp
# Or map JMX ports
docker run -p 9999:9999 myappSymptom: InstanceNotFoundException when accessing admin MBean
Causes and Solutions:
spring.application.admin.enabled=true// Core JMX admin classes
import org.springframework.boot.admin.SpringApplicationAdminMXBean;
import org.springframework.boot.admin.SpringApplicationAdminMXBeanRegistrar;
// JMX classes
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.MalformedObjectNameException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.ReflectionException;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
// Spring Framework
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.EnvironmentAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.DisposableBean;