How to make custom Spring MBeanExporter use the @Managed... annotations on a candidate class

4.3k views Asked by At

I've written a custom Spring MBeanExporter that takes a collection of pre-created objects and creates the mbeans for them. It apparently uses the "default" strategy for determining attributes and operations, just taking the existing properties and operations of the associated class.

I just have an "afterPropertiesSet()" method that does some work, populates the base "beans" list, and then calls its superclass method. This works reasonably well.

I now want to see if I can get it to utilize any "@Managed..." annotations on the associated class. For my first try, I simply put the expected annotations on the associated class without changing how the "beans" list is populated and processed. Unfortunately, this didn't work. I added several "description" attributes to the class, attributes, and operations, but these didn't show up in VisualVM.

Is there something I can do to make the MBeanExporter mechanism use the @Managed... annotations on the associated class?

Note that my current class extends MBeanExporter. If I change it to extend AnnotationMBeanExporter, then it fails on the classes that do NOT have @Managed... annotations. I need something that defaults to what "MBeanExporter" does, unless it finds @Managed... annotations in a class.

I guess I need to show some code, but this will be mostly just pseudocode.

My MBeanExporter looks something like this:

public class MyMBeanExporter extends MBeanExporter {

@Override
public void afterPropertiesSet() {
    // Do some pre-work to determine the list of beans to use.
    Map<String, Object> beans   = new HashMap<String, Object>();
    ... stuff
    setBeans(beans);

    // Now let the superclass create mbeans for all of the beans we found.
    super.afterPropertiesSet();
}

One of the beans that gets put into the list has a class that looks like this:

@ManagedResource(objectName = ":name=fancystuff", description = "This is some stuff")
public class Stuff {
    private int howMuchStuff;

    @ManagedAttribute(description = "This tells us how much stuff we have")
    public int getHowMuchStuff() { return howMuchStuff; }

    public void setHowMuchStuff(int howMuchStuff) { this.howMuchStuff = howMuchStuff; }

