MXBeansmap between arbitrary Java types and a fixed set of types in javax.management.openmbean called the Open Types. This allows clients to interact with MXBeans, without needing to know the original Java types (which might require putting extra jars in their classpath and so on).

Up until now the mapping rules were fixed. Certain types can not be mapped by these rules, for example self-referential types or types such as Object or Number. In the Java 7 platform, we're planning to allow customization of the rules, as part of JSR 255 which is defining version 2.0 of the JMX API. This is a summary of the proposed changes.

What problem are we solving?

There are three sorts of use cases here:

  1. You have some types that you control and that are used in your MXBean interfaces. For example, java.lang.management.MemoryUsage which is referenced by java.lang.management.MemoryMXBean. An update to java.lang.management could update MemoryUsage, for example to add an annotation to it.
  2. You have some types that you do not control that are used in MXBean interfaces that you do control. For example, you are an end-user and you define an MXBean interface MyMemoryMXBean that references java.lang.management.MemoryUsage. You can't change MemoryUsage but you can change MyMemoryMXBean.
  3. You have some types that you do not control that are used in MXBean interfaces that you do not control either. For example, you want to define an MXBean-ified version of some legacy MBeans defined by somebody else.

The proposal is to add one class, one interface, and two annotations to address these cases. The existing class javax.management.StandardMBean acquires two new constructors and the existing class javax.management.JMX acquires two new overloads of existing methods.

MXBeanMapping and @MXBeanMappingClass

The most important change is the new class javax.management.openmbean.MXBeanMapping:

public abstract class MXBeanMapping{
    protected MXBeanMapping(Type javaType, OpenType<?> openType);
    public final Type getJavaType();
    public final OpenType<?>getOpenType();
    public final Class<?>getOpenClass();
    public abstract Object fromOpenValue(Object openValue) throws InvalidObjectException;
    public abstract Object toOpenValue(Object javaValue) throws OpenDataException;
    public void checkReconstructible() throws InvalidObjectException;
}

Suppose we want to define a mapping for the class MyLinkedList, which looks like this:

public class MyLinkedList {
    public MyLinkedList(String name, MyLinkedList next) {...}
    public String getName() {...}
    public MyLinkedList getNext() {...}
}

This is not a valid type for MXBeans, because it contains a self-referential property "next" defined by the getNext() method. (This example comes from Simone Bordet.) So we would like to specify a mapping for it explicitly. When an MXBean interface contains MyLinkedList, that will be mapped into a String[], which is a valid Open Type.

To define this mapping, we first subclass MXBeanMapping:

public class MyLinkedListMapping extends MXBeanMapping {
    public MyLinkedListMapping(Type type) throws OpenDataException {
        super(MyLinkedList.class, ArrayType.getArrayType(SimpleType.STRING));
        if (type != MyLinkedList.class)
            throw new OpenDataException("Mapping only valid for MyLinkedList");
    }

    @Override
    public Object fromOpenValue(Object openValue) throws InvalidObjectException {
        String[] array = (String[]) openValue;
        MyLinkedList list = null;
        for (int i = array.length - 1; i >= 0; i--)
            list = new MyLinkedList(array[i], list);
        return list;
    }

    @Override
    public Object toOpenValue(Object javaValue) throws OpenDataException {
        ArrayList<String> array = new ArrayList<String>();
        for (MyLinkedList list = (MyLinkedList) javaValue; list != null; list = list.getNext())
            array.add(list.getName());
        return array.toArray(new String[0]);
    }
}

The call to the superclass constructor specifies what the original Java type is (MyLinkedList.class) and what Open Type it is mapped to (ArrayType.getArrayType(SimpleType.STRING)). The fromOpenValue method says how we go from the Java type to the Open Type, and the toOpenValue method says how we go from the Open Type to the Java type.

With this mapping defined, we can annotate MyLinkedList with the new annotation @javax.management.openmbean.MXBeanMappingClass:

@MXBeanMappingClass(MyLinkedListMapping.class)
public class MyLinkedList {...}

Now we can use MyLinkedList in an MXBean interface and it will work. This satisfies use case 1 above.

MXBeanMappingFactory and @MXBeanMappingFactoryClass

If we are unable to annotate individual classes, then we can define a mapping factory that is consulted every time a type needs to be mapped. This is also useful if we would like to apply the same set of rules across a whole set of classes (for example, any class that implements List<E>is mapped in the same way as List<E>).

A mapping factory is a subclass of javax.management.openmbean.MXBeanMappingFactory:

