Java Bean Introspector and Covariant/Generic Returns

April 11, 2012

Unfortunately, there is a rather unsettling bug in the JDK in the java.beans package for Introspector when handling bridge methods. For those unfamiliar with that term, JDK 5 created a new modifier called bridge that is only set by the compiler (ie: there is no actual keyword for it in the language that you can use). Bridge methods came along because of the new covariant returns and generics. For example, consider the following two classes:

public class Base {
    public Number getValue() { return null; }
}
 
public class Int extends Base {
    public Integer getValue() { return Integer.valueOf(1); }
}

Java allows you to override the return type of subclasses as long as the overridden type is compatible. Behind the scenes, the compiler creates two methods in the actual Int class:

public getValue()Ljava/lang/Integer;
public volatile bridge getValue()Ljava/lang/Number;

The first method is the overridden return type and the actual method that is invoked. The second method is a bridge method created by the compiler. The bridge method purely invokes the first method but returns the value cast as a Number. This has to occur since dependent classes that depend on the Base class can receive the expected Number return type whereas classes depending on Int directly can have the return type as an Integer.

Generics has the same problem with using the typed variable as the return value:

public class Base<T extends Number> {
    public T getValue() { return null; }
}
 
public class Int extends Base<Integer> {
    public Integer getValue() { return Integer.valueOf(1); }
}

This again results in two methods for the getValue method.

The reason I mention this is that if you were to invoke java.beans.Introspector.getBeanInfo(Int.class) and then lookup the property for the value property, the property type and read method would be the bridge methods on some systems. It is actually random since there is no defined ordered of methods in the class file format, so whichever appears first is the one that is selected. On some machines that may be the Integer-based method, while on others it will be the bridge method. In my opinion this is a huge gaping issue as several libraries depend on the underlying java.beans framework including Commons Beanutils. They all exhibit similar issues as they all rely on the JDK.

This is actually worse than just the return types. For example, if you have or use a library built around iterating the properties and then checking the read method for annotations, you may get the wrong method. If the bridge method is returned it will not have any annotations on it. Instead you would need to get the proper method to read the annotations. This can cause unexpected issues that would be fairly hard to diagnose and debug.

Hopefully this will be resolved in JDK 7. Until then, I have to fix my local code to hack the introspection library to walk each property, check if the read method is a bridge method and if so, lookup the actual method. Here is a quick example on how to do that:

// lookup bean info for given class
BeanInfo info = Introspector.getBeanInfo(clazz);
if (info != null) {
    // get list of descriptors and iterate to validate each property
    PropertyDescriptor[] descriptors = info.getPropertyDescriptors();
    for (int i = 0; i < descriptors.length; i++) {
        PropertyDescriptor descriptor = descriptors [i];
 
        // check if read method is bridge and lookup real method
        Method readMethod = descriptor .getReadMethod();
        if (readMethod != null && readMethod.isBridge()) {
            Method method = clazz.getMethod(
                readMethod.getName(), readMethod.getParameterTypes()
            );
 
            // if method found, update read method
            // which also updates property type
            if (method != null) {
                descriptor.setReadMethod(method);
 
                // TODO: lookup write method with same type in case the write
                // method is the bridge method
            }
        }
    }
 
    // return array of found descriptors
    return descriptors;
}

You would most likely want to perform similar logic on the write methods as write methods may also have bridge methods when dealing with generics. In this case, I would suggest looking up a setXyz method where the singular parameter type is the same as the return type from the newly found read method. If the method is found, replace the existing write method with that method.

If you are wondering why getMethod returns only the non-bridged type whereas calling getMethods returns both methods in the array, the answer lies in the source code for that method in the java.lang.Class class. The getMethod call eventually invokes private static Method searchMethods. That search method has a condition on it that it loops through all methods that match the name but the array does not terminate early upon first match. Instead, it keeps searching and if another matching method is found, then it uses the one with the more precise return type (ie: covariant returns). Since covariant returns must always be subclasses of the base, the method just finds the return type with the lowest subclass, which should be the non-bridge method.

For more information, see the following JDK bugs on the topic:

Comments are closed.