Skip navigation

One curiosity about Model MBeans is that attributes also appear as operations. Is there any way to avoid that?

We encounter this question occasionally, most recently in the JMX forum on SDN. As the contributor there notes, this is tracked as RFE 6339571, but won't be implemented until Java SE 7. What can you do in the mean time?

In order to define an attribute, say Foo, as being the result of calling the getBar method, Model MBeans require you to define a ModelMBeanAttributeInfo for Foo with the getMethod set to getBar. That seems reasonable so far. But they also require you to define a ModelMBeanOperationInfo for getBar. So if you connect with JConsole, say, then not only will you see the Foo attribute, you will also see the getBar operation. There are reasons for this, which are alluded to in RFE 6339571, but it's still annoying.

The getBar operation needs to be in the MBeanInfo so that the Model MBean can find it when you access the Foo attribute. But it doesn't need to be in the MBeanInfo that JConsole sees. Is there some way we could arrange for the MBeanInfo to be different in these two cases?

The answer is yes, via a hack. We can tweak the serialization of this MBeanInfo so that operations like getBar are removed when the MBeanInfo is being sent to a remote client such as JConsole. This will have no effect on the local MBeanInfo that the Model MBean itself sees, so the Foo attribute will continue to work.

We'll define a subclass of ModelMBeanInfoSupport called NoGetterMBeanInfo, and add a writeReplace method to this class. The writeReplace method can return a different object to be serialized in the place of the original one. Instead of serializing a NoGetterMBeanInfo, which would require a client such as JConsole to know that class, we can serialize a ModelMBeanInfoSupport. Since that's a standard class, every client must know it. And we can arrange for this new ModelMBeanInfoSupport not to contain any getter methods like getBar.

We identify getter methods by their Descriptor: if the Descriptor contains a role field with the value "getter", then we assume it's a getter. Likewise if the value is "setter", we'll assume it's a setter and also delete it. This is not a foolproof test: if the value is "operation" then it can still be used as a getter, but we'll assume that you can either change your code so that the field has the right value, or change NoGetterMBeanInfo to use a different test.

Here then is the code for NoGetterMBeanInfo:

 import java.util.ArrayList; import java.util.List; import javax.management.Descriptor; import javax.management.MBeanException; import javax.management.MBeanOperationInfo; import javax.management.modelmbean.ModelMBeanAttributeInfo; import javax.management.modelmbean.ModelMBeanConstructorInfo; import javax.management.modelmbean.ModelMBeanInfo; import javax.management.modelmbean.ModelMBeanInfoSupport; import javax.management.modelmbean.ModelMBeanNotificationInfo; import javax.management.modelmbean.ModelMBeanOperationInfo; public class NoGetterMBeanInfo extends ModelMBeanInfoSupport { public NoGetterMBeanInfo(ModelMBeanInfo mmbi) { super(mmbi); } @Override public NoGetterMBeanInfo clone() { return new NoGetterMBeanInfo(this); } private Object writeReplace() { List ops = new ArrayList(); for (MBeanOperationInfo mboi : this.getOperations()) { ModelMBeanOperationInfo mmboi = (ModelMBeanOperationInfo) mboi; Descriptor d = mmboi.getDescriptor(); String role = (String) d.getFieldValue("role"); if (!"getter".equalsIgnoreCase(role) && !"setter".equalsIgnoreCase(role)) ops.add(mmboi); } ModelMBeanOperationInfo[] mbois = new ModelMBeanOperationInfo[ops.size()]; ops.toArray(mbois); Descriptor mbeanDescriptor; try { mbeanDescriptor = this.getMBeanDescriptor(); } catch (MBeanException e) { throw new RuntimeException(e); } return new ModelMBeanInfoSupport( this.getClassName(), this.getDescription(), (ModelMBeanAttributeInfo[]) this.getAttributes(), (ModelMBeanConstructorInfo[]) this.getConstructors(), mbois, (ModelMBeanNotificationInfo[]) this.getNotifications(), mbeanDescriptor); } } 

