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.LinkedHashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028import java.util.concurrent.ConcurrentHashMap;
029import java.util.concurrent.ConcurrentMap;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033
034import org.springframework.core.SpringProperties;
035import org.springframework.core.convert.TypeDescriptor;
036import org.springframework.core.io.support.SpringFactoriesLoader;
037import org.springframework.lang.Nullable;
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 final 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        private static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTOR_ARRAY = {};
096
097
098        private static final boolean shouldIntrospectorIgnoreBeaninfoClasses =
099                        SpringProperties.getFlag(IGNORE_BEANINFO_PROPERTY_NAME);
100
101        /** Stores the BeanInfoFactory instances. */
102        private static final List<BeanInfoFactory> beanInfoFactories = SpringFactoriesLoader.loadFactories(
103                        BeanInfoFactory.class, CachedIntrospectionResults.class.getClassLoader());
104
105        private static final Log logger = LogFactory.getLog(CachedIntrospectionResults.class);
106
107        /**
108         * Set of ClassLoaders that this CachedIntrospectionResults class will always
109         * accept classes from, even if the classes do not qualify as cache-safe.
110         */
111        static final Set<ClassLoader> acceptedClassLoaders =
112                        Collections.newSetFromMap(new ConcurrentHashMap<>(16));
113
114        /**
115         * Map keyed by Class containing CachedIntrospectionResults, strongly held.
116         * This variant is being used for cache-safe bean classes.
117         */
118        static final ConcurrentMap<Class<?>, CachedIntrospectionResults> strongClassCache =
119                        new ConcurrentHashMap<>(64);
120
121        /**
122         * Map keyed by Class containing CachedIntrospectionResults, softly held.
123         * This variant is being used for non-cache-safe bean classes.
124         */
125        static final ConcurrentMap<Class<?>, CachedIntrospectionResults> softClassCache =
126                        new ConcurrentReferenceHashMap<>(64);
127
128
129        /**
130         * Accept the given ClassLoader as cache-safe, even if its classes would
131         * not qualify as cache-safe in this CachedIntrospectionResults class.
132         * <p>This configuration method is only relevant in scenarios where the Spring
133         * classes reside in a 'common' ClassLoader (e.g. the system ClassLoader)
134         * whose lifecycle is not coupled to the application. In such a scenario,
135         * CachedIntrospectionResults would by default not cache any of the application's
136         * classes, since they would create a leak in the common ClassLoader.
137         * <p>Any {@code acceptClassLoader} call at application startup should
138         * be paired with a {@link #clearClassLoader} call at application shutdown.
139         * @param classLoader the ClassLoader to accept
140         */
141        public static void acceptClassLoader(@Nullable ClassLoader classLoader) {
142                if (classLoader != null) {
143                        acceptedClassLoaders.add(classLoader);
144                }
145        }
146
147        /**
148         * Clear the introspection cache for the given ClassLoader, removing the
149         * introspection results for all classes underneath that ClassLoader, and
150         * removing the ClassLoader (and its children) from the acceptance list.
151         * @param classLoader the ClassLoader to clear the cache for
152         */
153        public static void clearClassLoader(@Nullable ClassLoader classLoader) {
154                acceptedClassLoaders.removeIf(registeredLoader ->
155                                isUnderneathClassLoader(registeredLoader, classLoader));
156                strongClassCache.keySet().removeIf(beanClass ->
157                                isUnderneathClassLoader(beanClass.getClassLoader(), classLoader));
158                softClassCache.keySet().removeIf(beanClass ->
159                                isUnderneathClassLoader(beanClass.getClassLoader(), classLoader));
160        }
161
162        /**
163         * Create CachedIntrospectionResults for the given bean class.
164         * @param beanClass the bean class to analyze
165         * @return the corresponding CachedIntrospectionResults
166         * @throws BeansException in case of introspection failure
167         */
168        static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
169                CachedIntrospectionResults results = strongClassCache.get(beanClass);
170                if (results != null) {
171                        return results;
172                }
173                results = softClassCache.get(beanClass);
174                if (results != null) {
175                        return results;
176                }
177
178                results = new CachedIntrospectionResults(beanClass);
179                ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse;
180
181                if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) ||
182                                isClassLoaderAccepted(beanClass.getClassLoader())) {
183                        classCacheToUse = strongClassCache;
184                }
185                else {
186                        if (logger.isDebugEnabled()) {
187                                logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe");
188                        }
189                        classCacheToUse = softClassCache;
190                }
191
192                CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results);
193                return (existing != null ? existing : results);
194        }
195
196        /**
197         * Check whether this CachedIntrospectionResults class is configured
198         * to accept the given ClassLoader.
199         * @param classLoader the ClassLoader to check
200         * @return whether the given ClassLoader is accepted
201         * @see #acceptClassLoader
202         */
203        private static boolean isClassLoaderAccepted(ClassLoader classLoader) {
204                for (ClassLoader acceptedLoader : acceptedClassLoaders) {
205                        if (isUnderneathClassLoader(classLoader, acceptedLoader)) {
206                                return true;
207                        }
208                }
209                return false;
210        }
211
212        /**
213         * Check whether the given ClassLoader is underneath the given parent,
214         * that is, whether the parent is within the candidate's hierarchy.
215         * @param candidate the candidate ClassLoader to check
216         * @param parent the parent ClassLoader to check for
217         */
218        private static boolean isUnderneathClassLoader(@Nullable ClassLoader candidate, @Nullable ClassLoader parent) {
219                if (candidate == parent) {
220                        return true;
221                }
222                if (candidate == null) {
223                        return false;
224                }
225                ClassLoader classLoaderToCheck = candidate;
226                while (classLoaderToCheck != null) {
227                        classLoaderToCheck = classLoaderToCheck.getParent();
228                        if (classLoaderToCheck == parent) {
229                                return true;
230                        }
231                }
232                return false;
233        }
234
235        /**
236         * Retrieve a {@link BeanInfo} descriptor for the given target class.
237         * @param beanClass the target class to introspect
238         * @return the resulting {@code BeanInfo} descriptor (never {@code null})
239         * @throws IntrospectionException from the underlying {@link Introspector}
240         */
241        private static BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
242                for (BeanInfoFactory beanInfoFactory : beanInfoFactories) {
243                        BeanInfo beanInfo = beanInfoFactory.getBeanInfo(beanClass);
244                        if (beanInfo != null) {
245                                return beanInfo;
246                        }
247                }
248                return (shouldIntrospectorIgnoreBeaninfoClasses ?
249                                Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) :
250                                Introspector.getBeanInfo(beanClass));
251        }
252
253
254        /** The BeanInfo object for the introspected bean class. */
255        private final BeanInfo beanInfo;
256
257        /** PropertyDescriptor objects keyed by property name String. */
258        private final Map<String, PropertyDescriptor> propertyDescriptors;
259
260        /** TypeDescriptor objects keyed by PropertyDescriptor. */
261        private final ConcurrentMap<PropertyDescriptor, TypeDescriptor> typeDescriptorCache;
262
263
264        /**
265         * Create a new CachedIntrospectionResults instance for the given class.
266         * @param beanClass the bean class to analyze
267         * @throws BeansException in case of introspection failure
268         */
269        private CachedIntrospectionResults(Class<?> beanClass) throws BeansException {
270                try {
271                        if (logger.isTraceEnabled()) {
272                                logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
273                        }
274                        this.beanInfo = getBeanInfo(beanClass);
275
276                        if (logger.isTraceEnabled()) {
277                                logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]");
278                        }
279                        this.propertyDescriptors = new LinkedHashMap<>();
280
281                        // This call is slow so we do it once.
282                        PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
283                        for (PropertyDescriptor pd : pds) {
284                                if (Class.class == beanClass &&
285                                                ("classLoader".equals(pd.getName()) ||  "protectionDomain".equals(pd.getName()))) {
286                                        // Ignore Class.getClassLoader() and getProtectionDomain() methods - nobody needs to bind to those
287                                        continue;
288                                }
289                                if (logger.isTraceEnabled()) {
290                                        logger.trace("Found bean property '" + pd.getName() + "'" +
291                                                        (pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : "") +
292                                                        (pd.getPropertyEditorClass() != null ?
293                                                                        "; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));
294                                }
295                                pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
296                                this.propertyDescriptors.put(pd.getName(), pd);
297                        }
298
299                        // Explicitly check implemented interfaces for setter/getter methods as well,
300                        // in particular for Java 8 default methods...
301                        Class<?> currClass = beanClass;
302                        while (currClass != null && currClass != Object.class) {
303                                introspectInterfaces(beanClass, currClass);
304                                currClass = currClass.getSuperclass();
305                        }
306
307                        this.typeDescriptorCache = new ConcurrentReferenceHashMap<>();
308                }
309                catch (IntrospectionException ex) {
310                        throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex);
311                }
312        }
313
314        private void introspectInterfaces(Class<?> beanClass, Class<?> currClass) throws IntrospectionException {
315                for (Class<?> ifc : currClass.getInterfaces()) {
316                        if (!ClassUtils.isJavaLanguageInterface(ifc)) {
317                                for (PropertyDescriptor pd : getBeanInfo(ifc).getPropertyDescriptors()) {
318                                        PropertyDescriptor existingPd = this.propertyDescriptors.get(pd.getName());
319                                        if (existingPd == null ||
320                                                        (existingPd.getReadMethod() == null && pd.getReadMethod() != null)) {
321                                                // GenericTypeAwarePropertyDescriptor leniently resolves a set* write method
322                                                // against a declared read method, so we prefer read method descriptors here.
323                                                pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
324                                                this.propertyDescriptors.put(pd.getName(), pd);
325                                        }
326                                }
327                                introspectInterfaces(ifc, ifc);
328                        }
329                }
330        }
331
332
333        BeanInfo getBeanInfo() {
334                return this.beanInfo;
335        }
336
337        Class<?> getBeanClass() {
338                return this.beanInfo.getBeanDescriptor().getBeanClass();
339        }
340
341        @Nullable
342        PropertyDescriptor getPropertyDescriptor(String name) {
343                PropertyDescriptor pd = this.propertyDescriptors.get(name);
344                if (pd == null && StringUtils.hasLength(name)) {
345                        // Same lenient fallback checking as in Property...
346                        pd = this.propertyDescriptors.get(StringUtils.uncapitalize(name));
347                        if (pd == null) {
348                                pd = this.propertyDescriptors.get(StringUtils.capitalize(name));
349                        }
350                }
351                return pd;
352        }
353
354        PropertyDescriptor[] getPropertyDescriptors() {
355                return this.propertyDescriptors.values().toArray(EMPTY_PROPERTY_DESCRIPTOR_ARRAY);
356        }
357
358        private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class<?> beanClass, PropertyDescriptor pd) {
359                try {
360                        return new GenericTypeAwarePropertyDescriptor(beanClass, pd.getName(), pd.getReadMethod(),
361                                        pd.getWriteMethod(), pd.getPropertyEditorClass());
362                }
363                catch (IntrospectionException ex) {
364                        throw new FatalBeanException("Failed to re-introspect class [" + beanClass.getName() + "]", ex);
365                }
366        }
367
368        TypeDescriptor addTypeDescriptor(PropertyDescriptor pd, TypeDescriptor td) {
369                TypeDescriptor existing = this.typeDescriptorCache.putIfAbsent(pd, td);
370                return (existing != null ? existing : td);
371        }
372
373        @Nullable
374        TypeDescriptor getTypeDescriptor(PropertyDescriptor pd) {
375                return this.typeDescriptorCache.get(pd);
376        }
377
378}