Skip to content

Commit 5bcf68e

Browse files
committed
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
1 parent b50bb50 commit 5bcf68e

File tree

7 files changed

+194
-50
lines changed

7 files changed

+194
-50
lines changed

spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,8 @@ private CachedIntrospectionResults(Class beanClass, boolean cacheFullMetadata) t
248248
}
249249
}
250250
if (beanInfo == null) {
251-
// If none of the factories supported the class, use the default
252-
beanInfo = new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass));
251+
// If none of the factories supported the class, fall back to the default
252+
beanInfo = Introspector.getBeanInfo(beanClass);
253253
}
254254
this.beanInfo = beanInfo;
255255

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.beans;
18+
19+
import java.beans.BeanInfo;
20+
import java.beans.IntrospectionException;
21+
import java.beans.Introspector;
22+
23+
import java.lang.reflect.Method;
24+
import java.lang.reflect.Modifier;
25+
26+
import org.springframework.core.Ordered;
27+
28+
/**
29+
* {@link BeanInfoFactory} implementation that evaluates whether bean classes have
30+
* "non-standard" JavaBeans setter methods and are thus candidates for introspection by
31+
* Spring's {@link ExtendedBeanInfo}.
32+
*
33+
* <p>Ordered at {@link Ordered#LOWEST_PRECEDENCE} to allow other user-defined
34+
* {@link BeanInfoFactory} types to take precedence.
35+
*
36+
* @author Chris Beams
37+
* @since 3.2
38+
* @see BeanInfoFactory
39+
*/
40+
class ExtendedBeanInfoFactory implements Ordered, BeanInfoFactory {
41+
42+
/**
43+
* Return whether the given bean class declares or inherits any non-void returning
44+
* JavaBeans setter methods.
45+
*/
46+
public boolean supports(Class<?> beanClass) {
47+
for (Method method : beanClass.getMethods()) {
48+
String methodName = method.getName();
49+
Class<?>[] parameterTypes = method.getParameterTypes();
50+
if (Modifier.isPublic(method.getModifiers())
51+
&& methodName.length() > 3
52+
&& methodName.startsWith("set")
53+
&& (parameterTypes.length == 1
54+
|| (parameterTypes.length == 2 && parameterTypes[0].equals(int.class)))
55+
&& !void.class.isAssignableFrom(method.getReturnType())) {
56+
return true;
57+
}
58+
}
59+
return false;
60+
}
61+
62+
/**
63+
* Return a new {@link ExtendedBeanInfo} for the given bean class.
64+
*/
65+
public BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
66+
return new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass));
67+
}
68+
69+
public int getOrder() {
70+
return Ordered.LOWEST_PRECEDENCE;
71+
}
72+
73+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.springframework.beans.ExtendedBeanInfoFactory

spring-beans/src/test/java/org/springframework/beans/CachedIntrospectionResultsTests.java

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.beans;
1818

1919
import java.beans.BeanInfo;
20+
import java.beans.PropertyDescriptor;
2021

2122
import static org.junit.Assert.assertFalse;
2223
import static org.junit.Assert.assertTrue;
@@ -25,6 +26,9 @@
2526

2627
import org.springframework.core.OverridingClassLoader;
2728

