FULL PRODUCT VERSION :
Java(TM) SE Runtime Environment (build 1.7.0_67-b01)
Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)
Reproduced also with 1.7.0_25-b17
ADDITIONAL OS VERSION INFORMATION :
Win 7 64 bit: Microsoft Windows [Version 6.1.7601]
(reproduced on several machines, also one with Win XP)
A DESCRIPTION OF THE PROBLEM :
- Error: After the Weak/Soft references in PropertyDescriptors have been cleared, the returned propertyType is not the concrete implementation type but the interface type: this completely breaks our code at a central point and renders our web server application unusable by causing exceptions on important client requests.
- System architecture: Client/Server application, Server uses JPA entity classes and corresponding DTO classes for client communication, both implementing same interfaces, which declare the getters (for brevity, test case contains only interfaces and entity classes).
- Error occurrence: After the server has been running for several hours, the GC clears weak/soft references in PropertyDescriptors. When retrieving PropertyDescriptor.propertyType after that, the interface type is returned instead of the correct implementation class type. In the test case, weak/soft references are cleared manually to emulate the effect.
- Analysis: Supposed reasons for failing to get expected propertyType and writeMethod:
After PropertyDescriptor.readMethodRef has been cleared, PropertyDescriptor.getReadMethod() calls Introspector.findMethod(cls, readMethodName, 0), and ultimately internalFindMethod(cls, methodName, argCount, args): the latter finds the first method matching readMethod name and parameters, which is (at least on the machines where we have tested) the interface method and not the implementing class method. Hence, the read method is set to it (in the test case: 'IAddress.getCity: ICity' instead of 'Address.getCity: City') and the propertyType to the interface type ICity instead of City.
As an aftereffect, getWriteMethod() tries to find a setter matching the propertyType ICity and finds none, because the only present setter Address.setCity(City) expects a City parameter: so writeMethod is set to null instead of the correct setter.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Start the given test case: results are output on console. The weak/soft references are cleared manually to emulate the effect of GC run, which takes place after the application has been running for several hours.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The descriptor must be same before and after clearing weak references:
java.beans.PropertyDescriptor[name=city; propertyType=class City; readMethod=public City Address.getCity(); writeMethod=public void Address.setCity(City)]
ACTUAL -
After clearing weak references, the descriptor has propertyType=interface ICity and writeMethod=null. The corresponding console output is:
Descriptor before clearing weak references: java.beans.PropertyDescriptor[name=city; propertyType=class City; readMethod=public City Address.getCity(); writeMethod=public void Address.setCity(City)]
Descriptor after clearing weak references : java.beans.PropertyDescriptor[name=city; propertyType=interface ICity; readMethod=public ICity Address.getCity()]
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.ref.Reference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
public class PropertyDescriptorBugDemo {
private static void testCityGetter() throws Exception {
System.out.println("Expected results:"
+ "\n- the propertyType in the descriptor must be the same after clearing weak references"
+ "\n- the writeMethod is still present after clearing weak references");
PropertyDescriptor descriptor = ReflUtil.findDescriptorFor(Address.class, "city");
callGetters(descriptor); // force initialization
System.out.println("Descriptor before clearing weak references: " + descriptor);
ReflUtil.clearWeakReferencesInPropertyDescriptorsFor(Address.class);
callGetters(descriptor); // force initialization
System.out.println("Descriptor after clearing weak references : " + descriptor);
}
private static void callGetters(PropertyDescriptor descriptor) {
descriptor.getReadMethod();
descriptor.getWriteMethod();
descriptor.getPropertyType();
}
public static void main(String[] args) throws Exception {
testCityGetter();
}
}
interface IAddress {
ICity getCity();
}
interface ICity {
String getName();
}
class City implements ICity {
private String name;
@Override
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Address implements IAddress {
private City city;
@Override
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
}
class ReflUtil {
private static final List<String> REFERENCE_FIELD_NAMES = Arrays.asList("propertyTypeRef", "readMethodRef", "writeMethodRef");
public static void clearWeakReferences(PropertyDescriptor descriptor) throws Exception {
for (String referenceFieldName : REFERENCE_FIELD_NAMES) {
Field refField = PropertyDescriptor.class.getDeclaredField(referenceFieldName);
refField.setAccessible(true);
Reference<Method> reference = (Reference<Method>) refField.get(descriptor);
if (reference != null) {
reference.clear();
}
}
}
public static void clearWeakReferencesInPropertyDescriptorsFor(Class<?>... classes) throws Exception {
for (Class<?> clazz : classes) {
for (PropertyDescriptor descriptor : Introspector.getBeanInfo(clazz).getPropertyDescriptors()) {
clearWeakReferences(descriptor);
}
}
}
public static PropertyDescriptor findDescriptorFor(Class<?> clazz, String propertyName) throws Exception {
for (PropertyDescriptor descriptor : Introspector.getBeanInfo(clazz).getPropertyDescriptors()) {
if (propertyName.equals(descriptor.getName())) {
return descriptor;
}
}
return null;
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
The situation is to some extent alleviated when providing own property descriptors for the own beans, which therefore subclass SimpleBeanInfo and return "guarded" property descriptors checking that they always have the correct property type and using Method.isBridge() to check that they've got the right readMethod. Unfortunately, even this inelegant add-on doesn't resolve all issues, I still get sporadic errors (e.g. NPE when accessing propertyType or writeMethod). The bottom line is that we will not be able to use interfaces for entity and DTO classes to full extent in productive code, until this issue is resolved, which noticeably constrains our architecture and optimization decisions (being forced to avoid interfaces is not what you'd want in object-oriented development ;)).