Skip to content

Commit ca017a4

Browse files
poutsmacbeams
authored andcommitted
Introduce strategy for BeanInfo creation
Before this commit, the CachedIntrospectionResults was hard-coded to create ExtendedBeanInfos for bean classes. The ExtendedBeanInfo support the JavaBeans property contract only. This commit introduces the BeanInfoFactory, a strategy for creating BeanInfos. Through this strategy, it is possible to support beans that do not necessarily implement the JavaBeans contract (i.e. have a different getter or setter style). BeanInfoFactories are are instantiated by the CachedIntrospectionResults, which looks for 'META-INF/spring.beanInfoFactories' files on the class path. These files contain one or more BeanInfoFactory class names. When a BeanInfo is to be created, the CachedIntrospectionResults will iterate through the factories, asking it to create a BeanInfo for the given bean class. If none of the factories support it, an ExtendedBeanInfo is created as a default. This commit also contains a change to Property, allowing BeanWrapperImpl to specify the property name at construction time (as opposed to using Property#resolveName(), which supports the JavaBeans contract only). Issue: SPR-9677
1 parent b955504 commit ca017a4

File tree

7 files changed

+208
-10
lines changed

7 files changed

+208
-10
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
22+
/**
23+
* Strategy for creating {@link BeanInfo} instances.
24+
*
25+
* @author Arjen Poutsma
26+
* @since 3.2
27+
*/
28+
public interface BeanInfoFactory {
29+
30+
/**
31+
* Indicates whether a bean with the given class is supported by this factory.
32+
*
33+
* @param beanClass the bean class
34+
* @return {@code true} if supported; {@code false} otherwise
35+
*/
36+
boolean supports(Class<?> beanClass);
37+
38+
/**
39+
* Returns the bean info for the given class.
40+
*
41+
* @param beanClass the bean class
42+
* @return the bean info
43+
* @throws IntrospectionException in case of exceptions
44+
*/
45+
BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException;
46+
47+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2011 the original author or authors.
2+
* Copyright 2002-2012 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -518,7 +518,7 @@ private Object convertForProperty(String propertyName, Object oldValue, Object n
518518

519519
private Property property(PropertyDescriptor pd) {
520520
GenericTypeAwarePropertyDescriptor typeAware = (GenericTypeAwarePropertyDescriptor) pd;
521-
return new Property(typeAware.getBeanClass(), typeAware.getReadMethod(), typeAware.getWriteMethod());
521+
return new Property(typeAware.getBeanClass(), typeAware.getReadMethod(), typeAware.getWriteMethod(), typeAware.getName());
522522
}
523523

524524

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

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2010 the original author or authors.
2+
* Copyright 2002-2012 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,19 +20,25 @@
2020
import java.beans.IntrospectionException;
2121
import java.beans.Introspector;
2222
import java.beans.PropertyDescriptor;
23+
import java.io.IOException;
2324
import java.lang.ref.Reference;
2425
import java.lang.ref.WeakReference;
26+
import java.util.ArrayList;
2527
import java.util.Collections;
2628
import java.util.HashSet;
2729
import java.util.Iterator;
2830
import java.util.LinkedHashMap;
31+
import java.util.List;
2932
import java.util.Map;
33+
import java.util.Properties;
3034
import java.util.Set;
3135
import java.util.WeakHashMap;
3236

3337
import org.apache.commons.logging.Log;
3438
import org.apache.commons.logging.LogFactory;
3539

40+
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
41+
import org.springframework.core.io.support.PropertiesLoaderUtils;
3642
import org.springframework.util.ClassUtils;
3743
import org.springframework.util.StringUtils;
3844

@@ -58,6 +64,12 @@
5864
*/
5965
public class CachedIntrospectionResults {
6066

67+
/**
68+
* The location to look for the bean info mapping files. Can be present in multiple JAR files.
69+
*/
70+
public static final String BEAN_INFO_FACTORIES_LOCATION = "META-INF/spring.beanInfoFactories";
71+
72+
6173
private static final Log logger = LogFactory.getLog(CachedIntrospectionResults.class);
6274

6375
/**
@@ -73,6 +85,11 @@ public class CachedIntrospectionResults {
7385
*/
7486
static final Map<Class, Object> classCache = Collections.synchronizedMap(new WeakHashMap<Class, Object>());
7587

88+
/** Stores the BeanInfoFactory instances */
89+
private static List<BeanInfoFactory> beanInfoFactories;
90+
91+
private static final Object beanInfoFactoriesMutex = new Object();
92+
7693

7794
/**
7895
* Accept the given ClassLoader as cache-safe, even if its classes would
@@ -221,7 +238,20 @@ private CachedIntrospectionResults(Class beanClass, boolean cacheFullMetadata) t
221238
if (logger.isTraceEnabled()) {
222239
logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
223240
}
224-
this.beanInfo = new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass));
241+
242+
BeanInfo beanInfo = null;
243+
List<BeanInfoFactory> beanInfoFactories = getBeanInfoFactories(beanClass.getClassLoader());
244+
for (BeanInfoFactory beanInfoFactory : beanInfoFactories) {
245+
if (beanInfoFactory.supports(beanClass)) {
246+
beanInfo = beanInfoFactory.getBeanInfo(beanClass);
247+
break;
248+
}
249+
}
250+
if (beanInfo == null) {
251+
// If none of the factories supported the class, use the default
252+
beanInfo = new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass));
253+
}
254+
this.beanInfo = beanInfo;
225255

226256
// Immediately remove class from Introspector cache, to allow for proper
227257
// garbage collection on class loader shutdown - we cache it here anyway,
@@ -305,4 +335,61 @@ private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class beanCla
305335
}
306336
}
307337

338+
private static List<BeanInfoFactory> getBeanInfoFactories(ClassLoader classLoader) {
339+
if (beanInfoFactories == null) {
340+
synchronized (beanInfoFactoriesMutex) {
341+
if (beanInfoFactories == null) {
342+
try {
343+
Properties properties =
344+
PropertiesLoaderUtils.loadAllProperties(
345+
BEAN_INFO_FACTORIES_LOCATION, classLoader);
346+
347+
if (logger.isDebugEnabled()) {
348+
logger.debug("Loaded BeanInfoFactories: " + properties.keySet());
349+
}
350+
351+
List<BeanInfoFactory> factories = new ArrayList<BeanInfoFactory>(properties.size());
352+
353+
for (Object key : properties.keySet()) {
354+
if (key instanceof String) {
355+
String className = (String) key;
356+
BeanInfoFactory factory = instantiateBeanInfoFactory(className, classLoader);
357+
factories.add(factory);
358+
}
359+
}
360+
361+
Collections.sort(factories, new AnnotationAwareOrderComparator());
362+
363+
beanInfoFactories = Collections.synchronizedList(factories);
364+
}
365+
catch (IOException ex) {
366+
throw new IllegalStateException(
367+
"Unable to load BeanInfoFactories from location [" + BEAN_INFO_FACTORIES_LOCATION + "]", ex);
368+
}
369+
}
370+
}
371+
}
372+
return beanInfoFactories;
373+
}
374+
375+
private static BeanInfoFactory instantiateBeanInfoFactory(String className,
376+
ClassLoader classLoader) {
377+
try {
378+
Class<?> factoryClass = ClassUtils.forName(className, classLoader);
379+
if (!BeanInfoFactory.class.isAssignableFrom(factoryClass)) {
380+
throw new FatalBeanException(
381+
"Class [" + className + "] does not implement the [" +
382+
BeanInfoFactory.class.getName() + "] interface");
383+
}
384+
return (BeanInfoFactory) BeanUtils.instantiate(factoryClass);
385+
}
386+
catch (ClassNotFoundException ex) {
387+
throw new FatalBeanException(
388+
"BeanInfoFactory class [" + className + "] not found", ex);
389+
}
390+
catch (LinkageError err) {
391+
throw new FatalBeanException("Invalid BeanInfoFactory class [" + className +
392+
"]: problem with handler class file or dependent class", err);
393+
}
394+
}
308395
}

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2008 the original author or authors.
2+
* Copyright 2002-2012 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,21 +16,24 @@
1616

1717
package org.springframework.beans;
1818

19-
import static org.junit.Assert.*;
19+
import java.beans.BeanInfo;
2020

21+
import static org.junit.Assert.assertFalse;
22+
import static org.junit.Assert.assertTrue;
2123
import org.junit.Test;
22-
import org.springframework.core.OverridingClassLoader;
23-
2424
import test.beans.TestBean;
2525

26+
import org.springframework.core.OverridingClassLoader;
27+
2628
/**
2729
* @author Juergen Hoeller
2830
* @author Chris Beams
31+
* @author Arjen Poutsma
2932
*/
3033
public final class CachedIntrospectionResultsTests {
3134

3235
@Test
33-
public void testAcceptClassLoader() throws Exception {
36+
public void acceptClassLoader() throws Exception {
3437
BeanWrapper bw = new BeanWrapperImpl(TestBean.class);
3538
assertTrue(bw.isWritableProperty("name"));
3639
assertTrue(bw.isWritableProperty("age"));
@@ -50,4 +53,12 @@ public void testAcceptClassLoader() throws Exception {
5053
assertTrue(CachedIntrospectionResults.classCache.containsKey(TestBean.class));
5154
}
5255

56+
@Test
57+
public void customBeanInfoFactory() throws Exception {
58+
CachedIntrospectionResults results = CachedIntrospectionResults.forClass(CachedIntrospectionResultsTests.class);
59+
BeanInfo beanInfo = results.getBeanInfo();
60+
61+
assertTrue("Invalid BeanInfo instance", beanInfo instanceof DummyBeanInfoFactory.DummyBeanInfo);
62+
}
63+
5364
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.PropertyDescriptor;
21+
import java.beans.SimpleBeanInfo;
22+
23+
public class DummyBeanInfoFactory implements BeanInfoFactory {
24+
25+
public boolean supports(Class<?> beanClass) {
26+
return CachedIntrospectionResultsTests.class.equals(beanClass);
27+
}
28+
29+
public BeanInfo getBeanInfo(Class<?> beanClass) {
30+
return new DummyBeanInfo();
31+
}
32+
33+
public static class DummyBeanInfo extends SimpleBeanInfo {
34+
35+
@Override
36+
public PropertyDescriptor[] getPropertyDescriptors() {
37+
return new PropertyDescriptor[0];
38+
}
39+
}
40+
41+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Dummy bean info factories file, used by CachedIntrospectionResultsTests
2+
3+
org.springframework.beans.DummyBeanInfoFactory

spring-core/src/main/java/org/springframework/core/convert/Property.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,20 @@ public final class Property {
5757

5858

5959
public Property(Class<?> objectType, Method readMethod, Method writeMethod) {
60+
this(objectType, readMethod, writeMethod, null);
61+
}
62+
63+
public Property(Class<?> objectType, Method readMethod, Method writeMethod, String name) {
6064
this.objectType = objectType;
6165
this.readMethod = readMethod;
6266
this.writeMethod = writeMethod;
6367
this.methodParameter = resolveMethodParameter();
64-
this.name = resolveName();
68+
if (name != null) {
69+
this.name = name;
70+
}
71+
else {
72+
this.name = resolveName();
73+
}
6574
this.annotations = resolveAnnotations();
6675
}
6776

0 commit comments

Comments
 (0)