JMX - building your own proxies - snapshot proxies

In my last blog entry, I described how it's possible to get synchronized access to multiple attributes of the same MBean via the use of a SnapshotProxy and a SynchronizedStandardMBean.

This blog entry describes the SnapshotProxy in more detail.

Background

JMX's use of proxies via the MBeanServerInvocationHandler APIs is extremely powerful, permitting type-safe programming for client applications. Out of the box, JMX provides a simple proxy implementation that forward the typed requests to an MBeanServer using the MBeanServerConnection API.

One can also enrich the code in the proxy logic to do more. Éamonn McManus's blog has an article on writing your own proxy factory to deal with type-safe proxies. In our case, we're interested in atomic access to multiple attributes on the same MBean.

Our SnapshotProxy will use a single call to MBeanServerConnection.getAttributes() to read the value of all the attributes of a given MBean in a single shot, and then respond to 'getter' requests on individual attributes from the data returned by the single snapshot.

The SnapshotProxy Factory Class

A proxy object is typically generated through a factory class, responsible for creating all the SnapshotProxy instances.

Here's our ProxyFactory class:


public class ProxyFactory {
    
    public Object getSnapshotProxy(MBeanServerConnection connection,
            ObjectName objectName,
            Class interfaceClass) {

        final InvocationHandler handler =
                new SnapshotProxy(connection,
                objectName,
                interfaceClass);
        
        final Class[] interfaces = new Class[] {interfaceClass };
        
        return Proxy.newProxyInstance(interfaceClass.getClassLoader(),
                interfaces,
                handler);
    }

    private static class SnapshotProxy extends MBeanServerInvocationHandler {

       ...
 
    }

}

 

As you can see in the above, the SnapshotProxy class is actually a private inner class that's only ever referenced by the Factory class.

The SnapshotProxy

The SnapshotProxy class's only real job is to implement the invoke method to implement a method invocation through reflection.

The proxy's behavior is to try, upon first invocation, to take a single snapshot of all the attributes, and to then respond to accessors to individual attributes by returning the value from the snapshot.

Here's the beginning of our class:

    private static class SnapshotProxy extends MBeanServerInvocationHandler {
        
        private MBeanServerConnection connection;
        private ObjectName objectName;
        private Class interfaceClass;
        private MapattributeMap = null;
        
        private SnapshotProxy(MBeanServerConnection connection,
                ObjectName objectName,
                Class interfaceClass) {
            
            super(connection, objectName);
            this.connection = connection;
            this.objectName = objectName;
            this.interfaceClass = interfaceClass;
        }
        
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            
            final String methodName = method.getName();
            final Class[] paramTypes = method.getParameterTypes();
            final Class returnType = method.getReturnType();
            
            /\* Inexplicably, InvocationHandler specifies that args is null
               when the method takes no arguments rather than a
               zero-length array.  \*/
            final int nargs = (args == null) ? 0 : args.length;
            
            String attributeName = decodeAttributeName(methodName, nargs, returnType);
            
            /\* We only implement the returning of attribute values \*/
            if (attributeName == null) {
                throw new RuntimeException("Proxy does not implement method "+method.getName());
            }
            
            /\* If we've never been called before, fetch all the attributes \*/
            if (attributeMap == null) {
                fillAttributeMap();
            }
            
            if (attributeMap == null || !attributeMap.containsKey(attributeName)) {
                // If we were unable to snapshot the attribute due
                // to some kind of error, fallback on default proxy
                return super.invoke(proxy, method, args);
            }
            
            return attributeMap.get(attributeName);
            
        }
        
 

Note that there's a difference in semantic between JMX's getAttribute method and the getAttributes method. Exceptions that would be thrown on a call to getAttribute are silently swallowed by getAttributes, the only visible sign of the error being that the attribute is missing from the returned values. As such, if our snapshot doesn't contain the requested value, our proxy falls back to the default proxy behavior to have the exception raised.

There are two key methods missing in the implementation above - decodeAttributeName() and fillAttributeMap(). These are detailed below.

decodeAttributeName()

decodeAttributeName is responsible for mapping the name of a method on the MBean interface to the equivalent MBean attribute name (if such a mapping exists), or returning null if the method is not an attribute accessor. Remember that 'getters' of attributes in an MBean interface may have one of two possible signatures:

  • Type getAttributeName()
  • boolean isAttributeName()

