View Javadoc
1   /*
2    * Copyright 2002-2014 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  import java.beans.Introspector;
22  import java.beans.PropertyDescriptor;
23  import java.util.Collections;
24  import java.util.Iterator;
25  import java.util.LinkedHashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Set;
29  import java.util.concurrent.ConcurrentHashMap;
30  import java.util.concurrent.ConcurrentMap;
31  
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  
35  import org.springframework.core.SpringProperties;
36  import org.springframework.core.convert.TypeDescriptor;
37  import org.springframework.core.io.support.SpringFactoriesLoader;
38  import org.springframework.util.ClassUtils;
39  import org.springframework.util.ConcurrentReferenceHashMap;
40  import org.springframework.util.StringUtils;
41  
42  /**
43   * Internal class that caches JavaBeans {@link java.beans.PropertyDescriptor}
44   * information for a Java class. Not intended for direct use by application code.
45   *
46   * <p>Necessary for own caching of descriptors within the application's
47   * ClassLoader, rather than rely on the JDK's system-wide BeanInfo cache
48   * (in order to avoid leaks on ClassLoader shutdown).
49   *
50   * <p>Information is cached statically, so we don't need to create new
51   * objects of this class for every JavaBean we manipulate. Hence, this class
52   * implements the factory design pattern, using a private constructor and
53   * a static {@link #forClass(Class)} factory method to obtain instances.
54   *
55   * <p>Note that for caching to work effectively, some preconditions need to be met:
56   * Prefer an arrangement where the Spring jars live in the same ClassLoader as the
57   * application classes, which allows for clean caching along with the application's
58   * lifecycle in any case. For a web application, consider declaring a local
59   * {@link org.springframework.web.util.IntrospectorCleanupListener} in {@code web.xml}
60   * in case of a multi-ClassLoader layout, which will allow for effective caching as well.
61   *
62   * <p>In case of a non-clean ClassLoader arrangement without a cleanup listener having
63   * been set up, this class will fall back to a weak-reference-based caching model that
64   * recreates much-requested entries every time the garbage collector removed them. In
65   * such a scenario, consider the {@link #IGNORE_BEANINFO_PROPERTY_NAME} system property.
66   *
67   * @author Rod Johnson
68   * @author Juergen Hoeller
69   * @since 05 May 2001
70   * @see #acceptClassLoader(ClassLoader)
71   * @see #clearClassLoader(ClassLoader)
72   * @see #forClass(Class)
73   */
74  public class CachedIntrospectionResults {
75  
76  	/**
77  	 * System property that instructs Spring to use the {@link Introspector#IGNORE_ALL_BEANINFO}
78  	 * mode when calling the JavaBeans {@link Introspector}: "spring.beaninfo.ignore", with a
79  	 * value of "true" skipping the search for {@code BeanInfo} classes (typically for scenarios
80  	 * where no such classes are being defined for beans in the application in the first place).
81  	 * <p>The default is "false", considering all {@code BeanInfo} metadata classes, like for
82  	 * standard {@link Introspector#getBeanInfo(Class)} calls. Consider switching this flag to
83  	 * "true" if you experience repeated ClassLoader access for non-existing {@code BeanInfo}
84  	 * classes, in case such access is expensive on startup or on lazy loading.
85  	 * <p>Note that such an effect may also indicate a scenario where caching doesn't work
86  	 * effectively: Prefer an arrangement where the Spring jars live in the same ClassLoader
87  	 * as the application classes, which allows for clean caching along with the application's
88  	 * lifecycle in any case. For a web application, consider declaring a local
89  	 * {@link org.springframework.web.util.IntrospectorCleanupListener} in {@code web.xml}
90  	 * in case of a multi-ClassLoader layout, which will allow for effective caching as well.
91  	 * @see Introspector#getBeanInfo(Class, int)
92  	 */
93  	public static final String IGNORE_BEANINFO_PROPERTY_NAME = "spring.beaninfo.ignore";
94  
95  
96  	private static final boolean shouldIntrospectorIgnoreBeaninfoClasses =
97  			SpringProperties.getFlag(IGNORE_BEANINFO_PROPERTY_NAME);
98  
99  	/** Stores the BeanInfoFactory instances */
100 	private static 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.equals(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 			this.typeDescriptorCache = new ConcurrentReferenceHashMap<PropertyDescriptor, TypeDescriptor>();
307 		}
308 		catch (IntrospectionException ex) {
309 			throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex);
310 		}
311 	}
312 
313 	BeanInfo getBeanInfo() {
314 		return this.beanInfo;
315 	}
316 
317 	Class<?> getBeanClass() {
318 		return this.beanInfo.getBeanDescriptor().getBeanClass();
319 	}
320 
321 	PropertyDescriptor getPropertyDescriptor(String name) {
322 		PropertyDescriptor pd = this.propertyDescriptorCache.get(name);
323 		if (pd == null && StringUtils.hasLength(name)) {
324 			// Same lenient fallback checking as in PropertyTypeDescriptor...
325 			pd = this.propertyDescriptorCache.get(name.substring(0, 1).toLowerCase() + name.substring(1));
326 			if (pd == null) {
327 				pd = this.propertyDescriptorCache.get(name.substring(0, 1).toUpperCase() + name.substring(1));
328 			}
329 		}
330 		return (pd == null || pd instanceof GenericTypeAwarePropertyDescriptor ? pd :
331 				buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd));
332 	}
333 
334 	PropertyDescriptor[] getPropertyDescriptors() {
335 		PropertyDescriptor[] pds = new PropertyDescriptor[this.propertyDescriptorCache.size()];
336 		int i = 0;
337 		for (PropertyDescriptor pd : this.propertyDescriptorCache.values()) {
338 			pds[i] = (pd instanceof GenericTypeAwarePropertyDescriptor ? pd :
339 					buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd));
340 			i++;
341 		}
342 		return pds;
343 	}
344 
345 	private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class<?> beanClass, PropertyDescriptor pd) {
346 		try {
347 			return new GenericTypeAwarePropertyDescriptor(beanClass, pd.getName(), pd.getReadMethod(),
348 					pd.getWriteMethod(), pd.getPropertyEditorClass());
349 		}
350 		catch (IntrospectionException ex) {
351 			throw new FatalBeanException("Failed to re-introspect class [" + beanClass.getName() + "]", ex);
352 		}
353 	}
354 
355 	TypeDescriptor addTypeDescriptor(PropertyDescriptor pd, TypeDescriptor td) {
356 		TypeDescriptor existing = this.typeDescriptorCache.putIfAbsent(pd, td);
357 		return (existing != null ? existing : td);
358 	}
359 
360 	TypeDescriptor getTypeDescriptor(PropertyDescriptor pd) {
361 		return this.typeDescriptorCache.get(pd);
362 	}
363 
364 }