Use ExtendedBeanInfo on an as-needed basis only

Prior to this change, CachedIntrospectionResults delegated to
ExtendedBeanInfo by default in order to inspect JavaBean
PropertyDescriptor information for bean classes.

Originally introduced with SPR-8079, ExtendedBeanInfo was designed to
go beyond the capabilities of the default JavaBeans Introspector in
order to support non-void returning setter methods, principally to
support use of builder-style APIs within Spring XML. This is a complex
affair, and the non-trivial logic in ExtendedBeanInfo has led to various
bugs including regressions for bean classes that do not declare
non-void returning setters.

This commit takes advantage of the new BeanInfoFactory mechanism
introduced in SPR-9677 to take ExtendedBeanInfo out of the default code
path for CachedIntrospectionResults. Now, the new
ExtendedBeanInfoFactory class will be detected and instantiated (per its
entry in the META-INF/spring.beanInfoFactories properties file shipped
with the spring-beans jar). ExtendedBeanInfoFactory#supports is invoked
for all bean classes in order to determine whether they are candidates
for ExtendedBeanInfo introspection, i.e. whether they declare non-void
returning setter methods.

If a class does not declare any such non-standard setter methods (the
99% case), then CachedIntrospectionResults will fall back to the
default JavaBeans Introspector. While efforts have been made to fix any
bugs with ExtendedBeanInfo, this change means that EBI will not pose
any future risk for bean classes that do not declare non-standard
setter methods, and also means greater efficiency in general.

Issue: SPR-9723, SPR-9677, SPR-8079
This commit is contained in:
Chris Beams
2012-09-09 16:04:40 +02:00
parent b50bb5071a
commit 5bcf68e25a
7 changed files with 194 additions and 50 deletions

View File

@@ -248,8 +248,8 @@ public class CachedIntrospectionResults {
}
}
if (beanInfo == null) {
// If none of the factories supported the class, use the default
beanInfo = new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass));
// If none of the factories supported the class, fall back to the default
beanInfo = Introspector.getBeanInfo(beanClass);
}
this.beanInfo = beanInfo;

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import org.springframework.core.Ordered;
/**
* {@link BeanInfoFactory} implementation that evaluates whether bean classes have
* "non-standard" JavaBeans setter methods and are thus candidates for introspection by
* Spring's {@link ExtendedBeanInfo}.
*
* <p>Ordered at {@link Ordered#LOWEST_PRECEDENCE} to allow other user-defined
* {@link BeanInfoFactory} types to take precedence.
*
* @author Chris Beams
* @since 3.2
* @see BeanInfoFactory
*/
class ExtendedBeanInfoFactory implements Ordered, BeanInfoFactory {
/**
* Return whether the given bean class declares or inherits any non-void returning
* JavaBeans setter methods.
*/
public boolean supports(Class<?> beanClass) {
for (Method method : beanClass.getMethods()) {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
if (Modifier.isPublic(method.getModifiers())
&& methodName.length() > 3
&& methodName.startsWith("set")
&& (parameterTypes.length == 1
|| (parameterTypes.length == 2 && parameterTypes[0].equals(int.class)))
&& !void.class.isAssignableFrom(method.getReturnType())) {
return true;
}
}
return false;
}
/**
* Return a new {@link ExtendedBeanInfo} for the given bean class.
*/
public BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
return new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass));
}
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}

View File

@@ -0,0 +1 @@
org.springframework.beans.ExtendedBeanInfoFactory

View File