Here's the code:

        private String decodeAttributeName(String methodName, int argCount, Class returnType) {
            
            String attributeName = null;
            
            if (methodName.startsWith("get")
                    && methodName.length() > 3
                    && argCount == 0
                    && !returnType.equals(Void.TYPE)) {
                attributeName = methodName.substring(3);
            } else if (methodName.startsWith("is")
                    && methodName.length() > 2
                    && argCount == 0
                    && returnType.equals(Boolean.TYPE)) {
                attributeName = methodName.substring(2);
            }
            
            return attributeName;
        }
 
fillAttributeMap()

The method fillAttributeMap() is responsible for filling a map with the results obtained from a single snapshot call to getAttributes().

To do this, it needs to obtain the names of all attributes in the MBean's interace through reflection, and then formulate a call to the MBeanServer to read all those attributes. It then needs to store the name/attribute pairs in a map so that the invoker code above can return the stored values. Here's the code:

        private void fillAttributeMap() {
            
            String[] attributeNames = getAttributeNames(interfaceClass);
            try {
                
                Map myMap = new HashMap();
                
                AttributeList attributeList =
                        connection.getAttributes(objectName, attributeNames);
                for (Iterator i = attributeList.iterator(); i.hasNext(); ) {
                    Attribute attr = i.next();
                    myMap.put(attr.getName(), attr);
                }
                attributeMap = myMap;
            } catch (Exception e) {
                // leave map uninitialized in case of error
            }
        }
        
 

As you can see, this code calls down to getAttributeNames, which is responsible for the introspection of the interface class to find out all the methods that indicate an accessor to an MBean attribute.

getAttributeNames()

There is a small added complication when introspecting the interface class to obtain the names of all the attributes - one Java interface can extend another Java interface, so it's necessary not only to iterate over the methods in the interface, but also to iterate over those interfaces declared as implemented by this interface.

        private String[] getAttributeNames(Class interfaceClass) {
            Set nameSet = new HashSet();
            
            addAttributeNames(nameSet, interfaceClass);
                
            // recurse over all other interfaces implemented by this interface
            Class[] interfaces = interfaceClass.getInterfaces();
            for (int i = 0; i < interfaces.length; i++) {
                addAttributeNames(nameSet, interfaces[i]);
            }
            return (String[])nameSet.toArray(new String[] {});
        }
        
        private void addAttributeNames(Set nameSet, Class interfaceClass) {
            
            Method[] methods = interfaceClass.getDeclaredMethods();
            
            for (int i = 0 ; i < methods.length; i++) {
                
                // Only consider public attributes
                if (!Modifier.isPublic(methods[i].getModifiers()))
                    continue;
                
                String methodName = methods[i].getName();
                final Class[] paramTypes = methods[i].getParameterTypes();
                final Class returnType = methods[i].getReturnType();
                
                String attrName =
                        decodeAttributeName(methodName, paramTypes.length, returnType);
                if (attrName != null) {
                    nameSet.add(attrName);
                }
            }
            
            return;
        }
 

And that's it! All the code to implement a SnapshotProxy factory and its simple SnapshotProxy.

You can download the example code detailed above here.

The SnapshotProxy - room for improvement

This SnapshotProxy can be useful for more than just atomic or near-atomic access to MBean attributes (fully atomic only if the MBean has been registered or implemented using a design pattern such as the SynchronizedStandardMBean). The SnapshotProxy also has interesting network characteristics, since it can reduce the number of communications with the MBeanServer.

There is still room for improvement, though. Here are a few suggestions:

  • Cache the return value of getAttributeNames(Class interfaceClass) in a WeakHashMap to avoid using reflection for each proxy instance.
  • Cache the exceptions returned for individual attributes via the call to getAttribute, to avoid making the call if the attribute is requested again.
  • Make the proxy listen for AttributeValueChangeNotifications from the MBean, and either invalidate itself or permit the snapshot to be refreshed.
  • Add a constructor permitting a proxy lifetime parameter so that the proxy either becomes invalid or permits the snapshot to be refreshed on a regular basis.
Comments:

Instead of throwing an exception when decodeAttributeName() returns null, you could delegate the invocation to the super class.
The snapshot proxy would then be able to handle all method calls, like a regular proxy.

Posted by daniel on September 14, 2006 at 11:30 AM CEST #

If you permit set and invoke operations, then it would almost certainly be appropriate to invalidate the snapshot, since these actions may be state-changing for the MBean.

One simple policy is to use the snapshot proxy when constructing information to display (e.g. a web page) and to use a normal proxy when performing control actions.

Posted by Nick on September 15, 2006 at 04:15 AM CEST #

Post a Comment:
Comments are closed for this entry.
About

nickstephen

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today