diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/BeanInfoFactory.java new file mode 100644 index 0000000000..dd1b2426af --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/BeanInfoFactory.java @@ -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; + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java index b57cada675..730f593bf6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java @@ -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()); } diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index fc6438434e..a06edf17c1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -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 classCache = Collections.synchronizedMap(new WeakHashMap()); + /** Stores the BeanInfoFactory instances */ + private static List 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 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 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 factories = new ArrayList(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); + } + } } diff --git a/spring-beans/src/test/java/org/springframework/beans/CachedIntrospectionResultsTests.java b/spring-beans/src/test/java/org/springframework/beans/CachedIntrospectionResultsTests.java index 5e42a92584..e5a6a0f82c 100644 --- a/spring-beans/src/test/java/org/springframework/beans/CachedIntrospectionResultsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/CachedIntrospectionResultsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 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. @@ -16,21 +16,24 @@ package org.springframework.beans; -import static org.junit.Assert.*; +import java.beans.BeanInfo; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import org.junit.Test; -import org.springframework.core.OverridingClassLoader; - import test.beans.TestBean; +import org.springframework.core.OverridingClassLoader; + /** * @author Juergen Hoeller * @author Chris Beams + * @author Arjen Poutsma */ public final class CachedIntrospectionResultsTests { @Test - public void testAcceptClassLoader() throws Exception { + public void acceptClassLoader() throws Exception { BeanWrapper bw = new BeanWrapperImpl(TestBean.class); assertTrue(bw.isWritableProperty("name")); assertTrue(bw.isWritableProperty("age")); @@ -50,4 +53,12 @@ public final class CachedIntrospectionResultsTests { assertTrue(CachedIntrospectionResults.classCache.containsKey(TestBean.class)); } + @Test + public void customBeanInfoFactory() throws Exception { + CachedIntrospectionResults results = CachedIntrospectionResults.forClass(CachedIntrospectionResultsTests.class); + BeanInfo beanInfo = results.getBeanInfo(); + + assertTrue("Invalid BeanInfo instance", beanInfo instanceof DummyBeanInfoFactory.DummyBeanInfo); + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/DummyBeanInfoFactory.java b/spring-beans/src/test/java/org/springframework/beans/DummyBeanInfoFactory.java new file mode 100644 index 0000000000..ef52db2771 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/DummyBeanInfoFactory.java @@ -0,0 +1,41 @@ +/* + * 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]; + } + } + +} diff --git a/spring-beans/src/test/resources/META-INF/spring.beanInfoFactories b/spring-beans/src/test/resources/META-INF/spring.beanInfoFactories new file mode 100644 index 0000000000..c2a8c64a4e --- /dev/null +++ b/spring-beans/src/test/resources/META-INF/spring.beanInfoFactories @@ -0,0 +1,3 @@ +# Dummy bean info factories file, used by CachedIntrospectionResultsTests + +org.springframework.beans.DummyBeanInfoFactory \ No newline at end of file diff --git a/spring-core/src/main/java/org/springframework/core/convert/Property.java b/spring-core/src/main/java/org/springframework/core/convert/Property.java index 09a1e6ada0..2fa51345cd 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/Property.java +++ b/spring-core/src/main/java/org/springframework/core/convert/Property.java @@ -57,11 +57,20 @@ public final class Property { public Property(Class objectType, Method readMethod, Method writeMethod) { + this(objectType, readMethod, writeMethod, null); + } + + public Property(Class objectType, Method readMethod, Method writeMethod, String name) { this.objectType = objectType; this.readMethod = readMethod; this.writeMethod = writeMethod; this.methodParameter = resolveMethodParameter(); - this.name = resolveName(); + if (name != null) { + this.name = name; + } + else { + this.name = resolveName(); + } this.annotations = resolveAnnotations(); }