Initial commit
This commit is contained in:
582
skills/spring-boot-actuator/references/jmx.md
Normal file
582
skills/spring-boot-actuator/references/jmx.md
Normal file
@@ -0,0 +1,582 @@
|
||||
# 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
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
jmx:
|
||||
enabled: true
|
||||
default-domain: com.example.myapp
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
jmx:
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
jmx:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
### Custom MBean Server Configuration
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```java
|
||||
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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```yaml
|
||||
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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```java
|
||||
@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
|
||||
|
||||
```yaml
|
||||
# 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
|
||||
|
||||
```java
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user