Por que o comportamento do PropertyDescriptor mudou do Java 1.6 para 1.7?

Atualização: A Oracle confirmou isso como um bug.

Resumo: certo costumeBeanInfoareiaPropertyDescriptors que funcionam no JDK 1.6 falham no JDK 1.7, e alguns apenas falham após a Coleta de Garbage ter executado e limpo certas SoftReferences.

Edit: Isso também irá quebrar oExtendedBeanInfo na primavera 3.1, como indicado na parte inferior do post.

Edit: Se você invocar seções 7.1 ou 8.3 da especificação JavaBeans, explique exatamente onde essas partes da especificaçãoexigir qualquer coisa. A linguagem não é imperativa ou normativa nessas seções. A linguagem nessas seções é a dos exemplos, que são, na melhor das hipóteses, ambíguos como uma especificação. Além disso, oBeanInfo API especificamente permite alterar o comportamento padrão, e é claramente quebrado no segundo exemplo abaixo.

A especificação Java Beans procura os métodos padrão setter com um tipo de retorno void, mas permite a customização dos métodos getter e setter através de umjava.beans.PropertyDescriptor. A maneira mais simples de usá-lo foi especificar os nomes do getter e do setter.

new PropertyDescriptor("foo", MyClass.class, "getFoo", "setFoo");

Isso funcionou no JDK 1.5 e no JDK 1.6 para especificar o nome do setter mesmo quando seu tipo de retorno não é vazio como no caso de teste abaixo:

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;
    }
}

O exemplo do costumeBeanInfos, que permitem o controle programático dePropertyDescriptors na especificação Java Beans usam todos os tipos de retorno void para seus setters, mas nada na especificação indica que esses exemplos são normativos, e agora o comportamento deste utilitário de baixo nível mudou nas novas classes Java, que por acaso quebraram algum código no qual estou trabalhando.

Existem inúmeras mudanças nojava.beans pacote entre o JDK 1.6 e 1.7, mas aquele que faz com que este teste falhe parece estar neste 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) {

Em vez de simplesmente aceitar o método com o nome e os parâmetros corretos, oPropertyDescriptor agora também está verificando o tipo de retorno para ver se é nulo, então o setter fluente não é mais usado. oPropertyDescriptor lança umIntrospectionException Neste caso: "Método não encontrado: setI".

No entanto, o problema é muito mais insidioso do que o teste simples acima. Outra maneira de especificar os métodos getter e setter noPropertyDescriptor para um costumeBeanInfo é usar o realMethod objetos:

@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;
}

Agora o código acimavai passar um teste de unidade em 1.6 e 1.7, mas o código começará a falhar em algum momento durante a vida da instância da JVM devido à mesma alteração que faz com que o primeiro exemplo falhe imediatamente. No segundo exemplo, a única indicação de que alguma coisa deu errado vem ao tentar usar oPropertyDescriptor. O setter é nulo e a maioria dos códigos de utilitários leva isso para significar que a propriedade é somente leitura.

O código no diff está dentroPropertyDescriptor.getWriteMethod(). Ele executa quando oSoftReference segurando o setter realMethod está vazia. Este código é invocado peloPropertyDescriptor construtor no primeiro exemplo que usa o método do acessadornomes acima porque inicialmente não háMethod salvo noSoftReferences segurando o getter e setter reais.

No segundo exemplo, o método read e o método write são armazenados emSoftReference objetos noPropertyDescriptor pelo construtor e, a princípio, estes conterão referências aoreadMethod ewriteMethod getter e setterMethods dado ao construtor. Se, em algum momento, essas referências do Soft forem limpas, como o Garbage Collector pode fazer (e o fará), então ogetWriteMethod() código vai ver que oSoftReference retorna null, e ele tentará descobrir o setter.Desta vez, usando o mesmo caminho de código dentroPropertyDescriptor que faz com que o primeiro exemplo falhe no JDK 1.7, ele irá definir a gravaçãoMethod paranull porque o tipo de retorno não évoid. (O tipo de retorno énão parte de um Javaassinatura do método.)

Tendo o comportamento mudar assim ao longo do tempo ao usar um personalizadoBeanInfo pode ser extremamente confuso. Tentando duplicar as condições que fazem com que o Garbage Collector limpe essasSoftReferences também é entediante (embora talvez alguma simulação de instrumentação possa ajudar.)

A primaveraExtendedBeanInfo classe tem testes semelhantes aos acima. Aqui está um teste de unidade Spring 3.1.1ExtendedBeanInfoTest que passará no modo de teste de unidade, mas o código que está sendo testado falhará no modo insidioso pós-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));
}

Uma sugestão é que podemos manter o código atual trabalhando com os setters não nulos, impedindo que os métodos de setter sejam acessíveis apenas suavemente. Parece que funcionaria, mas isso é um truque em torno do comportamento alterado no JDK 1.7.

P: Há alguma especificação definitiva afirmando que os setters não nulos devem ser anátema? Eu não encontrei nada, e atualmente considero isso um bug nas bibliotecas do JDK 1.7. Estou errado e por quê?

questionAnswers(4)

yourAnswerToTheQuestion