public abstract class MXBeanMappingFactory{
    public static final MXBeanMappingFactoryDEFAULT;
    protected MXBeanMappingFactory();
    public abstract MXBeanMappingmappingForType(Type t, MXBeanMappingFactory f)
    throws OpenDataException;
}

For example, suppose we can't change MyLinkedList, so we can't add the @MXBeanMappingClass annotation to it. We can achieve the same effect by defining a mapping factory as follows:

public classMyLinkedListMappingFactory extends MXBeanMappingFactory {
    public MyLinkedListMappingFactory() {}

    @Override
    public MXBeanMapping mappingForType(Type t, MXBeanMappingFactory f)
    throws OpenDataException {
        if (t == MyLinkedList.class)
            return new MyLinkedListMapping(t);
        else
            return MXBeanMappingFactory.DEFAULT.mappingForType(t, f);
    }
}

Now we can add the new annotation javax.management.openmbean.MXBeanMappingFactoryClass to any MXBean interface that references MyLinkedList:

@MXBeanMappingFactoryClass(MyLinkedListMappingFactory.class)
public interface SomethingMXBean {
    public MyLinkedList getSomething();
}

This satisfies use case 2 above.

New StandardMBean constructors and JMX.newMXBeanProxy method

The existing class StandardMBean is used to make instances of Standard MBeans and MXBeans when you need to control some aspects of their behaviour, such as the descriptions in the MBeanInfo. We can extend the set of constructors as follows:

public class StandardMBean implements DynamicMBean {
    ...existing constructors...
    public <T> StandardMBean(T impl, Class<T> intf, MBeanOptions options);
    protected StandardMBean(Class<?> intf, MBeanOptions options);
}