    @ManagedOperation(description = "Use this to add more stuff")
    public void makeSomeMoreStuff(int stuffToAdd) {
        howMuchStuff    += stuffToAdd;
    }
}

When this gets rendered in VisualVM, none of the metadata described in the @Managed... annotations is used. I can tell this for certain because the resulting ObjectName isn't the overriding value that I specified in the "@ManagedResource" annotation.

If I instead change the base class to "AnnotationMBeanExporter", then the bean associated with this class does get the metadata that I specified in the annotations. However, all the other beans that are associated with classes that do NOT have a "@ManagedResource" annotation all fail with exceptions like this:

InvalidMetadataException: No ManagedResource attribute found for class: class ...

My temporary workaround is simply to define my MBeanExporter subclass so it can behave as either a plain MBeanExporter or an AnnotationMBeanExporter, depending on a constructor flag. Then, I can simply define two instances of it, one with the flag, and one without, and with a different set of paths to process. This works.

My next thing to try is to have a single "fake" MBeanExporter that internally manages an MBeanExporter and an AnnotationMBeanExporter. It will build the initial beans list, but then process each one, looking at the class associated with the bean to see if the @ManagedResource annotation is present. That will indicate whether it will end up in the list of beans to be processed by the AnnotationMBeanExporter or the regular one.

Update: I've hit a problem with this strategy, as I can't just create a raw AnnotationMBeanExporter and call "afterPropertiesSet()" on it. It fails with:

MBeanExportException: Cannot autodetect MBeans if not running in a BeanFactory
2

There are 2 answers

0
David M. Karr On

Ok, I think I have something that works now. I'm not sure whether this is all correct. As Martin described, I created new "assembler" and "naming strategy" classes that combine the behavior of the "default" and "annotation" variations, such that if the class in scope has a "ManagedResource" annotation, then it uses the behavior from MetadataMBeanInfoAssembler & MetadataNamingStrategy, otherwise SimpleReflectiveMBeanInfoAssembler & KeyNamingStrategy.

The implementation of the "naming strategy" subclass was somewhat simple, but the "assembler" implementation was a little more annoying because I simply had to copy the body of all the methods in MetadataMBeanInfoAssembler that were overridden and insert them in a wrapper check for the class annotation. There was no way to simply have an embedded "MetadataMBeanInfoAssembler" and call methods on it.

Here is my "naming strategy" subclass (I could use some hints on how to get code samples to cleanly display in here):

package <packagepath>.mbeans;

import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.jmx.export.metadata.JmxAttributeSource;
import org.springframework.jmx.export.naming.KeyNamingStrategy;
import org.springframework.jmx.export.naming.MetadataNamingStrategy;
import org.springframework.jmx.export.naming.ObjectNamingStrategy;

public class MetadataOrKeyNamingStrategy implements ObjectNamingStrategy, InitializingBean {
private MetadataNamingStrategy  metadataNamingStrategy  = new MetadataNamingStrategy();
private KeyNamingStrategy       keyNamingStrategy       = new KeyNamingStrategy();

public MetadataOrKeyNamingStrategy(JmxAttributeSource attributeSource) {
    metadataNamingStrategy.setAttributeSource(attributeSource);
}

@Override
public void afterPropertiesSet() throws Exception {
    metadataNamingStrategy.afterPropertiesSet();
    keyNamingStrategy.afterPropertiesSet();
}

/**
 * Specify the default domain to be used for generating ObjectNames
 * when no source-level metadata has been specified.
 * <p>The default is to use the domain specified in the bean name
 * (if the bean name follows the JMX ObjectName syntax); else,
 * the package name of the managed bean class.
 */
public void setDefaultDomain(String defaultDomain) {
    metadataNamingStrategy.setDefaultDomain(defaultDomain);
}

@Override
public ObjectName getObjectName(Object managedBean, String beanKey)
    throws MalformedObjectNameException {
    Class<?> managedClass = AopUtils.getTargetClass(managedBean);
    if (managedClass.getAnnotation(ManagedResource.class) != null)
    return metadataNamingStrategy.getObjectName(managedBean, beanKey);
    return keyNamingStrategy.getObjectName(managedBean, beanKey);
}
}

Here is the "assembler" subclass:

package <packagepath>.mbeans;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;

import javax.management.JMException;

import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.jmx.export.assembler.AbstractConfigurableMBeanInfoAssembler;
import org.springframework.jmx.export.metadata.JmxAttributeSource;
import org.springframework.jmx.export.metadata.ManagedAttribute;
import org.springframework.jmx.export.metadata.ManagedMetric;
import org.springframework.jmx.export.metadata.ManagedOperation;
import org.springframework.util.StringUtils;

public class MetadataOrSimpleReflectiveMBeanInfoAssembler extends AbstractConfigurableMBeanInfoAssembler {

private JmxAttributeSource                  attributeSource;

public MetadataOrSimpleReflectiveMBeanInfoAssembler() { }

public MetadataOrSimpleReflectiveMBeanInfoAssembler(JmxAttributeSource attributeSource) {
    this.attributeSource    = attributeSource;
}

@Override
protected boolean includeReadAttribute(Method method, String beanKey) {
    if (method.getDeclaringClass().getAnnotation(ManagedResource.class) != null)
    return hasManagedAttribute(method) || hasManagedMetric(method);
    return true;
}

@Override
protected boolean includeWriteAttribute(Method method, String beanKey) {
    if (method.getDeclaringClass().getAnnotation(ManagedResource.class) != null)
    return hasManagedAttribute(method);
    return true;
}

@Override
protected boolean includeOperation(Method method, String beanKey) {
    if (method.getDeclaringClass().getAnnotation(ManagedResource.class) != null) {
    PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method);
    if (pd != null) {
        if(hasManagedAttribute(method)) {
        return true;
        }
    }
    return hasManagedOperation(method);
    }
    return true;
}

/**
 * Checks to see if the given Method has the {@code ManagedAttribute} attribute.
 */
private boolean hasManagedAttribute(Method method) {
    return (attributeSource.getManagedAttribute(method) != null);
}

/**
 * Checks to see if the given Method has the {@code ManagedMetric} attribute.
 */
private boolean hasManagedMetric(Method method) {
    return (this.attributeSource.getManagedMetric(method) != null);
}

/**
 * Checks to see if the given Method has the {@code ManagedOperation} attribute.
 * @param method the method to check
 */
private boolean hasManagedOperation(Method method) {
    return (this.attributeSource.getManagedOperation(method) != null);
}

@Override
protected void checkManagedBean(Object managedBean) throws IllegalArgumentException {
    if (managedBean.getClass().getAnnotation(ManagedResource.class) != null) {
    if (AopUtils.isJdkDynamicProxy(managedBean)) {
        throw new IllegalArgumentException(
            "MetadataMBeanInfoAssembler does not support JDK dynamic proxies - " +
            "export the target beans directly or use CGLIB proxies instead");
    }
    }
}

@Override
protected String getDescription(Object managedBean, String beanKey) throws JMException {
    if (managedBean.getClass().getAnnotation(ManagedResource.class) != null) {
    org.springframework.jmx.export.metadata.ManagedResource mr = this.attributeSource.getManagedResource(getClassToExpose(managedBean));
    return (mr != null ? mr.getDescription() : "");
    }
    else
    return super.getDescription(managedBean, beanKey);
}

@Override
protected String getAttributeDescription(PropertyDescriptor propertyDescriptor, String beanKey) {
    Method readMethod = propertyDescriptor.getReadMethod();
    if (readMethod != null && readMethod.getDeclaringClass().getAnnotation(ManagedResource.class) != null) {
    Method writeMethod = propertyDescriptor.getWriteMethod();

    ManagedAttribute getter =
        (readMethod != null ? this.attributeSource.getManagedAttribute(readMethod) : null);
    ManagedAttribute setter =
        (writeMethod != null ? this.attributeSource.getManagedAttribute(writeMethod) : null);

    if (getter != null && StringUtils.hasText(getter.getDescription())) {
        return getter.getDescription();
    }
    else if (setter != null && StringUtils.hasText(setter.getDescription())) {
        return setter.getDescription();
    }

    ManagedMetric metric = (readMethod != null ? this.attributeSource.getManagedMetric(readMethod) : null);
    if (metric != null && StringUtils.hasText(metric.getDescription())) {
        return metric.getDescription();
    }

    return propertyDescriptor.getDisplayName();

    }
    else
    return super.getAttributeDescription(propertyDescriptor, beanKey);
}

@Override
protected String getOperationDescription(Method method, String beanKey) {
    if (method.getDeclaringClass().getAnnotation(ManagedResource.class) != null) {
    PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method);
    if (pd != null) {
        ManagedAttribute ma = this.attributeSource.getManagedAttribute(method);
        if (ma != null && StringUtils.hasText(ma.getDescription())) {
        return ma.getDescription();
        }
        ManagedMetric metric = this.attributeSource.getManagedMetric(method);
        if (metric != null && StringUtils.hasText(metric.getDescription())) {
        return metric.getDescription();
        }
        return method.getName();
    }
    else {
        ManagedOperation mo = this.attributeSource.getManagedOperation(method);
        if (mo != null && StringUtils.hasText(mo.getDescription())) {
        return mo.getDescription();
        }
        return method.getName();
    }
    }
    else
    return super.getOperationDescription(method, beanKey);
}

}

