For over a decade and a half, Java Management Extensions (JMX) has been the standard means of monitoring and managing Java applications. By exposing managed components known as MBeans (managed beans), an external JMX client can manage an application by invoking operations, inspecting properties, and monitoring events from MBeans.
JMX is automatically enabled by default in a Spring Boot application. As a result, all of the Actuator endpoints are exposed as MBeans. And it sets us up nicely to expose any other bean in the Spring application context as an MBean. We’ll start exploring Spring and JMX by looking at how Actuator endpoints are exposed as MBeans.
Take a look back at table 16.1. All of the Actuator endpoints listed there, except for /heapdump, are exposed as MBeans. You can use any JMX client you wish to connect with Actuator endpoint MBeans. Using JConsole, which comes with the Java Development Kit, you’ll find Actuator MBeans listed under the org.springframework.boot domain, as shown in figure 18.1.
One thing that’s nice about Actuator MBean endpoints is that they’re all exposed by default. There’s no need to explicitly include any of them, as you had to do with HTTP. You can, however, choose to narrow down the choices by setting management.endpoints.jmx.exposure.include and management.endpoints.jmx.exposure.exclude. For example, to limit Actuator endpoint MBeans to only the /health, /info, /bean, and /conditions endpoints, set management.endpoints.jmx.exposure.include like this:
management: endpoints: jmx: exposure: include: health,info,bean,conditions
Or, if there are only a few you want to exclude, you can set management.endpoints.jmx.exposure.exclude like this:
management: endpoints: jmx: exposure: exclude: env,metrics
Here, you use management.endpoints.jmx.exposure.exclude to exclude the /env and /metrics endpoints. All other Actuator endpoints will still be exposed as MBeans.
To invoke the managed operations on one of the Actuator MBeans in JConsole, expand the endpoint MBean in the left-hand tree, and then select the desired operation under Operations.
For example, if you’d like to inspect the logging levels for the tacos.ingredients package, expand the Loggers MBean and click on the operation named loggerLevels, as shown in figure 18.2. In the form at the top right, fill in the Name field with the package name (tacos.ingredients), and then click the loggerLevels button.
After you click the loggerLevels button, a dialog box will pop up showing you the response from the /loggers endpoint MBean. It might look a little like figure 18.3.
Although the JConsole UI is a bit clumsy to work with, you should be able to get the hang of it and use it to explore any Actuator endpoint in much the same way. If you don’t like JConsole, that’s fine—there are plenty of other JMX clients to choose from.
Spring makes it easy to expose any bean you want as a JMX MBean. All you must do is annotate the bean class with @ManagedResource and then annotate any methods or properties with @ManagedOperation or @ManagedAttribute. Spring will take care of the rest.
For example, suppose you want to provide an MBean that tracks how many tacos have been ordered through Taco Cloud. You can define a service bean that keeps a running count of how many tacos have been created. The following listing shows what such a service might look like.
package tacos.tacos; import java.util.concurrent.atomic.AtomicLong; import org.springframework.data.rest.core.event.AbstractRepositoryEventListener; import org.springframework.jmx.export.annotation.ManagedAttribute; import org.springframework.jmx.export.annotation.ManagedOperation; import org.springframework.jmx.export.annotation.ManagedResource; import org.springframework.stereotype.Service; @Service @ManagedResource public class TacoCounter extends AbstractRepositoryEventListener<Taco> { private AtomicLong counter; public TacoCounter(TacoRepository tacoRepo) { long initialCount = tacoRepo.count(); this.counter = new AtomicLong(initialCount); } @Override protected void onAfterCreate(Taco entity) { counter.incrementAndGet(); } @ManagedAttribute public long getTacoCount() { return counter.get(); } @ManagedOperation public long increment(long delta) { return counter.addAndGet(delta); } }
The TacoCounter class is annotated with @Service so that it will be picked up by component scanning, and an instance will be registered as a bean in the Spring application context. But it’s also annotated with @ManagedResource to indicate that this bean should also be an MBean. As an MBean, it will expose one attribute and one operation. The getTacoCount() method is annotated with @ManagedAttribute so that it will be exposed as an MBean attribute, while the increment() method is annotated with @ManagedOperation, exposing it as an MBean operation.
Figure 18.4 shows how the TacoCounter MBean appears in JConsole.
TacoCounter has another trick up its sleeve, although it has nothing to do with JMX. Because it extends AbstractRepositoryEventListener, it will be notified of any persistence events when a Taco is saved through TacoRepository. In this particular case, the onAfterCreate() method will be invoked anytime a new Taco object is created and saved, and it will increment the counter by one. But AbstractRepositoryEventListener also offers several methods for handling events both before and after objects are created, saved, or deleted.
Working with MBean operations and attributes is largely a pull operation. That is, even if the value of an MBean attribute changes, you won’t know until you view the attribute through a JMX client. Let’s turn the tables and see how you can push notifications from an MBean to a JMX client.
MBeans can push notifications to interested JMX clients with Spring’s NotificationPublisher. NotificationPublisher has a single sendNotification() method that, when given a Notification object, publishes the notification to any JMX clients that have subscribed to the MBean.
For an MBean to be able to publish notifications, it must implement the NotificationPublisherAware interface, which requires that a setNotificationPublisher() method be implemented. For example, suppose you want to publish a notification every for every 100 tacos that are created. You can change the TacoCounter class so that it implements NotificationPublisherAware and uses the injected NotificationPublisher to send notifications for every 100 tacos that are created. The following listing shows the changes that must be made to TacoCounter to enable such notifications.
@Service @ManagedResource public class TacoCounter extends AbstractRepositoryEventListener<Taco> implements NotificationPublisherAware { private AtomicLong counter; private NotificationPublisher np; ... @Override public void setNotificationPublisher(NotificationPublisher np) { this.np = np; } ... @ManagedOperation public long increment(long delta) { long before = counter.get(); long after = counter.addAndGet(delta); if ((after / 100) > (before / 100)) { Notification notification = new Notification( "taco.count", this, before, after + "th taco created!"); np.sendNotification(notification); } return after; } }
In the JMX client, you’ll need to subscribe to the TacoCounter MBean to receive notifications. Then, as tacos are created, the client will receive notifications for each century count. Figure 18.5 shows how the notifications may appear in JConsole.
Notifications are a great way for an application to actively send data and alerts to a monitoring client without requiring the client to poll managed attributes or invoke managed operations.