001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.beans;
018
019import java.beans.BeanInfo;
020import java.beans.IntrospectionException;
021import java.beans.Introspector;
022import java.beans.PropertyDescriptor;
023import java.util.Collections;
024import java.util.Iterator;
025import java.util.LinkedHashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.concurrent.ConcurrentMap;
031
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034
035import org.springframework.core.SpringProperties;
036import org.springframework.core.convert.TypeDescriptor;
037import org.springframework.core.io.support.SpringFactoriesLoader;
038import org.springframework.util.ClassUtils;
039import org.springframework.util.ConcurrentReferenceHashMap;
040import org.springframework.util.StringUtils;
041
042/**
043 * Internal class that caches JavaBeans {@link java.beans.PropertyDescriptor}
044 * information for a Java class. Not intended for direct use by application code.
045 *
046 * <p>Necessary for Spring's own caching of bean descriptors within the application
047 * {@link ClassLoader}, rather than relying on the JDK's system-wide {@link BeanInfo}
048 * cache (in order to avoid leaks on individual application shutdown in a shared JVM).
049 *
050 * <p>Information is cached statically, so we don't need to create new
051 * objects of this class for every JavaBean we manipulate. Hence, this class
052 * implements the factory design pattern, using a private constructor and
053 * a static {@link #forClass(Class)} factory method to obtain instances.
054 *
055 * <p>Note that for caching to work effectively, some preconditions need to be met:
056 * Prefer an arrangement where the Spring jars live in the same ClassLoader as the
057 * application classes, which allows for clean caching along with the application's
058 * lifecycle in any case. For a web application, consider declaring a local
059 * {@link org.springframework.web.util.IntrospectorCleanupListener} in {@code web.xml}
060 * in case of a multi-ClassLoader layout, which will allow for effective caching as well.
061 *
062 * <p>In case of a non-clean ClassLoader arrangement without a cleanup listener having
063 * been set up, this class will fall back to a weak-reference-based caching model that
064 * recreates much-requested entries every time the garbage collector removed them. In
065 * such a scenario, consider the {@link #IGNORE_BEANINFO_PROPERTY_NAME} system property.
066 *
067 * @author Rod Johnson
068 * @author Juergen Hoeller
069 * @since 05 May 2001
070 * @see #acceptClassLoader(ClassLoader)
071 * @see #clearClassLoader(ClassLoader)
072 * @see #forClass(Class)
073 */
074public class CachedIntrospectionResults {
075
076        /**
077         * System property that instructs Spring to use the {@link Introspector#IGNORE_ALL_BEANINFO}
078         * mode when calling the JavaBeans {@link Introspector}: "spring.beaninfo.ignore", with a
079         * value of "true" skipping the search for {@code BeanInfo} classes (typically for scenarios
080         * where no such classes are being defined for beans in the application in the first place).
081         * <p>The default is "false", considering all {@code BeanInfo} metadata classes, like for
082         * standard {@link Introspector#getBeanInfo(Class)} calls. Consider switching this flag to
083         * "true" if you experience repeated ClassLoader access for non-existing {@code BeanInfo}
084         * classes, in case such access is expensive on startup or on lazy loading.
085         * <p>Note that such an effect may also indicate a scenario where caching doesn't work
086         * effectively: Prefer an arrangement where the Spring jars live in the same ClassLoader
087         * as the application classes, which allows for clean caching along with the application's
088         * lifecycle in any case. For a web application, consider declaring a local
089         * {@link org.springframework.web.util.IntrospectorCleanupListener} in {@code web.xml}
090         * in case of a multi-ClassLoader layout, which will allow for effective caching as well.
091         * @see Introspector#getBeanInfo(Class, int)
092         */
093        public static final String IGNORE_BEANINFO_PROPERTY_NAME = "spring.beaninfo.ignore";
094
095
096        private static final boolean shouldIntrospectorIgnoreBeaninfoClasses =
097                        SpringProperties.getFlag(IGNORE_BEANINFO_PROPERTY_NAME);
098
099        /** Stores the BeanInfoFactory instances */
100        private static final List<BeanInfoFactory> beanInfoFactories = SpringFactoriesLoader.loadFactories(
101                        BeanInfoFactory.class, CachedIntrospectionResults.class.getClassLoader());
102
103        private static final Log logger = LogFactory.getLog(CachedIntrospectionResults.class);
104
105        /**
106         * Set of ClassLoaders that this CachedIntrospectionResults class will always
107         * accept classes from, even if the classes do not qualify as cache-safe.
108         */
109        static final Set<ClassLoader> acceptedClassLoaders =
110                        Collections.newSetFromMap(new ConcurrentHashMap<ClassLoader, Boolean>(16));
111
112        /**
113         * Map keyed by Class containing CachedIntrospectionResults, strongly held.
114         * This variant is being used for cache-safe bean classes.
115         */
116        static final ConcurrentMap<Class<?>, CachedIntrospectionResults> strongClassCache =
117                        new ConcurrentHashMap<Class<?>, CachedIntrospectionResults>(64);
118
119        /**
120         * Map keyed by Class containing CachedIntrospectionResults, softly held.
121         * This variant is being used for non-cache-safe bean classes.
122         */
123        static final ConcurrentMap<Class<?>, CachedIntrospectionResults> softClassCache =
124                        new ConcurrentReferenceHashMap<Class<?>, CachedIntrospectionResults>(64);
125
126
127        /**
128         * Accept the given ClassLoader as cache-safe, even if its classes would
129         * not qualify as cache-safe in this CachedIntrospectionResults class.
130         * <p>This configuration method is only relevant in scenarios where the Spring
131         * classes reside in a 'common' ClassLoader (e.g. the system ClassLoader)
132         * whose lifecycle is not coupled to the application. In such a scenario,
133         * CachedIntrospectionResults would by default not cache any of the application's
134         * classes, since they would create a leak in the common ClassLoader.
135         * <p>Any {@code acceptClassLoader} call at application startup should
136         * be paired with a {@link #clearClassLoader} call at application shutdown.
137         * @param classLoader the ClassLoader to accept
138         */
139        public static void acceptClassLoader(ClassLoader classLoader) {
140                if (classLoader != null) {
141                        acceptedClassLoaders.add(classLoader);
142                }
143        }
144
145        /**
146         * Clear the introspection cache for the given ClassLoader, removing the
147         * introspection results for all classes underneath that ClassLoader, and
148         * removing the ClassLoader (and its children) from the acceptance list.
149         * @param classLoader the ClassLoader to clear the cache for
150         */
151        public static void clearClassLoader(ClassLoader classLoader) {
152                for (Iterator<ClassLoader> it = acceptedClassLoaders.iterator(); it.hasNext();) {
153                        ClassLoader registeredLoader = it.next();
154                        if (isUnderneathClassLoader(registeredLoader, classLoader)) {
155                                it.remove();
156                        }
157                }
158                for (Iterator<Class<?>> it = strongClassCache.keySet().iterator(); it.hasNext();) {
159                        Class<?> beanClass = it.next();
160                        if (isUnderneathClassLoader(beanClass.getClassLoader(), classLoader)) {
161                                it.remove();
162                        }
163                }
164                for (Iterator<Class<?>> it = softClassCache.keySet().iterator(); it.hasNext();) {
165                        Class<?> beanClass = it.next();
166                        if (isUnderneathClassLoader(beanClass.getClassLoader(), classLoader)) {
167                                it.remove();
168                        }
169                }
170        }
171
172        /**
173         * Create CachedIntrospectionResults for the given bean class.
174         * @param beanClass the bean class to analyze
175         * @return the corresponding CachedIntrospectionResults
176         * @throws BeansException in case of introspection failure
177         */
178        @SuppressWarnings("unchecked")
179        static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
180                CachedIntrospectionResults results = strongClassCache.get(beanClass);
181                if (results != null) {
182                        return results;
183                }
184                results = softClassCache.get(beanClass);
185                if (results != null) {
186                        return results;
187                }
188
189                results = new CachedIntrospectionResults(beanClass);
190                ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse;
191
192                if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) ||
193                                isClassLoaderAccepted(beanClass.getClassLoader())) {
194                        classCacheToUse = strongClassCache;
195                }
196                else {
197                        if (logger.isDebugEnabled()) {
198                                logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe");
199                        }
200                        classCacheToUse = softClassCache;
201                }
202
203                CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results);
204                return (existing != null ? existing : results);
205        }
206
207        /**
208         * Check whether this CachedIntrospectionResults class is configured
209         * to accept the given ClassLoader.
210         * @param classLoader the ClassLoader to check
211         * @return whether the given ClassLoader is accepted
212         * @see #acceptClassLoader
213         */
214        private static boolean isClassLoaderAccepted(ClassLoader classLoader) {
215                for (ClassLoader acceptedLoader : acceptedClassLoaders) {
216                        if (isUnderneathClassLoader(classLoader, acceptedLoader)) {
217                                return true;
218                        }
219                }
220                return false;
221        }
222
223        /**
224         * Check whether the given ClassLoader is underneath the given parent,
225         * that is, whether the parent is within the candidate's hierarchy.
226         * @param candidate the candidate ClassLoader to check
227         * @param parent the parent ClassLoader to check for
228         */
229        private static boolean isUnderneathClassLoader(ClassLoader candidate, ClassLoader parent) {
230                if (candidate == parent) {
231                        return true;
232                }
233                if (candidate == null) {
234                        return false;
235                }
236                ClassLoader classLoaderToCheck = candidate;
237                while (classLoaderToCheck != null) {
238                        classLoaderToCheck = classLoaderToCheck.getParent();
239                        if (classLoaderToCheck == parent) {
240                                return true;
241                        }
242                }
243                return false;
244        }
245
246
247        /** The BeanInfo object for the introspected bean class */
248        private final BeanInfo beanInfo;
249
250        /** PropertyDescriptor objects keyed by property name String */
251        private final Map<String, PropertyDescriptor> propertyDescriptorCache;
252
253        /** TypeDescriptor objects keyed by PropertyDescriptor */
254        private final ConcurrentMap<PropertyDescriptor, TypeDescriptor> typeDescriptorCache;
255
256
257        /**
258         * Create a new CachedIntrospectionResults instance for the given class.
259         * @param beanClass the bean class to analyze
260         * @throws BeansException in case of introspection failure
261         */
262        private CachedIntrospectionResults(Class<?> beanClass) throws BeansException {
263                try {
264                        if (logger.isTraceEnabled()) {
265                                logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
266                        }
267
268                        BeanInfo beanInfo = null;
269                        for (BeanInfoFactory beanInfoFactory : beanInfoFactories) {
270                                beanInfo = beanInfoFactory.getBeanInfo(beanClass);
271                                if (beanInfo != null) {
272                                        break;
273                                }
274                        }
275                        if (beanInfo == null) {
276                                // If none of the factories supported the class, fall back to the default
277                                beanInfo = (shouldIntrospectorIgnoreBeaninfoClasses ?
278                                                Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) :
279                                                Introspector.getBeanInfo(beanClass));
280                        }
281                        this.beanInfo = beanInfo;
282
283                        if (logger.isTraceEnabled()) {
284                                logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]");
285                        }
286                        this.propertyDescriptorCache = new LinkedHashMap<String, PropertyDescriptor>();
287
288                        // This call is slow so we do it once.
289                        PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
290                        for (PropertyDescriptor pd : pds) {
291                                if (Class.class == beanClass &&
292                                                ("classLoader".equals(pd.getName()) ||  "protectionDomain".equals(pd.getName()))) {
293                                        // Ignore Class.getClassLoader() and getProtectionDomain() methods - nobody needs to bind to those
294                                        continue;
295                                }
296                                if (logger.isTraceEnabled()) {
297                                        logger.trace("Found bean property '" + pd.getName() + "'" +
298                                                        (pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : "") +
299                                                        (pd.getPropertyEditorClass() != null ?
300                                                                        "; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));
301                                }
302                                pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
303                                this.propertyDescriptorCache.put(pd.getName(), pd);
304                        }
305
306                        // Explicitly check implemented interfaces for setter/getter methods as well,
307                        // in particular for Java 8 default methods...
308                        Class<?> clazz = beanClass;
309                        while (clazz != null) {
310                                Class<?>[] ifcs = clazz.getInterfaces();
311                                for (Class<?> ifc : ifcs) {
312                                        BeanInfo ifcInfo = Introspector.getBeanInfo(ifc, Introspector.IGNORE_ALL_BEANINFO);
313                                        PropertyDescriptor[] ifcPds = ifcInfo.getPropertyDescriptors();
314                                        for (PropertyDescriptor pd : ifcPds) {
315                                                if (!this.propertyDescriptorCache.containsKey(pd.getName())) {
316                                                        pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
317                                                        this.propertyDescriptorCache.put(pd.getName(), pd);
318                                                }
319                                        }
320                                }
321                                clazz = clazz.getSuperclass();
322                        }
323
324                        this.typeDescriptorCache = new ConcurrentReferenceHashMap<PropertyDescriptor, TypeDescriptor>();
325                }
326                catch (IntrospectionException ex) {
327                        throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex);
328                }
329        }
330
331        BeanInfo getBeanInfo() {
332                return this.beanInfo;
333        }
334
335        Class<?> getBeanClass() {
336                return this.beanInfo.getBeanDescriptor().getBeanClass();
337        }
338
339        PropertyDescriptor getPropertyDescriptor(String name) {
340                PropertyDescriptor pd = this.propertyDescriptorCache.get(name);
341                if (pd == null && StringUtils.hasLength(name)) {
342                        // Same lenient fallback checking as in Property...
343                        pd = this.propertyDescriptorCache.get(StringUtils.uncapitalize(name));
344                        if (pd == null) {
345                                pd = this.propertyDescriptorCache.get(StringUtils.capitalize(name));
346                        }
347                }
348                return (pd == null || pd instanceof GenericTypeAwarePropertyDescriptor ? pd :
349                                buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd));
350        }
351
352        PropertyDescriptor[] getPropertyDescriptors() {
353                PropertyDescriptor[] pds = new PropertyDescriptor[this.propertyDescriptorCache.size()];
354                int i = 0;
355                for (PropertyDescriptor pd : this.propertyDescriptorCache.values()) {
356                        pds[i] = (pd instanceof GenericTypeAwarePropertyDescriptor ? pd :
357                                        buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd));
358                        i++;
359                }
360                return pds;
361        }
362
363        private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class<?> beanClass, PropertyDescriptor pd) {
364                try {
365                        return new GenericTypeAwarePropertyDescriptor(beanClass, pd.getName(), pd.getReadMethod(),
366                                        pd.getWriteMethod(), pd.getPropertyEditorClass());
367                }
368                catch (IntrospectionException ex) {
369                        throw new FatalBeanException("Failed to re-introspect class [" + beanClass.getName() + "]", ex);
370                }
371        }
372
373        TypeDescriptor addTypeDescriptor(PropertyDescriptor pd, TypeDescriptor td) {
374                TypeDescriptor existing = this.typeDescriptorCache.putIfAbsent(pd, td);
375                return (existing != null ? existing : td);
376        }
377
378        TypeDescriptor getTypeDescriptor(PropertyDescriptor pd) {
379                return this.typeDescriptorCache.get(pd);
380        }
381
382}