Обновление: Oracle подтвердила, что это ошибка.
Резюме: некоторые пользовательские BeanInfo
и PropertyDescriptor
, которые работают в JDK 1.6, не работают в JDK 1.7, а некоторые терпят неудачу только после того, как сборщик мусора запустился и очистил определенные SoftReferences.
Изменить: это также сломает
ExtendedBeanInfo
в Spring 3.1, как указано в нижней части поста.Изменить: Если вы вызываете разделы 7.1 или 8.3 спецификации JavaBeans, объясните именно там, где эти части спецификации требуютчего-либо. То язык не является императивным или нормативным в этих разделах. То язык в этих разделах - это язык примеров, которые в лучшем случае неоднозначно как спецификация. Кроме того, API
BeanInfo
специально позволяет изменить поведение по умолчанию, и это явно сломан во втором примере ниже.
Спецификация Java Beans ищет методы установки по умолчанию с возвращаемым типом void, но позволяет настраивать методы получения и установки с помощью java.beans.PropertyDescriptor
. Самый простой способ его использования — указать имена геттера и сеттера.
new PropertyDescriptor("foo", MyClass.class, "getFoo", "setFoo");
Это работало в JDK 1.5 и JDK 1.6, чтобы указать имя установщика, даже если его тип возвращаемого значения не пуст, как в приведенном ниже тестовом примере:
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import org.testng.annotations.*;
/**
* Shows what has worked up until JDK 1.7.
*/
public class PropertyDescriptorTest
{
private int i;
public int getI() { return i; }
// A setter that my people call "fluent".
public PropertyDescriptorTest setI(final int i) { this.i = i; return this; }
@Test
public void fluentBeans() throws IntrospectionException
{
// This throws an exception only in JDK 1.7.
final PropertyDescriptor pd = new PropertyDescriptor("i",
PropertyDescriptorTest.class, "getI", "setI");
assert pd.getReadMethod() != null;
assert pd.getWriteMethod() != null;
}
}
Пример пользовательских BeanInfo
, которые позволяют программно управлять PropertyDescriptor
s в спецификации Java Beans все используют возвращаемые типы void для своих сеттеров, но ничто в спецификации не указывает на то, что эти примеры являются нормативными, и теперь поведение этой низкоуровневой утилиты изменилось в новых классах Java, которые, как оказалось, нарушили некоторый код, над которым я работаю.
В пакет java.beans
между JDK 1.6 и 1.7 внесены многочисленные изменения, но то, что приводит к сбою этого теста, похоже, находится в этом различии:
@@ -240,11 +289,16 @@
}
if (writeMethodName == null) {
- writeMethodName = "set" + getBaseName();
+ writeMethodName = Introspector.SET_PREFIX + getBaseName();
}
- writeMethod = Introspector.findMethod(cls, writeMethodName, 1,
- (type == null) ? null : new Class[] { type });
+ Class[] args = (type == null) ? null : new Class[] { type };
+ writeMethod = Introspector.findMethod(cls, writeMethodName, 1, args);
+ if (writeMethod != null) {
+ if (!writeMethod.getReturnType().equals(void.class)) {
+ writeMethod = null;
+ }
+ }
try {
setWriteMethod(writeMethod);
} catch (IntrospectionException ex) {
Вместо того, чтобы просто принять с правильным именем и параметрами, PropertyDescriptor
теперь также проверяет тип возвращаемого значения, чтобы определить, является ли он нулевым, поэтому плавный сеттер больше не используется. PropertyDescriptor
выдает IntrospectionException
в этом случае: «Метод не найден: setI».
Однако проблема гораздо более коварна, чем простой тест выше. Другой способ указать методы получения и установки в PropertyDescriptor
для пользовательского BeanInfo
— использовать фактические объекты Method
:
@Test
public void fluentBeansByMethod()
throws IntrospectionException, NoSuchMethodException
{
final Method readMethod = PropertyDescriptorTest.class.getMethod("getI");
final Method writeMethod = PropertyDescriptorTest.class.getMethod("setI",
Integer.TYPE);
final PropertyDescriptor pd = new PropertyDescriptor("i", readMethod,
writeMethod);
assert pd.getReadMethod() != null;
assert pd.getWriteMethod() != null;
}
Теперь код выше пройдет модульный тест как в 1.6, так и в 1.7, но код начнет давать сбой в какой-то момент времени в течение жизни экземпляра JVM из-за того же самого изменения, которое приводит к немедленному сбою первого примера. .Во втором примере единственным признаком того, что что-то пошло не так, является попытка использовать пользовательский PropertyDescriptor
. Сеттер имеет значение null, и в большинстве служебных программ это означает, что свойство доступно только для чтения.
Код в разнице находится внутри PropertyDescriptor.getWriteMethod()
. Он выполняется, когда SoftReference
, содержащий фактический установщик Method
, пуст. Этот код вызывается конструктором PropertyDescriptor
в первом примере, который использует метод доступа nameвыше, потому что изначально в SoftReference не сохранен метод
содержит геттер и сеттер.Method
.
Во втором примере метод чтения и метод записи сохраняются в объектах SoftReference
в PropertyDescriptor
конструктором, и сначала они будут содержать ссылки на readMethod
и writeMethod
getter и setter Method
передаются конструктору. Если в какой-то момент эти Soft-ссылки будут очищены, как разрешено делать сборщику мусора (и это будет сделано), то код getWriteMethod()
увидит, что SoftReference
возвращает null , и он попытается обнаружить сеттер. На этот раз, используя тот же путь кода внутри PropertyDescriptor
, который приводит к сбою первого примера в JDK 1.7, он установит для записи Method
значение null
, так как возвращаемый тип не void
.(Тип возвращаемого значения нечасть сигнатуры метода Java .)
Такое изменение поведения с течением времени при использовании пользовательского BeanInfo
может быть крайне запутанно. Попытка продублировать условия, которые заставляют сборщик мусора очищать эти конкретные SoftReferences
, также утомительна (хотя, возможно, некоторые инструментальные насмешки могут помочь).
Класс Spring ExtendedBeanInfo
имеет тесты, аналогичные приведенным выше. Вот фактический модульный тест Spring 3.1.1 из ExtendedBeanInfoTest
, который пройдет в режиме модульного тестирования, но тестируемый код не будет работать в коварном режиме после GC::
@Test
public void nonStandardWriteMethodOnly() throws IntrospectionException {
@SuppressWarnings("unused") class C {
public C setFoo(String foo) { return this; }
}
BeanInfo bi = Introspector.getBeanInfo(C.class);
ExtendedBeanInfo ebi = new ExtendedBeanInfo(bi);
assertThat(hasReadMethodForProperty(bi, "foo"), is(false));
assertThat(hasWriteMethodForProperty(bi, "foo"), is(false));
assertThat(hasReadMethodForProperty(ebi, "foo"), is(false));
assertThat(hasWriteMethodForProperty(ebi, "foo"), is(true));
}
Одно из предположений состоит в том, что мы может сохранить текущий код, работающий с непустыми сеттерами, предотвращая только программную доступность методов сеттера. Похоже, это сработает, но это скорее хак вокруг измененного поведения в JDK 1.7.
В: Есть ли определенная спецификация, утверждающая, что непустые сеттеры должны быть преданы анафеме? Я ничего не нашел и сейчас считаю это ошибкой в библиотеках JDK 1.7. Я ошибаюсь и почему?