The options parameter allows us to specify a number of potentially interesting things:

  • an MXBeanMappingFactory (this is the case we're looking at here)
  • whether this is an MXBean or not
  • perhaps the contents of the MBeanInfo, as an alternative to subclassing and overriding e.g. getDescription(MBeanAttributeInfo)
  • whether the methods of MBeanRegistration should be forwarded to the implementation object if it implements that interface

The question of what the MBeanOptionstype is is still open. It could be a Map<String, ?> where each option is represented by a string constant. This is the approach taken by the JMX Remote API, for example. Or, it could be a special-purpose class called MBeanOptions with methods to set each of the options. I plan to write more on this later; for now I'll assume it's the MBeanOptions option.

If you need to create an MXBean that implements the SomethingMXBean interface above and uses theMyLinkedListMappingFactory, but you can't add an annotation to SomethingMXBean, then you can do so using either of the two existing ways to use StandardMBean, subclassing or delegation, but supplying an option:

MBeanOptions options = MBeanOptions.DEFAULT.withMXBeanMappingFactory(
    new MyLinkedListMappingFactory);
// either use delegation:
SomethingMXBean impl = new SomethingImpl();
StandardMBean mbean = new StandardMBean(impl, SomethingMXBean.class, options);
// or use subclassing:
public class MySomething extends StandardMBean implements SomethingMXBean {
    public MySomething() {
        super(SomethingMXBean.class, options);
    }
}

If you have a custom mapping in your MBean server, then you need the same custom mapping in a client if the client is making a proxy. So we add another overloading of JMX.newMXBeanProxy:

public class JMX {
    ...
    public static <T> TnewMXBeanProxy(
        MBeanServerConnection mbsc,
        ObjectName objectName,
        Class<T> intf,
        MBeanOptionsoptions);
    ...
}

Using this, you can create a proxy like this:

SomethingMXBean proxy = JMX.newMXBeanProxy(
    mbsc, objectName, SomethingMXBean.class,options);

Here the options object can be the same as before.

This satisfies use case 3 above.

Interoperation when there are custom types

MXBeans map arbitrary Java types to Open Types. The addition of custom mappings doesn't change this - the result still has to be Open Types. So for generic clients like JConsole, nothing changes when custom types are added into the mix.

A client that is aware of the MXBean interfaces in use (like SomethingMXBean) can construct a proxy. To do that, it must have the interface available. If the interface has a @MXBeanMappingFactoryClassannotation, or if it contains a type that has a @MXBeanMappingClass annotation, then the classes referenced by those annotations must be present in the client too. It usually isn't any more difficult to arrange for the mapping classes to be present than to arrange for the original MXBean interface to be present.

If the mapping has been specified on the server using an explicit MXBeanMappingFactory, then the same or an equivalent factory must be used on the client. This is the case where there is the most risk of inconsistency. To help check that client and server are using the same MXBeanMappingFactory, the Descriptor of an MXBean using an MXBeanMappingFactory will contain a field naming the factory class, MyLinkedListMappingFactory in the examples above.

Some details

Here are some details I omitted above for clarity.

The methods toOpenValue and fromOpenValue have inconsistentthrows clauses. This reflects a similar inconsistency in the MXBean spec, itself due to historical reasons.

The Java type and Open Type in MXBeanMapping must be supplied to the constructor by the subclass, and cannot be changed thereafter. This could be a limitation but I cannot currently see any cases where you would not be able to supply values to the super-constructor call. MXBeanMapping is a class and not an interface for a number of reasons, notably that it allows us to have final methods (getOpenType, getOpenClass, getJavaType) and methods with default implementations (checkReconstructible). MXBeanMappingFactory is a class not an interface so that we can add methods to it in later versions of the API if necessary.

The method MXBeanMapping.checkReconstructible() is used to determine if it is possible to map back from a value of the OpenType to a value of the original Java type. The default implementation does nothing. A subclass can override this method to throw OpenDataException if this mapping is not possible. See the MXBean specification for a discussion of reconstructible types.

MXBeanMappingClass can be defined like this:

@Documented @Retention(RUNTIME) @Target(TYPE) @Inherited
public @interface MXBeanMappingClass {
    Class<? extends MXBeanMapping> value();
}

The use of Class<? extends MXBeanMapping> means that if you write @MXBeanMappingClass(Something.class) and Something is not a subclass of MXBeanMapping, then you will get a compile error.

The class mentioned in the annotation must have a public constructor with a single parameter of type java.lang.reflect.Type. Unfortunately we can't get the compiler to check that!

The @Inherited annotation implies that if you subclass MyLinkedList then the subclass will inherit the same MXBeanMapping, which might not be what you want. But not inheriting the mapping seems more likely to surprise users.

The signature of MXBeanMappingFactory.mappingForType is this:

public MXBeanMapping mappingForType(Type t, MXBeanMappingFactory f)
    throws OpenDataException

The MXBeanMappingFactory parameter ensures that the same mappings are applied recursively. So if the type to be mapped is MyLinkedList[] in the example above, the mapping will proceed as follows:

MyLinkedListMappingFactory.mappingForType(MyLinkedList[].class, MyLinkedListMappingFactory) →
    MXBeanMappingFactory.DEFAULT.mappingForType(MyLinkedList[].class, MyLinkedListMappingFactory) →
        MyLinkedListMappingFactory.mappingForType(MyLinkedList.class, MyLinkedListMappingFactory)
        ← MyLinkedListMapping
    ← ArrayMapping(MyLinkedListMapping)
← ArrayMapping(MyLinkedListMapping)

where ArrayMapping is the private class that the default mapping factory uses to map arrays.

The key point here is that, even though MyLinkedListMappingFactory forwards any type it doesn't handle to the default mapping factory, the default factory will rerun anycontained type through the user-specified mapping factory. So MyLinkedListMappingFactory doesn't have to recognize MyLinkedList[] or List<MyLinkedList> or whatever in addition to plain MyLinkedList. The general rules for SomeType[] orList<SomeType> in the default factory will apply, and then MyLinkedListMappingFactory will be applied to the contained SomeType, and do the right thing if that is MyLinkedList.

Open questions

This is still a draft proposal, and some questions remain.

As I mentioned above, the exact type of the MBeanOptions parameter to various methods is yet to be determined.

Should the @MXBeanMappingFactory option be inherited? For example, if you define

public interface SubSomethingMXBean extends SomethingMXBean {...}

should you inherit the @MXBeanMappingFactory annotation from SomethingMXBean? I think the answer must be yes, but what if SubSomethingMXBean inherits from more than one MXBean interface, and they have different @MXBeanMappingFactory annotations?

Should the @MXBeanMappingFactory option be applicable to packages? A reminder that you can annotate packages by creating a file called package-info.java in the package with contents like this:

@javax.management.openmbean.MXBeanMappingFactory(MyLinkedListMappingFactory.class)
package com.example.mbeans;

If @MXBeanMappingFactory can apply to packages and is also inherited from superinterfaces then we may have some complicated rules for precedence between the two.

Conclusion

In conclusion, we're specifying something quite simple: how to extend the standard MXBean mappings with custom mappings. But the details turn out not to be so simple!

Acknowledgements

This specification has been discussed in the JSR 255 Expert Group. Mandy Chungalso had some very helpful comments.

[Tags: jmx, mxbeans.]