I should define a reusable "AnnotationOrDefaultMBeanExporter", and then subclass that, but I'm presently using this from a custom MBeanExporter subclass, with these relevant pieces:

Instance variables:

private AnnotationJmxAttributeSource annotationSource = new AnnotationJmxAttributeSource();
private MetadataOrKeyNamingStrategy  metadataOrKeyNamingStrategy = new MetadataOrKeyNamingStrategy(annotationSource);
private MetadataOrSimpleReflectiveMBeanInfoAssembler    metadataOrSimpleReflectiveMBeanInfoAssembler    =
        new MetadataOrSimpleReflectiveMBeanInfoAssembler(annotationSource);

Constructor body:

setNamingStrategy(metadataOrKeyNamingStrategy);
setAssembler(metadataOrSimpleReflectiveMBeanInfoAssembler);
setAutodetectMode(AUTODETECT_ALL);

And then:

public void setDefaultDomain(String defaultDomain) {
    this.metadataOrKeyNamingStrategy.setDefaultDomain(defaultDomain);
}

@Override
public void setBeanFactory(BeanFactory beanFactory) {
    super.setBeanFactory(beanFactory);
    this.annotationSource.setBeanFactory(beanFactory);
}

@Override
public void afterPropertiesSet() {
    // Find some beans that should be registered
    setBeans(beans);

    // Now let the superclass create mbeans for all of the beans we found.
    super.afterPropertiesSet();
}