29+
import static org.hamcrest.CoreMatchers.*;
30+
import static org.junit.Assert.*;
31+
2832
/**
2933
* @author Juergen Hoeller
3034
* @author Chris Beams
@@ -54,11 +58,32 @@ public void acceptClassLoader() throws Exception {
5458
}
5559

5660
@Test
57-
public void customBeanInfoFactory() throws Exception {
58-
CachedIntrospectionResults results = CachedIntrospectionResults.forClass(CachedIntrospectionResultsTests.class);
59-
BeanInfo beanInfo = results.getBeanInfo();
61+
public void shouldUseExtendedBeanInfoWhenApplicable() throws NoSuchMethodException, SecurityException {
62+
// given a class with a non-void returning setter method
63+
@SuppressWarnings("unused")
64+
class C {
65+
public Object setFoo(String s) { return this; }
66+
public String getFoo() { return null; }
67+
}
68+
69+
// CachedIntrospectionResults should delegate to ExtendedBeanInfo
70+
CachedIntrospectionResults results = CachedIntrospectionResults.forClass(C.class);
71+
BeanInfo info = results.getBeanInfo();
72+
PropertyDescriptor pd = null;
73+
for (PropertyDescriptor candidate : info.getPropertyDescriptors()) {
74+
if (candidate.getName().equals("foo")) {
75+
pd = candidate;
76+
}
77+
}
6078

61-
assertTrue("Invalid BeanInfo instance", beanInfo instanceof DummyBeanInfoFactory.DummyBeanInfo);
79+
// resulting in a property descriptor including the non-standard setFoo method
80+
assertThat(pd, notNullValue());
81+
assertThat(pd.getReadMethod(), equalTo(C.class.getMethod("getFoo")));
82+
assertThat(
83+
"No write method found for non-void returning 'setFoo' method. " +
84+
"Check to see if CachedIntrospectionResults is delegating to " +
85+
"ExtendedBeanInfo as expected",
86+
pd.getWriteMethod(), equalTo(C.class.getMethod("setFoo", String.class)));
6287
}
6388

6489
}

spring-beans/src/test/java/org/springframework/beans/DummyBeanInfoFactory.java

Lines changed: 0 additions & 41 deletions
This file was deleted.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2002-2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.beans;
18+
19+
import java.beans.IntrospectionException;
20+
21+
import org.junit.Test;
22+
23+
import static org.hamcrest.CoreMatchers.*;
24+
import static org.junit.Assert.*;
25+
26+
/**
27+
* Unit tests for {@link ExtendedBeanInfoTests}.
28+
*
29+
* @author Chris Beams
30+
*/
31+
public class ExtendedBeanInfoFactoryTests {
32+
33+
private ExtendedBeanInfoFactory factory = new ExtendedBeanInfoFactory();
34+
35+
@Test
36+
public void shouldNotSupportClassHavingOnlyVoidReturningSetter() throws IntrospectionException {
37+
@SuppressWarnings("unused")
38+
class C {
39+
public void setFoo(String s) { }
40+
}
41+
assertThat(factory.supports(C.class), is(false));
42+
}
43+
44+
@Test
45+
public void shouldSupportClassHavingNonVoidReturningSetter() throws IntrospectionException {
46+
@SuppressWarnings("unused")
47+
class C {
48+
public C setFoo(String s) { return this; }
49+
}
50+
assertThat(factory.supports(C.class), is(true));
51+
}
52+
53+
@Test
54+
public void shouldSupportClassHavingNonVoidReturningIndexedSetter() throws IntrospectionException {
55+
@SuppressWarnings("unused")
56+
class C {
57+
public C setFoo(int i, String s) { return this; }
58+
}
59+
assertThat(factory.supports(C.class), is(true));
60+
}
61+
62+
@Test
63+
public void shouldNotSupportClassHavingNonPublicNonVoidReturningIndexedSetter() throws IntrospectionException {
64+
@SuppressWarnings("unused")
65+
class C {
66+
void setBar(String s) { }
67+
}
68+
assertThat(factory.supports(C.class), is(false));
69+
}
70+
71+
@Test
72+
public void shouldNotSupportClassHavingNonVoidReturningParameterlessSetter() throws IntrospectionException {
73+
@SuppressWarnings("unused")
74+
class C {
75+
C setBar() { return this; }
76+
}
77+
assertThat(factory.supports(C.class), is(false));
78+
}
79+
80+
@Test
81+
public void shouldNotSupportClassHavingNonVoidReturningMethodNamedSet() throws IntrospectionException {
82+
@SuppressWarnings("unused")
83+
class C {
84+
C set(String s) { return this; }
85+
}
86+
assertThat(factory.supports(C.class), is(false));
87+
}
88+
89+
}

spring-beans/src/test/resources/META-INF/spring.beanInfoFactories

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)