@@ -17,6 +17,7 @@
package org.springframework.beans;
import java.beans.BeanInfo;
import java.beans.PropertyDescriptor;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -25,6 +26,9 @@ import test.beans.TestBean;
import org.springframework.core.OverridingClassLoader;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/**
* @author Juergen Hoeller
* @author Chris Beams
@@ -54,11 +58,32 @@ public final class CachedIntrospectionResultsTests {
}
@Test
public void customBeanInfoFactory() throws Exception {
CachedIntrospectionResults results = CachedIntrospectionResults.forClass(CachedIntrospectionResultsTests.class);
BeanInfo beanInfo = results.getBeanInfo();
public void shouldUseExtendedBeanInfoWhenApplicable() throws NoSuchMethodException, SecurityException {
// given a class with a non-void returning setter method
@SuppressWarnings("unused")
class C {
public Object setFoo(String s) { return this; }
public String getFoo() { return null; }
}
assertTrue("Invalid BeanInfo instance", beanInfo instanceof DummyBeanInfoFactory.DummyBeanInfo);
// CachedIntrospectionResults should delegate to ExtendedBeanInfo
CachedIntrospectionResults results = CachedIntrospectionResults.forClass(C.class);
BeanInfo info = results.getBeanInfo();
PropertyDescriptor pd = null;
for (PropertyDescriptor candidate : info.getPropertyDescriptors()) {
if (candidate.getName().equals("foo")) {
pd = candidate;
}
}
// resulting in a property descriptor including the non-standard setFoo method
assertThat(pd, notNullValue());
assertThat(pd.getReadMethod(), equalTo(C.class.getMethod("getFoo")));
assertThat(
"No write method found for non-void returning 'setFoo' method. " +
"Check to see if CachedIntrospectionResults is delegating to " +
"ExtendedBeanInfo as expected",
pd.getWriteMethod(), equalTo(C.class.getMethod("setFoo", String.class)));
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans;
import java.beans.BeanInfo;
import java.beans.PropertyDescriptor;
import java.beans.SimpleBeanInfo;
public class DummyBeanInfoFactory implements BeanInfoFactory {
public boolean supports(Class<?> beanClass) {
return CachedIntrospectionResultsTests.class.equals(beanClass);
}
public BeanInfo getBeanInfo(Class<?> beanClass) {
return new DummyBeanInfo();
}
public static class DummyBeanInfo extends SimpleBeanInfo {
@Override
public PropertyDescriptor[] getPropertyDescriptors() {
return new PropertyDescriptor[0];
}
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans;
import java.beans.IntrospectionException;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/**
* Unit tests for {@link ExtendedBeanInfoTests}.
*
* @author Chris Beams
*/
public class ExtendedBeanInfoFactoryTests {
private ExtendedBeanInfoFactory factory = new ExtendedBeanInfoFactory();
@Test
public void shouldNotSupportClassHavingOnlyVoidReturningSetter() throws IntrospectionException {
@SuppressWarnings("unused")
class C {
public void setFoo(String s) { }
}
assertThat(factory.supports(C.class), is(false));
}
@Test
public void shouldSupportClassHavingNonVoidReturningSetter() throws IntrospectionException {
@SuppressWarnings("unused")
class C {
public C setFoo(String s) { return this; }
}
assertThat(factory.supports(C.class), is(true));
}
@Test
public void shouldSupportClassHavingNonVoidReturningIndexedSetter() throws IntrospectionException {
@SuppressWarnings("unused")
class C {
public C setFoo(int i, String s) { return this; }
}
assertThat(factory.supports(C.class), is(true));
}
@Test
public void shouldNotSupportClassHavingNonPublicNonVoidReturningIndexedSetter() throws IntrospectionException {
@SuppressWarnings("unused")
class C {
void setBar(String s) { }
}
assertThat(factory.supports(C.class), is(false));
}
@Test
public void shouldNotSupportClassHavingNonVoidReturningParameterlessSetter() throws IntrospectionException {
@SuppressWarnings("unused")
class C {
C setBar() { return this; }
}
assertThat(factory.supports(C.class), is(false));
}
@Test
public void shouldNotSupportClassHavingNonVoidReturningMethodNamedSet() throws IntrospectionException {
@SuppressWarnings("unused")
class C {
C set(String s) { return this; }
}
assertThat(factory.supports(C.class), is(false));
}
}

View File

@@ -1,3 +0,0 @@
# Dummy bean info factories file, used by CachedIntrospectionResultsTests
org.springframework.beans.DummyBeanInfoFactory