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
This commit is contained in:
Arjen Poutsma
2012-08-20 12:39:22 +02:00
committed by Chris Beams
parent b95550489b
commit ca017a4880
7 changed files with 208 additions and 10 deletions

View File

@@ -0,0 +1,47 @@
/*
* 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;
/**
* Strategy for creating {@link BeanInfo} instances.
*
* @author Arjen Poutsma
* @since 3.2
*/
public interface BeanInfoFactory {
/**
* Indicates whether a bean with the given class is supported by this factory.
*
* @param beanClass the bean class
* @return {@code true} if supported; {@code false} otherwise
*/
boolean supports(Class<?> beanClass);
/**
* Returns the bean info for the given class.
*
* @param beanClass the bean class
* @return the bean info
* @throws IntrospectionException in case of exceptions
*/
BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* 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.
@@ -518,7 +518,7 @@ public class BeanWrapperImpl extends AbstractPropertyAccessor implements BeanWra
private Property property(PropertyDescriptor pd) {
GenericTypeAwarePropertyDescriptor typeAware = (GenericTypeAwarePropertyDescriptor) pd;
return new Property(typeAware.getBeanClass(), typeAware.getReadMethod(), typeAware.getWriteMethod());
return new Property(typeAware.getBeanClass(), typeAware.getReadMethod(), typeAware.getWriteMethod(), typeAware.getName());
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2010 the original author or authors.
* 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.
@@ -20,19 +20,25 @@ import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.WeakHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@@ -58,6 +64,12 @@ import org.springframework.util.StringUtils;
*/
public class CachedIntrospectionResults {
/**
* The location to look for the bean info mapping files. Can be present in multiple JAR files.
*/
public static final String BEAN_INFO_FACTORIES_LOCATION = "META-INF/spring.beanInfoFactories";
private static final Log logger = LogFactory.getLog(CachedIntrospectionResults.class);
/**
@@ -73,6 +85,11 @@ public class CachedIntrospectionResults {
*/
static final Map<Class, Object> classCache = Collections.synchronizedMap(new WeakHashMap<Class, Object>());
/** Stores the BeanInfoFactory instances */
private static List<BeanInfoFactory> beanInfoFactories;
private static final Object beanInfoFactoriesMutex = new Object();
/**
* Accept the given ClassLoader as cache-safe, even if its classes would
@@ -221,7 +238,20 @@ public class CachedIntrospectionResults {
if (logger.isTraceEnabled()) {
logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
}
this.beanInfo = new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass));
BeanInfo beanInfo = null;
List<BeanInfoFactory> beanInfoFactories = getBeanInfoFactories(beanClass.getClassLoader());
for (BeanInfoFactory beanInfoFactory : beanInfoFactories) {
if (beanInfoFactory.supports(beanClass)) {
beanInfo = beanInfoFactory.getBeanInfo(beanClass);
break;
}
}
if (beanInfo == null) {
// If none of the factories supported the class, use the default
beanInfo = new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass));
}
this.beanInfo = beanInfo;
// Immediately remove class from Introspector cache, to allow for proper
// garbage collection on class loader shutdown - we cache it here anyway,
@@ -305,4 +335,61 @@ public class CachedIntrospectionResults {
}
}
private static List<BeanInfoFactory> getBeanInfoFactories(ClassLoader classLoader) {
if (beanInfoFactories == null) {
synchronized (beanInfoFactoriesMutex) {
if (beanInfoFactories == null) {
try {
Properties properties =
PropertiesLoaderUtils.loadAllProperties(
BEAN_INFO_FACTORIES_LOCATION, classLoader);
if (logger.isDebugEnabled()) {
logger.debug("Loaded BeanInfoFactories: " + properties.keySet());
}
List<BeanInfoFactory> factories = new ArrayList<BeanInfoFactory>(properties.size());
for (Object key : properties.keySet()) {
if (key instanceof String) {
String className = (String) key;
BeanInfoFactory factory = instantiateBeanInfoFactory(className, classLoader);
factories.add(factory);
}
}
Collections.sort(factories, new AnnotationAwareOrderComparator());
beanInfoFactories = Collections.synchronizedList(factories);
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load BeanInfoFactories from location [" + BEAN_INFO_FACTORIES_LOCATION + "]", ex);
}
}
}
}
return beanInfoFactories;
}
private static BeanInfoFactory instantiateBeanInfoFactory(String className,
ClassLoader classLoader) {
try {
Class<?> factoryClass = ClassUtils.forName(className, classLoader);
if (!BeanInfoFactory.class.isAssignableFrom(factoryClass)) {
throw new FatalBeanException(
"Class [" + className + "] does not implement the [" +
BeanInfoFactory.class.getName() + "] interface");
}
return (BeanInfoFactory) BeanUtils.instantiate(factoryClass);
}
catch (ClassNotFoundException ex) {
throw new FatalBeanException(
"BeanInfoFactory class [" + className + "] not found", ex);
}
catch (LinkageError err) {
throw new FatalBeanException("Invalid BeanInfoFactory class [" + className +
"]: problem with handler class file or dependent class", err);
}
}
}