Почему поведение PropertyDescriptor изменилось с Java 1.6 до 1.7?

Обновление: Oracle подтвердила это как ошибку.

Сводка: Определенные пользовательские BeanInfo и PropertyDescriptor, которые работают в JDK 1.6, не работают в JDK 1.7, а некоторые только после сбоя Garbage Collection запускают и очищают определенные SoftReferences.

Изменить: это также сломает ExtendedBeanInfo в Spring 3.1, как указано в нижней части сообщения.

Изменить: если вы вызываете разделы 7.1 или 8.3 спецификации JavaBeans, объясните точно, где те части спецификации требуют что-либо. язык не является обязательным или нормативным в этих разделах. язык в этих разделах - это примеры, которые в лучшем случае двусмысленный как спецификация. Кроме того, API BeanInfoв частности, позволяет изменить поведение по умолчанию, и это явно нарушена во втором примере ниже.

Спецификация Java Beans ищет методы установки по умолчанию с типом возвращаемого типа void, но позволяет настраивать методы getter и setter с помощью 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 s, который позволяет программному управлению PropertyDescriptor в спецификации Java Beans использовать все типы void return для своих сеттеров, но ничто в спецификации не указывает, что эти примеры являются нормативными, и теперь поведение этой низкоуровневой утилиты изменилось в новых классах Java, что, случается, сломало некоторый код, на котором я работаю.

В пакете java.beans существует множество изменений между JDK 1.6 и 1.7, но тот, который вызывает этот тест, оказывается в этом diff:

@@ -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".

Однако проблема намного более коварна, чем простой тест выше. Другой способ указать методы getter и setter в 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;
}

Теперь приведенный выше код передаст unit test как в 1.6, так и в 1.7, но код начнет сбой в какой-то момент времени в течение срока действия экземпляра JVM из-за того же изменения, которое вызывает первый пример сбой немедленно. Во втором примере единственное указание на то, что что-то пошло не так, возникает при попытке использовать пользовательский PropertyDescriptor. Setter имеет значение null, и в большинстве случаев код утилиты принимает значение, означающее, что свойство доступно только для чтения.

Код в diff находится внутри PropertyDescriptor.getWriteMethod(). Он выполняется, когда SoftReference, содержащий фактический установщик Method, пуст. Этот код вызывается конструктором PropertyDescriptor в первом примере, который принимает имена метода доступа выше, потому что изначально нет Method, сохраненных в SoftReference, содержащих фактический геттер и сеттер.

Во втором примере метод чтения и метод записи хранятся в SoftReference объектах в PropertyDescriptor конструктором, и сначала они будут содержать ссылки на readMethod и writeMethod getter и setter Method, заданный конструктору. Если в какой-то момент эти Soft-ссылки очищаются, так как сборщик мусора разрешен (и он будет делать), тогда код getWriteMethod() увидит, что SoftReference возвращает значение null, и он попытается обнаружить установщика. На этот раз, используя тот же путь кода внутри PropertyDescriptor, который приведет к сбою первого примера в JDK 1.7, он установит запись Method в null, потому что тип возврата не является void. (Тип возврата не является частью сигнатуры метода .

Наличие изменения поведения, подобного этому со временем при использовании пользовательского BeanInfo, может быть чрезвычайно запутанным. Пытаться дублировать условия, которые заставляют сборщик мусора очищать те, что относятся к SoftReferences, также утомительно (хотя, возможно, может быть полезно какое-то инструментальное издевательство).

Класс Spring ExtendedBeanInfo имеет тесты, аналогичные приведенным выше. Вот фактический Spring 3.1.1 unit test из ExtendedBeanInfoTest, который пройдет в режиме unit test, но проверенный код завершится неудачно в пост-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));
}

Одно из предложений заключается в том, что мы можем поддерживать текущий код с не-void сеттерами, не позволяя методам setter быть только доступным для достижения цели. Кажется, что это сработает, но это скорее взломать измененное поведение в JDK 1.7.

Q: Есть ли какая-то определенная спецификация, указывающая, что не-void сеттеры должны быть анафемой? Я ничего не нашел, и в настоящее время считаю это ошибкой в ​​библиотеках JDK 1.7. Я ошибаюсь и почему?

Ответы

Ответ 1

Так как я нашел Spring 3.1.1 ExtendedBeanInfo модульные тесты, которые ожидают, что код не будет разбит, и потому что изменение поведения после сборки мусора, очевидно, является ошибкой, я отвечу на это и учтут номера ошибок Java, Ошибки все еще не видны во внешней базе данных Java-ошибок, но я надеюсь, что они появятся в какой-то момент:

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7172854 (Oracle закрыла это как дубликат ошибки ниже, поскольку они имеют одну и ту же причину, несмотря на различные проявления.)

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7172865

(Ошибки были отправлены 30 мая 2012 года.)

По состоянию на 20 июня 2012 года ошибки отображаются во внешней базе данных по ссылкам выше.

Ответ 2

Похоже, что спецификация не изменилась (для нее требуется недействительный сеттер), но реализация была обновлена, чтобы разрешить создание сетки void.

Спецификация:

http://www.oracle.com/technetwork/java/javase/documentation/spec-136004.html

Более подробно см. раздел 7.1 (методы доступа) и 8.3 (шаблоны проектирования для простых свойств)

См. некоторые более поздние ответы в этом вопросе stackoverflow:

Разрешает ли сеттер Java bean это?

Ответ 3

В разделе 8.2 указаны:

Однако в Java Beans использование имен методов и типов, соответствующих шаблонам проектирования, является необязательным. Если программист готов явно указать свои свойства, методы и события, используя интерфейс BeanInfo, тогда они могут вызывать свои методы и типы, которые им нравятся. Однако эти методы и типы все равно должны будут соответствовать требуемым типам подписей, так как это важно для их работы.

(выделено курсивом)

Кроме того, я верю, что сигнатуры методов, показанные в 7.1 и 8.3, фактически являются нормативными. Они являются примерами только в том смысле, что они используют "foo" в качестве имени свойства примера.

Ответ 4

Я также хотел бы сказать, что отказ от не-void seters - ошибка. Это просто делает свободное программирование невозможным. Вот почему это нужно изменить.