JDK-7172854 : PropertyDescriptor.getWriteMethod() is broken after GC clears SoftReferences
  • Type: Bug
  • Component: client-libs
  • Sub-Component: java.beans
  • Affected Version: 7
  • Priority: P3
  • Status: Closed
  • Resolution: Duplicate
  • OS: linux
  • CPU: x86
  • Submitted: 2012-05-30
  • Updated: 2014-10-13
  • Resolved: 2012-06-07
Related Reports
Relates :  
Description
FULL PRODUCT VERSION :
java version "1.7.0_04"
Java(TM) SE Runtime Environment (build 1.7.0_04-b20)
Java HotSpot(TM) 64-Bit Server VM (build 23.0-b21, mixed mode)


ADDITIONAL OS VERSION INFORMATION :
Linux jpb-twostep 3.0.0-19-generic #33-Ubuntu SMP Thu Apr 19 19:05:14 UTC 2012 x86_64 x86_64 x86_64 GNU/Linux


A DESCRIPTION OF THE PROBLEM :
The behavior of PropertyDescriptor has changed from JDK 1.6 (and previous versions) to JDK 1.7.  If a property setter has been set with PropertyDescriptor.setWriteMethod(), the PropertyDescriptor.getWriteMethod() will return the setter Method correctly only until some specific garbage collection activity occurs.

The setter Method is stored in a SoftReference inside PropertyDescriptor, and the garbage collection can clear that to null.  In many typical situations this causes no problems because the getWriteMethod() call will attempt to rediscover the setter Method to use based on the PropertyDescriptor.writeMethodName.

However, starting with JDK 1.7, code was added to PropertyDescriptor.getWriteMethod() that checks to see if the property setter Method has a void return type, which is typical of many setters on bean classes.  The void return type is not required by the Bean Specification 1.01 though.  The Bean Specification examples for setters do use void, but the Specification says only the method signature (which does not include the return type, according to section 8.4.2 of the Java Language Spec) must match the example design pattern, and besides that, the return type of a setter also has no effect on the ability of a setter method to be a setter method.

This deviation from the specified and historical behavior of PropertyDescriptor can cause serious and confusing errors because suddenly a writeable bean property becomes read-only as a result of garbage collection.  Both the Spring Framework and Stripes Framework (and possibly others) contain bean utility classes that cache PropertyDescriptor objects, and they depend on the PropertyDescriptor continuing to work as they did prior to JDK 1.7.

The customization of bean properties via custom BeanInfo classes has long been the accepted and specified way of overriding the default bean property discovery mechanism, but now it does not work property because some additional code checks to make sure only methods that more closely follow the default patterns for bean property accessors work.

REGRESSION.  Last worked in version 6u31

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the test program, which will allocate enough objects to cause garbage collection to occur and clear the SoftReference objects inside of PropertyDescriptors.



EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The PropertyDescriptor.getWriteMethod() should always continue to return the method that was specified to PropertyDescriptor.setWriteMethod(Method setter).
ACTUAL -
The test program finds that the PropertyDescriptor.getWriteMethod() initially returns the specified write method, but then begins at a later time to return null rather than the specified write method.

ERROR MESSAGES/STACK TRACES THAT OCCUR :
The test program produces results like this:

$ java -server -XX:+UseSerialGC -Xmx32m PropertyDescriptorTest
Allocated property descriptors.
Accessors were not null.
Pass 1 allocated temporary arrays.
java.lang.Throwable: Something is setting my write method to null!
        at PropertyDescriptorTest$1.setWriteMethod(PropertyDescriptorTest.java:34)
        at java.beans.PropertyDescriptor.getWriteMethod(PropertyDescriptor.java:303)
        at PropertyDescriptorTest.checkForNullAccessors(PropertyDescriptorTest.java:49)
        at PropertyDescriptorTest.fluentBeansByMethod(PropertyDescriptorTest.java:85)
        at PropertyDescriptorTest.main(PropertyDescriptorTest.java:99)