To use it, change code where you do something like this...

 ModelMBean mbean = new RequiredModelMBean(myModelMBeanInfo); 

...into this...

 ModelMBean mbean = new RequiredModelMBean(new NoGetterMBeanInfo(myModelMBeanInfo)); 

Of course this hack will only work if you are using a connector that is based on Java object serialization, such as the RMI connector that is part of the Java platform. If you are using a SOAP-based connector, say, then you will need to look at how to achieve the same result in that context. (One possibility is to insert an MBeanServerForwarder that intercepts a remote getMBeanInfo operation and rewrites the MBeanInfo as above.)

The forum question asked about how to do this in the context of Spring; since Spring uses Model MBeans the question arises frequently. I was able to plug in the NoGetterMBeanInfo by using a custom MBeanExporter like this:

 import javax.management.JMException; import javax.management.ObjectName; import javax.management.modelmbean.ModelMBean; import javax.management.modelmbean.ModelMBeanInfo; import org.springframework.jmx.export.MBeanExporter; public class NoGetterExporter extends MBeanExporter { public NoGetterExporter() { } @Override protected void doRegister(Object mbean, ObjectName objectName) throws JMException { if (mbean instanceof ModelMBean) { ModelMBean mmb = (ModelMBean) mbean; ModelMBeanInfo mmbi = (ModelMBeanInfo) mmb.getMBeanInfo(); mmb.setModelMBeanInfo(new NoGetterMBeanInfo(mmbi)); } super.doRegister(mbean, objectName); } } 

You probably already have an MBeanExporter in your Spring configuration file, with lines looking something like this:

 <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> ... </bean> 

You should just be able to change the MBeanExporter class name to the fully-qualified name of NoGetterExporter.

NoGetterMBeanInfo is a hack, but I hope it's a useful one!

One of the features planned for version 2.0 of the JMX API iscascading, also known as federation. Here's what it is, and how you can build a simplified form of the same thing without waiting for 2.0.

Update: a subset of the Java DMK product has been released as open source. Daniel Fuchs explains how to use the Cascading API from Open DMK. I would recommend using this in production if you need Cascading.

Cascading

The basic idea behind cascading is that you can "import" MBeans from one MBean Server into another MBean Server. In the picture here, the top MBean Server (labeled Master Agent) imports MBeans from two other MBean Servers (Subagent 1 andSubagent 2).

A mirror MBean forwards everything to another, remote MBean 

The different MBean Servers could be in the same Java VM, or more likely they could be in different VMs, possibly on different machines.

By "importing", I mean the Master Agent has a "mirror" for each of the imported MBeans. This mirror shows exactly the same MBean interface as the MBean it reflects. An operation on the mirror is forwarded to the remote MBean.

So for example suppose the middle MBean in Subagent 1 has attributes "Size" and "Capacity" and an operation "reset". Its mirror in the Master Agent will have the same attributes and the same operation. If I get the "Size" attribute from the mirror, it will forward the request to the remote MBean, and return the result it gets back. If I invoke the "reset" operation on the mirror, it will forward that request to the remote MBean, which will do the real "reset" operation.

Getting Size attribute from mirror forwards to remote MBean 

The end result is that a client of the Master Agent (such as JConsole) doesn't have to know about the other MBean Servers at all. It can just interact with the mirror MBeans, and the result will be the same as if it had interacted with the corresponding MBeans in the other MBean Servers.

Some of the MBeans in the subagents might themselves be mirrors for "subsubagents", so you could have a multilevel hierarchy. This is where the name "cascading" comes from.

