June 24, 2016

Overriding Default Spring MBeanExporter Behavior

There are a variety of approaches to integrating JMX with Spring. I have found the easiest way is to introduce an @EnableMBeanExport annotation in a Spring configuration class. It will take care of registering all managed resources in your Spring context with the local JMX Agent. This convention is typical for consumers who do not need any applied customization or overrides to JMX registration, configuration, or behavior. However, interesting but also problematic circumstances arise when managed bean classes extend a common implementation base class or when you want explicit control over which managed beans are registered within your application. The purpose of this post is to present a recent use case scenario and the applied implementation.

We recently introduced a shared library which provides a clean and consolidated implementation approach for both managing in-memory caches and externalizing cache service operations within JMX. The following class diagram captures the base hierarchy and structure:

image

A consuming application incorporated the shared library within their project and refactored a legacy Country Code Service to extend the AbstractCacheService and renamed it CountryCacheService.  An instance of the GenericCacheStatusMBean, named “countryCacheMBean” was added to their Spring context. The cacheService property on the GenericCacheStatusMBean references the new CountryCacheService:

<bean id="countryCacheService" class="CountryCacheService" 
   p:expirationTime="{cache.country.expiration}"/>
<bean id="countryCacheMBean"
      class="com.common.cache.impl.GenericCacheStatusMBean">
      <property name="cacheService" ref="countryCacheService"/>
      <property name="appName" ref="applicationName"/>
      <property name="cacheBeanName"        
      value="#{applicationName}.cache:name=countryCacheMBean"/
</bean>

However upon running a JMX console in a deployed environment the application group noted two unexpected MBean related issues:

  • The JMX registered, “countryCacheMBean”, has an incorrect full qualified path and type name :com.common.caching.impl:name=countryCacheMBean,type=GenericCacheStatusMBean. If other caches were introduced within the application, they would all share the same type and qualified package name. This naming convention could introduce confusion to Application Admins and IT Operations Team members who monitor the application.  

  • Other Managed Beans introduced by internal and external third party library dependencies were also displayed in the JMX console.

Digging into the @EnabledMBeanExport annotation class itself and traversing back a bit through the Spring framework code, the underlying reason for each bullet point item result can be easily explained. The EnableMBeanExport imports an MBeanExportConfiguration which in turn registers an AnnotationMBeanExporter. The AnnotationMBeanExporter is a framework provided subclass extension of the MBeanExporter and a convenience class which establishes implementation strategies for both autodetection and registration of Managed Beans. The auto-detection strategy is set to a MetadataMBeanInfoAssembler class, which scans the entire Spring Context for any beans with source level JMX annotations. The MetadataNamingStrategy class is set as the AnnotationMBeanExporter’s naming policy registrar. It will either apply the objectName property if it is set on the bean’s ManagedResource annotation OR use a string which appends the class name to the full package path as the name to register with the JMX Agent.

With an understanding of what was going on underneath the Spring JMX Integration covers, we needed to find a way to override the default autodetection and naming policy for consumers of this new shared library. Here are the steps taken along with the actual code applied:

Step #1: Create a subclass of the MetadataMBeanInfoAssembler and override the includeBean method so that only GenericCacheStatusMBeans are JMX auto-detected.

public class CacheMetadataMBeanInfoAssembler extends MetadataMBeanInfoAssembler
      
    @Override
    public boolean includeBean(Class<?> beanClass, String beanName) {
        return GenericCacheStatusMBean.class.isAssignableFrom(beanClass) &&          
            super.includeBean(beanClass, beanName);
     }

Step #2: Remove the @EnableMBeanExport from our “cache” @Configuration class and create our own MBeanExporter sub-class, CacheMBeanExporter. The CacheMBeanExporter was later added as a @Bean definition in a @Configuration class.

public class CacheMBeanExporter extends MBeanExporter {
...
}

Step #3: setAutodetectMode(MBeanExporter.AUTODETECT_ASSEMBLER) within the CacheMBeanExporter constructor. Register/set the CacheMetadataMBeanInfoAssembler as the assembler on this subclass. During MBeanExporter.registerBeans() processing if the autodetectMode is set to MBeanExporter.AUTODETECTASSEMBLER, a call to the MBeanExporter.autodetectMBeans(), will perform an assembler callback to the includeBean method. This callback will be to our CacheMetadataMBeanInfoAssembler.

/**
 * Default ctor.
 */
public CacheMBeanExporter() {
    setAutodetectMode(MBeanExporter.AUTODETECT_ASSEMBLER);
    setAssembler(new CacheMetadataMBeanInfoAssembler(new AnnotationJmxAttributeSource()));
}

Step #4: GenericCacheStatusMBean should also implement the SelfNaming interface. The MBeanExporter.getObjectName is invoked for the purposes of registering the bean name with JMX. By implementing the SelfNaming interface, the GenericCacheStatusMBean can derive the appropriate JMX name to register the bean. We just reused/returned the pre-existing cacheBeanName property!

public class GenericCacheStatusMBean implements ICacheStatusMBean, SelfNaming

    @Override
    public ObjectName getObjectName() throws MalformedObjectNameException {
    return new ObjectName(cacheBeanName); 
}

Brett Edminster

Solutions Architect