I could use some ideas that might possibly simplify this.

0
David M. Karr On

Ok, here's another stab at this. I didn't like the fact that I ended up having to copy method bodies from either of the two alternative implementations. I concluded that I could reduce this copying if I made both the "strategy" and "assembler" custom classes be subclasses of the "metadata" versions. The basic idea is that after checking for the existence of the "@ManagedResource" annotation on the class, I can either call the superclass method, or inline the "non-metadata" version, which in all cases was less code than in the "metadata" version.

In the case of the "strategy" class, since the relevant method in the "non-metadata" version was public, I could still create a local instance of the "strategy" class and just call the relevant method, instead of inlining the method body.

For the "assembler" class, I found that I didn't have to copy the bodies of any methods in the "metadata" version, but I found that I had to override additional methods, and I did have to copy in the method body of one method in the "super super class" because I can't access that directly. The resulting class is a little shorter than my first try, so I suppose it's better, even with those slightly gnarly bits.

To fully clean this up, this would have to be integrated into Spring to allow for the best refactoring. The functionality that this provides seems like a good thing to have, so I'll file a ticket to at least ask for this, and I'll post these classes in the ticket.

In this version, I fully refactored the implementation into a "AnnotationOrDefaultMBeanExporter" class, and then my actual application-specific class extends that (which I don't need to show here).

Here's the main exporter class:

package <package>;

import java.util.logging.Logger;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.jmx.export.MBeanExporter;
import org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource;
import org.springframework.jmx.export.naming.MetadataNamingStrategy;

public class AnnotationOrDefaultMBeanExporter extends MBeanExporter {

protected static Logger logger = Logger.getLogger(AnnotationOrDefaultMBeanExporter.class.getName());
private AnnotationJmxAttributeSource annotationSource = new AnnotationJmxAttributeSource();
protected MetadataOrKeyNamingStrategy metadataOrKeyNamingStrategy = new MetadataOrKeyNamingStrategy(annotationSource);
protected MetadataOrSimpleReflectiveMBeanInfoAssembler metadataOrSimpleReflectiveMBeanInfoAssembler = new MetadataOrSimpleReflectiveMBeanInfoAssembler(annotationSource);

public AnnotationOrDefaultMBeanExporter() {
    setNamingStrategy(metadataOrKeyNamingStrategy);
    setAssembler(metadataOrSimpleReflectiveMBeanInfoAssembler);
    setAutodetectMode(AUTODETECT_ALL);
}

/**
 * Specify the default domain to be used for generating ObjectNames
 * when no source-level metadata has been specified.
 * <p>The default is to use the domain specified in the bean name
 * (if the bean name follows the JMX ObjectName syntax); else,
 * the package name of the managed bean class.
 * @see MetadataNamingStrategy#setDefaultDomain
 */
public void setDefaultDomain(String defaultDomain) {
    this.metadataOrKeyNamingStrategy.setDefaultDomain(defaultDomain);
}

@Override
public void setBeanFactory(BeanFactory beanFactory) {
    super.setBeanFactory(beanFactory);
    this.annotationSource.setBeanFactory(beanFactory);
}
}

Following this is the "strategy" class. The somewhat gnarly bit here is wrapping a checked exception with throwing a RuntimeException().

package <package>;

import java.io.IOException;

import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import org.springframework.aop.support.AopUtils;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.jmx.export.metadata.JmxAttributeSource;
import org.springframework.jmx.export.naming.KeyNamingStrategy;
import org.springframework.jmx.export.naming.MetadataNamingStrategy;

public class MetadataOrKeyNamingStrategy extends MetadataNamingStrategy {
private KeyNamingStrategy       keyNamingStrategy       = new KeyNamingStrategy();

public MetadataOrKeyNamingStrategy() { }

public MetadataOrKeyNamingStrategy(JmxAttributeSource attributeSource) {
    super(attributeSource);
}

@Override
public void afterPropertiesSet() {
    super.afterPropertiesSet();
    try {
    keyNamingStrategy.afterPropertiesSet();
    }
    catch (IOException ex) {
    throw new RuntimeException(ex);
    }
}

@Override
public ObjectName getObjectName(Object managedBean, String beanKey)
    throws MalformedObjectNameException {
    Class<?> managedClass = AopUtils.getTargetClass(managedBean);
    if (managedClass.getAnnotation(ManagedResource.class) != null)
    return super.getObjectName(managedBean, beanKey);
    return keyNamingStrategy.getObjectName(managedBean, beanKey);
}
}

And here's the "assembler" class:

package <package>;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;

import javax.management.Descriptor;

import org.springframework.aop.support.AopUtils;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler;
import org.springframework.jmx.export.metadata.JmxAttributeSource;

public class MetadataOrSimpleReflectiveMBeanInfoAssembler extends MetadataMBeanInfoAssembler {

public MetadataOrSimpleReflectiveMBeanInfoAssembler() { }

public MetadataOrSimpleReflectiveMBeanInfoAssembler(JmxAttributeSource attributeSource) {
    super(attributeSource);
}

@Override
protected boolean includeReadAttribute(Method method, String beanKey) {
    if (method.getDeclaringClass().getAnnotation(ManagedResource.class) != null)
    return super.includeReadAttribute(method, beanKey);
    return true;
}

@Override
protected boolean includeWriteAttribute(Method method, String beanKey) {
    if (method.getDeclaringClass().getAnnotation(ManagedResource.class) != null)
    return super.includeWriteAttribute(method, beanKey);
    return true;
}

@Override
protected boolean includeOperation(Method method, String beanKey) {
    if (method.getDeclaringClass().getAnnotation(ManagedResource.class) != null)
    return super.includeOperation(method, beanKey);
    return true;
}

@Override
protected String getDescription(Object managedBean, String beanKey) {
    if (managedBean.getClass().getAnnotation(ManagedResource.class) != null)
    return super.getDescription(managedBean, beanKey);
    return superSuperGetDescription(managedBean, beanKey);
}

/** Copied from AbstractMBeanInfoAssembler.getDescription(), the super.superclass of this class, which can't be easily called. */
private String superSuperGetDescription(Object managedBean, String beanKey) {
    String targetClassName = getTargetClass(managedBean).getName();
    if (AopUtils.isAopProxy(managedBean)) {
    return "Proxy for " + targetClassName;
    }
    return targetClassName;
}

@Override
protected String getAttributeDescription(PropertyDescriptor propertyDescriptor, String beanKey) {
    Method readMethod = propertyDescriptor.getReadMethod();
    if (readMethod != null && readMethod.getDeclaringClass().getAnnotation(ManagedResource.class) != null)
    return super.getAttributeDescription(propertyDescriptor, beanKey);
    return propertyDescriptor.getDisplayName();
}

@Override
protected String getOperationDescription(Method method, String beanKey) {
    if (method.getDeclaringClass().getAnnotation(ManagedResource.class) != null)
    return super.getOperationDescription(method, beanKey);
    return method.getName(); 
}

@Override
protected void populateAttributeDescriptor(Descriptor desc, Method getter, Method setter, String beanKey) {
    Method  methodToUse = getter;
    if (methodToUse == null)
    methodToUse = setter;
    if (methodToUse != null) {
    if (methodToUse.getDeclaringClass().getAnnotation(ManagedResource.class) != null)
        super.populateAttributeDescriptor(desc, getter, setter, beanKey);
    else
        applyDefaultCurrencyTimeLimit(desc);
    }
    else
    applyDefaultCurrencyTimeLimit(desc);
}

@Override
protected void populateMBeanDescriptor(Descriptor descriptor, Object managedBean, String beanKey) {
    if (managedBean.getClass().getAnnotation(ManagedResource.class) != null)
    super.populateMBeanDescriptor(descriptor, managedBean, beanKey);
    else
    applyDefaultCurrencyTimeLimit(descriptor);
}

@Override
protected void populateOperationDescriptor(Descriptor desc, Method method, String beanKey) {
    if (method != null) {
    if (method.getClass().getAnnotation(ManagedResource.class) != null)
        super.populateOperationDescriptor(desc, method, beanKey);
    else
        applyDefaultCurrencyTimeLimit(desc);
    }
    else
    applyDefaultCurrencyTimeLimit(desc);
}
}