(Cascading has existed for years as part of Sun's Java Dynamic Management Kitproduct (Java DMK), and you can read about how it works there in the tutorial.)

So what's it for?

There are several cases where cascading is useful.

The most obvious case is where you have a number of different MBean Servers with interesting MBeans and you want to be able to manage them all. You can do this by importing the MBeans into a single MBean Server and attaching a management client like JConsole to this MBean Server.

This is much simpler for the client than having to connect separately to each MBean Server. It might even be that the links between the Master Agent and the Subagents are over a private network that is not accessible to the client, so it couldn't connect directly to the Subagents even if it wanted to.

A second case where cascading could come in handy is if you don't want to expose all of the MBeans in an MBean Server to a particular client. You can create a Master Agent that only imports the subset of MBeans that you do want to expose, and let the client connect to that.

A related case is where you want to give the MBeans different names. There's no requirement that the mirror MBean have the same name as the original MBean in the subagent. You can construct a new MBean model by importing MBeans and giving them different names. Of course, you don't have to import all the MBeans, and you don't have to import them all from the same place. So this is quite a general mechanism.

A simple implementation

Let's look at how we might implement a basic form of cascading. The idea is to have a class MBeanMirrorFactory that allows us to create mirror MBeans. After creating a mirror MBean, we can register it in the Master Agent under whatever name we've chosen.

To show how this works, suppose we want to create an MBean Server that contains all the same MBeans as the Platform MBean Server, but where every MBean's name starts with "mirror/". So the MBean called "java.lang:type=Runtime" in the Platform MBean Server will have a mirror called "mirror/java.lang:type=Runtime" in the new MBean Server. Here's the code to do that usingMBeanMirrorFactory:

        MBeanServer platformMBS =
                ManagementFactory.getPlatformMBeanServer();
        
        MBeanServer mirrorMBS =
                MBeanServerFactory.newMBeanServer();
        
        Set<ObjectName> names = platformMBS.queryNames(null, null);
        for (ObjectName name : names) {            ObjectName mirrorName = new ObjectName("mirror/" + name);
            MBeanMirror mirror =
                    MBeanMirrorFactory.newMBeanMirror(platformMBS, name);
            mirrorMBS.registerMBean(mirror, mirrorName);
        }

As another example, here's how we might set up a configuration like the one in the picture above, except that all MBeans from both subagents are imported. The MBeans from Subagent 1 will be prefixed with "subagent1/" and the ones from Subagent 2 with "subagent2/". So we will have mirror MBeans called "subagent1/java.lang:type=Runtime" and "subagent2/java.lang:type=Runtime", for example.

        ...
        JMXServiceURL url1 =
                new JMXServiceURL("service:jmx:rmi:///jndi/rmi://oneman:8888/jmxrmi");
        JMXServiceURL url2 =
                new JMXServiceURL("service:jmx:rmi:///jndi/rmi://oneman:9999/jmxrmi");

        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
        importAll(url1, "subagent1/", mbs);
        importAll(url2, "subagent2/", mbs);
        ...

    private static void importAll(
            JMXServiceURL url, String prefix, MBeanServer localMBS)
    throws IOException {
        JMXConnector conn = JMXConnectorFactory.connect(url);
        MBeanServerConnection remoteMBS = conn.getMBeanServerConnection();

        Set<ObjectName> names = remoteMBS.queryNames(null, null);
        for (ObjectName name : names) {
            try {
                ObjectName mirrorName = new ObjectName(prefix + name);
                MBeanMirror mirror =
                        MBeanMirrorFactory.newMBeanMirror(remoteMBS, name);
                localMBS.registerMBean(mirror, mirrorName);
            } catch (Exception e) {
                // log the exception and skip this MBean
            }
        }
    }

The JMXServiceURLs are just examples, of course. (oneman is the anagrammatic name of my workstation.)

If we attach JConsole to the Master Agent here, we'll see something like this:

JConsole attached to master agent shows subagent mirrors 

The attributes shown are those of the MBean "subagent1/java.lang:type=ClassLoading" and are the same as we would see if we attached directly to Subagent 1 and looked at its MBean "java.lang:type=ClassLoading".

Mirror MBean implementation

The mirror MBean implementation is actually quite simple. The same class can implement a mirror for any MBean. The secret is that we do not have to know the management interface at compile time. We can discover the interface at run time and implement a Dynamic MBean that forwards every operation on the mirror to the remote MBean.

Here's a first attempt for the implementation class:

// First attempt.  THIS DOES NOT COMPILE.
public class PlainMBeanMirror implements DynamicMBean {
    private final MBeanServerConnection mbsc;
    private final ObjectName objectName;
    
    public PlainMBeanMirror(MBeanServerConnection mbsc, ObjectName objectName) {
        this.mbsc = mbsc;
        this.objectName = objectName;
    }

    public Object getAttribute(String name) {
        return mbsc.getAttribute(objectName, name);
    }
    
    public void setAttribute(Attribute attr) {
    mbsc.setAttribute(objcetName, attr);
    }
    
    public AttributeList getAttributes(String[] names) {
    return mbsc.getAttributes(objectName, names);
    }

    public AttributeList setAttributes(AttributeList attrs) {
        return mbsc.setAttributes(objectName, attrs);
    }

    public Object invoke(String opName, Object[] args, String[] sig) {
        return mbsc.invoke(objectName, opName, args, sig);
    }

    public MBeanInfo getMBeanInfo() {
        return mbsc.getMBeanInfo(objectName);
    }
}

Each of the six methods of the DynamicMBean is forwarded to the corresponding method in the MBeanServerConnection interface. The MBeanServerConnection methods have an extra ObjectName parameter, which here is the name of the remote MBean.

This is just a little too good to be true, and indeed if we try compiling it we will get errors, because we haven't considered exceptions carefully enough.

Exceptions

If we look at a method in the DynamicMBean interface, say getAttribute, and the corresponding method in the MBeanServerConnection interface, we will see that DynamicMBean.getAttribute throws AttributeNotFoundException, MBeanException, and ReflectionException. MBeanServerConnection.getAttribute throws the same exceptions, but also InstanceNotFoundException and IOException. It is the same story for the other five DynamicMBean methods.

This makes sense. If we invoke getAttribute on a mirror MBean, it will invoke getAttribute on the remote MBean. If that gets AttributeNotFoundException, then the mirror MBean can simply throw the same exception. But there are two other ways we could get an exception. One is if the remote MBean does not exist. The other is if we get a communication failure, for example because the remote machine is not reachable.

So we need to rewrite PlainMirrorMBean.getAttribute to take these extra exceptions into account. Happily, the exception MBeanException exists precisely to wrap these general sorts of exception. So here's what the new version looks like:

    public Object getAttribute(String name)
    throws AttributeNotFoundException, MBeanException, ReflectionException {
        try {
            return mbsc.getAttribute(objectName, name);
        } catch (IOException e) {
            throw new MBeanException(e);
        } catch (InstanceNotFoundException e) {
            throw new MBeanException(e);
        }
    }

We put in the throws clause the exceptions declared by DynamicMBean.getAttribute. And we catch the two other exceptions from MBeanServerConnection.getAttribute and wrap them in MBeanException. We can handle setAttribute and invoke in the same way.

But that still leaves three other DynamicMBean methods that don't throw MBeanException. How do we handle those?

For getAttributes and setAttributes, the answer is simple. These methods are supposed to return an AttributeList containing all the attributes that were successfully got or set. If the attempt to get any given attribute produces an error, then that attribute is simply omitted from the returned list. If you want to know what the error was, you have to call getAttribute or setAttribute (rather than get/setAttributes) with just the attribute in question and see what exception it produces.

So if we get an IOException or an InstanceNotFoundException when forwarding the getAttributes or setAttributes call to the remote MBean, we can simply consider that every attribute produced an error, and return an empty AttributeList.

Here's what the rewritten getAttributes looks like:

     public AttributeList getAttributes(String[] names) {
        try {
            return mbsc.getAttributes(objectName, names);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            return new AttributeList();
        }
    }

Since DynamicMBean.getAttributes doesn't declare any checked exceptions, we catch and rethrow RuntimeException, then catch Exception, which is then all checked exceptions. This is easier than catching the three checked exceptions that MBeanServerConnection.getAttributes declares.

We can rewrite setAttributes the same way. So that just leaves one method from DynamicMBean, getMBeanInfo.

getMBeanInfo()

DynamicMBean.getMBeanInfo does not declare any checked exceptions, so we can't wrap an IOException or InstanceNotFoundException in an MBeanException as we did for getAttribute and co. We could try returning a special "empty" MBeanInfo, but that is hacky. We could also wrap the exception in a RuntimeException, which is less hacky but still not very satisfactory. No RuntimeExceptions are mentioned in the specification of MBeanServerConnection.getMBeanInfo so callers won't necessarily be prepared to deal with them.

I think the best solution is to get the MBeanInfo once when the mirror is created and then simply return this value forever after from the getMBeanInfo() method. This has a number of advantages.

  • If the remote MBean is nonexistent or inaccessible, you might as well find out at once when you try to create the mirror, rather than waiting until the first time you use it.
  • The getMBeanInfo() method will always be called at least once anyway, when the MBean is registered. This is because MBeanServer.registerMBean returns an ObjectInstance, which is an ObjectName plus a class name. The class name is the value of MBeanInfo.getClassName() for the MBean.
  • If the Java VM where the mirror is registered is running with aSecurityManager then every operation on the mirror MBean will need MBeanInfo.getClassName() in order to construct the MBeanPermission that will be checked. If we don't keep a copy of the MBeanInfo within the mirror, then every MBean operation will require two round trips to the remote MBean, one to get the MBeanInfo, and one to do the actual operation.

The principal disadvantage of this solution is that the MBeanInfo of the remote MBean could change, and the mirror will never show that. It is rare to have MBeans where the MBeanInfo changes, and if you do then you can always override PlainMBeanMirror.getMBeanInfo() to fetch the remote MBeanInfo when appropriate.

So here's the new constructor that caches the MBeanInfo, and the new, trivial getMBeanInfo() method that returns it:

public class PlainMBeanMirror implements DynamicMBean {
    private final MBeanServerConnection mbsc;
    private final ObjectName objectName;
    private final MBeanInfo mbeanInfo;

    public PlainMBeanMirror(MBeanServerConnection mbsc, ObjectName objectName)
    throws IOException, InstanceNotFoundException, IntrospectionException {
        this.mbsc = mbsc;
        this.objectName = objectName;
        try {
            this.mbeanInfo = mbsc.getMBeanInfo(objectName);
        } catch (ReflectionException e) {
            // Callers cannot possibly care about the difference between
            // IntrospectionException and ReflectionException
            IntrospectionException ie = new IntrospectionException(e.getMessage());
            ie.initCause(e);
            throw ie;
        }
    }

    public MBeanInfo getMBeanInfo() {
        return mbeanInfo;
    }

    ...
}

Notifications

Another thing we'd like to be able to do with a mirror MBean is receive notifications that were sent by the original MBean. That is, if we call MBeanServerConnection.addNotificationListener on the mirror MBean, we'd like our listener to receive the same notifications as if we had called addNotificationListener on the remote MBean.

We can do this just by implementing NotificationEmitter (or its parent NotificationBroadcaster) and forwarding its methods in the same way as we did for the methods in DynamicMBean. However, it is better not to implement NotificationEmitter if the MBean does not in fact emit notifications. A client should be able to tell whether addNotificationListener is allowed using isInstanceOf(mirrorName, NotificationBroadcaster.class.getName()). In other words, we should have one sort of mirror for MBeans that are NotificationBroadcasters and another sort for MBeans that are not.

The obvious way to achieve this is to have a subclass of PlainMBeanMirror, say NotifyingMBeanMirror, that implements NotificationEmitter, in the same way as StandardEmitterMBean subclasses StandardMBean in Java SE 6. But the drawback of that is that if we subclass PlainMBeanMirror for another reason, for example to override getMBeanInfo as we saw above, then we will usually need to subclass NotifyingMBeanMirror as well and duplicate the same code in the two subclasses.

An alternative that avoids this problem is to use delegation instead of subclassing. The idea is that a NotifyingMBeanMirror wraps a PlainMBeanMirror and delegates the methods of DynamicMBean to it.

For this to work cleanly, we define an interface that will be implemented by both PlainMBeanMirror and NotifyingMBeanMirror.

public interface MBeanMirror extends DynamicMBean {
    public MBeanServerConnection getMBeanServerConnection();
    public ObjectName getRemoteObjectName();
}

The methods in this interface are useful for code that has created a mirror to register it in the MBean Server. They are also useful to NotifyingMBeanMirror because it can use the interface for its delegation rather than hardwiring the concrete class PlainMBeanMirror.

You might also have noticed earlier that MBeanMirrorFactory.newMBeanMirror returns an MBeanMirror. The idea is that this method returns a NotifyingMBeanMirror if the remote MBean is a NotificationEmitter, and otherwise a PlainMBeanMirror:

public class MBeanMirrorFactory {
    private MBeanMirrorFactory() {} // there are no instances of this class

    public static MBeanMirror newMBeanMirror(
            MBeanServerConnection mbsc,
            ObjectName objectName)
    throws IOException, InstanceNotFoundException, IntrospectionException {
        MBeanMirror mirror = new PlainMBeanMirror(mbsc, objectName);
        if (mbsc.isInstanceOf(objectName, NotificationBroadcaster.class.getName()))
            mirror = new NotifyingMBeanMirror(mirror);
        return mirror;
    }
}

The code of NotifyingMBeanMirror is straightforward but a bit tedious. For the addNotificationListener and removeNotificationListener methods from NotificationEmitter, we again have a problem with exceptions like IOException and InstanceNotFoundException, and here we don't have a much better solution than wrapping them in a RuntimeException. (Daniel Fuchs has suggested usingUndeclaredThrowableException here.)

Here's the outline of NotifyingMBeanMirror:

public class NotifyingMBeanMirror implements MBeanMirror, NotificationEmitter {
    private final MBeanMirror mirror;

    public NotifyingMBeanMirror(MBeanMirror mirror) {
        this.mirror = mirror;
    }

    public Object getAttribute(String name)
    throws AttributeNotFoundException, MBeanException, ReflectionException {
        return mirror.getAttribute(name);
    }

    ...same for the other five methods from DynamicMBean...

    public void addNotificationListener(
            NotificationListener listener,
            NotificationFilter filter,
            Object handback) {
        try {
            mirror.getMBeanServerConnection().addNotificationListener(
                    mirror.getRemoteObjectName(), listener, filter, handback);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    ...similar for the other three methods from NotificationEmitter...
}

Further work

One thing that the Cascading Service from Java DMK does in addition to mirroring is to track the creation and deletion of remote MBeans. If you import "java.lang:*" from Subagent 1, say, and a new MBean appears in Subagent 1 called "java.lang:type=New", then a new mirror will appear automatically in the Master Agent. Conversely, if an imported MBean disappears from the subagent then its mirror automatically disappears too. This works by using a listener on the MBeanServerDelegate to learn of remote creations and deletions.

Another interesting question is whether ObjectNames should sometimes be rewritten. If I subscribe to notifications from "subagent1/java.lang:type=Runtime", what will the source of the notifications be? Using the design above, it will be "java.lang:type=Runtime", which might be unexpected. The Cascading Service from Java DMK would rewrite the source to be "subagent1/java.lang:type=Runtime". This discussion can go much further if we think about whether ObjectName attributes and parameters should also be rewritten.

The version of Cascading in version 2.0 of the JMX API will probably be based on Virtual MBeans, and the details will look quite different from what I've described here. But it will solve the same problems.

I haven't talked at all about security, not because it is unimportant but because there's too much to say. Another day perhaps. The basic question is, how does the Master Agent connect securely to each Subagent? And is there a way to have different access to the Subagent MBeans for different users that might be connected to the Master Agent?

The source code

The source code for the classes I've described above is in mirrormbean.zip, along with what may be the most twisted unit test you have ever seen.