Exception in thread "main" java.lang.InternalError: Write method was null on PropertyDescriptor: PropertyDescriptorTest$1[name=i; propertyType=int; readMethod=public int PropertyDescriptorTest$BeanOne.getI()]
        at PropertyDescriptorTest.checkForNullAccessors(PropertyDescriptorTest.java:49)
        at PropertyDescriptorTest.fluentBeansByMethod(PropertyDescriptorTest.java:85)
        at PropertyDescriptorTest.main(PropertyDescriptorTest.java:99)

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.beans.*;
import java.lang.ref.*;
import java.lang.reflect.Method;
import java.util.*;

public class PropertyDescriptorTest
{
	public static class BeanOne
	{
		private int i;
		public int getI() { return i; }
		public BeanOne setI(final int i) { this.i = i; return this; }
	}

	public static class BeanTwo
	{
		private int i;
		public int getI() { return i; }
		public BeanTwo setI(final int i) { this.i = i; return this; }
	}

	public PropertyDescriptor makeDescriptor(final Class clazz) throws IntrospectionException, NoSuchMethodException
	{
		return new PropertyDescriptor("i",
									  clazz.getMethod("getI"),
									  clazz.getMethod("setI", Integer.TYPE)) {
			private boolean done = false;

			public PropertyDescriptor doneDone() { this.done = true; return this; }

			@Override public void setWriteMethod(final Method m) throws IntrospectionException {
				if (done) {
					if (m == null) {
						new Throwable("Something is setting my write method to null!").printStackTrace();
					}
					else {
						System.out.println("Something is setting my write method, but not to null.");
					}
				}
				super.setWriteMethod(m);
			}
		}.doneDone();
	}

	private void checkForNullAccessors(final List<PropertyDescriptor> list)
	{
		for (final PropertyDescriptor pd : list) {
			if (pd.getReadMethod() == null) throw new InternalError("Read method was null on PropertyDescriptor: " + pd);
			if (pd.getWriteMethod() == null) throw new InternalError("Write method was null on PropertyDescriptor: " + pd);
		}
	}

	private List allocateSomeMemory(final int times)
	{
		return new ArrayList() {{
			for (int i = 0; i < times; ++i) {
				int[] ar = new int[1000000];
				add(new SoftReference<int[]>(ar));
				add(new WeakReference<int[]>(ar));
				for (int j = 0; j < ar.length; j += ar.length / 100000) {
					final int val = ar[j];
					if (val != 0) throw new RuntimeException("Invalid default value:" + val);
				}
			}
		}};
	}

	public void fluentBeansByMethod() throws IntrospectionException, NoSuchMethodException
	{
		final ArrayList<PropertyDescriptor> list = new ArrayList<PropertyDescriptor>() {{
				// Create many PropertyDescriptor objects.
				for (int i = 0; i < 1000; ++i) {
					add(makeDescriptor(BeanOne.class));
					add(makeDescriptor(BeanTwo.class));
				}
			}};

		System.out.println("Allocated property descriptors.");

		int count = 0;
		for (int i = 0; i < 50; ++i) {
			++count;

			// Make sure the methods are not null.
			checkForNullAccessors(list);
			System.out.println("Accessors were not null.");

			// Put more pressure on the available memory.
			Object o = allocateSomeMemory(1000);

			System.out.println("Pass " + count + " allocated temporary arrays.");
		}

		System.out.println("Finished " + count + " passes without error.");
	}

	public static void main(final String[] argv) throws Exception
	{
		new PropertyDescriptorTest().fluentBeansByMethod();
	}
}

---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Libraries and applications that use PropertyDescriptors can work around this by preventing the Method instances for the property setters from ever becoming softly reachable.  One approach is to store references to all custom bean methods in static fields of the class objects of a custom BeanInfo, and makeing sure the class object itself is never garbage collected.

Users of the Spring ExtendedBeanInfo may run into more trouble if the problem occurs for them because they would have to do a fair amount of work to keep references to all setter methods that have non-void return types.

Comments
EVALUATION Attached test shows that new setter is not valid according JavaBeans spec, so the PropertyDescriptor could not find non-void method.
07-06-2012