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 costumeBeanInfo
areiaPropertyDescriptor
s 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 costumeBeanInfo
s, que permitem o controle programático dePropertyDescriptor
s 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 noSoftReference
s 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 setterMethod
s 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ê?