1 /*
2  * Copyright 2002-2013 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.lang.ref.Reference;
24 import java.lang.ref.WeakReference;
25 import java.lang.reflect.Method;
26 import java.util.Collections;
27 import java.util.HashSet;
28 import java.util.Iterator;
29 import java.util.LinkedHashMap;
30 import java.util.Map;
31 import java.util.Set;
32 import java.util.WeakHashMap;
33 
34 import org.apache.commons.logging.Log;
35 import org.apache.commons.logging.LogFactory;
36 import org.springframework.util.ClassUtils;
37 import org.springframework.util.StringUtils;
38 
39 /**
40  * Internal class that caches JavaBeans {@link java.beans.PropertyDescriptor}
41  * information for a Java class. Not intended for direct use by application code.
42  *
43  * <p>Necessary for own caching of descriptors within the application's
44  * ClassLoader, rather than rely on the JDK's system-wide BeanInfo cache
45  * (in order to avoid leaks on ClassLoader shutdown).
46  *
47  * <p>Information is cached statically, so we don't need to create new
48  * objects of this class for every JavaBean we manipulate. Hence, this class
49  * implements the factory design pattern, using a private constructor and
50  * a static {@link #forClass(Class)} factory method to obtain instances.
51  *
52  * @author Rod Johnson
53  * @author Juergen Hoeller
54  * @since 05 May 2001
55  * @see #acceptClassLoader(ClassLoader)
56  * @see #clearClassLoader(ClassLoader)
57  * @see #forClass(Class)
58  */
59 public class CachedIntrospectionResults {
60 
61 	private static final Log logger = LogFactory.getLog(CachedIntrospectionResults.class);
62 
63 	/**
64 	 * Set of ClassLoaders that this CachedIntrospectionResults class will always
65 	 * accept classes from, even if the classes do not qualify as cache-safe.
66 	 */
67 	static final Set<ClassLoader> acceptedClassLoaders = Collections.synchronizedSet(new HashSet<ClassLoader>());
68 
69 	/**
70 	 * Map keyed by class containing CachedIntrospectionResults.
71 	 * Needs to be a WeakHashMap with WeakReferences as values to allow
72 	 * for proper garbage collection in case of multiple class loaders.
73 	 */
74 	static final Map<Class, Object> classCache = Collections.synchronizedMap(new WeakHashMap<Class, Object>());
75 
76 
77 	/**
78 	 * Accept the given ClassLoader as cache-safe, even if its classes would
79 	 * not qualify as cache-safe in this CachedIntrospectionResults class.
80 	 * <p>This configuration method is only relevant in scenarios where the Spring
81 	 * classes reside in a 'common' ClassLoader (e.g. the system ClassLoader)
82 	 * whose lifecycle is not coupled to the application. In such a scenario,
83 	 * CachedIntrospectionResults would by default not cache any of the application's
84 	 * classes, since they would create a leak in the common ClassLoader.
85 	 * <p>Any <code>acceptClassLoader</code> call at application startup should
86 	 * be paired with a {@link #clearClassLoader} call at application shutdown.
87 	 * @param classLoader the ClassLoader to accept
88 	 */
acceptClassLoader(ClassLoader classLoader)89 	public static void acceptClassLoader(ClassLoader classLoader) {
90 		if (classLoader != null) {
91 			acceptedClassLoaders.add(classLoader);
92 		}
93 	}
94 
95 	/**
96 	 * Clear the introspection cache for the given ClassLoader, removing the
97 	 * introspection results for all classes underneath that ClassLoader,
98 	 * and deregistering the ClassLoader (and any of its children) from the
99 	 * acceptance list.
100 	 * @param classLoader the ClassLoader to clear the cache for
101 	 */
clearClassLoader(ClassLoader classLoader)102 	public static void clearClassLoader(ClassLoader classLoader) {
103 		synchronized (classCache) {
104 			for (Iterator<Class> it = classCache.keySet().iterator(); it.hasNext();) {
105 				Class beanClass = it.next();
106 				if (isUnderneathClassLoader(beanClass.getClassLoader(), classLoader)) {
107 					it.remove();
108 				}
109 			}
110 		}
111 		synchronized (acceptedClassLoaders) {
112 			for (Iterator<ClassLoader> it = acceptedClassLoaders.iterator(); it.hasNext();) {
113 				ClassLoader registeredLoader = it.next();
114 				if (isUnderneathClassLoader(registeredLoader, classLoader)) {
115 					it.remove();
116 				}
117 			}
118 		}
119 	}
120 
121 	/**
122 	 * Create CachedIntrospectionResults for the given bean class.
123 	 * <P>We don't want to use synchronization here. Object references are atomic,
124 	 * so we can live with doing the occasional unnecessary lookup at startup only.
125 	 * @param beanClass the bean class to analyze
126 	 * @return the corresponding CachedIntrospectionResults
127 	 * @throws BeansException in case of introspection failure
128 	 */
forClass(Class beanClass)129 	static CachedIntrospectionResults forClass(Class beanClass) throws BeansException {
130 		CachedIntrospectionResults results;
131 		Object value = classCache.get(beanClass);
132 		if (value instanceof Reference) {
133 			Reference ref = (Reference) value;
134 			results = (CachedIntrospectionResults) ref.get();
135 		}
136 		else {
137 			results = (CachedIntrospectionResults) value;
138 		}
139 		if (results == null) {
140 			if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) ||
141 					isClassLoaderAccepted(beanClass.getClassLoader())) {
142 				results = new CachedIntrospectionResults(beanClass);
143 				classCache.put(beanClass, results);
144 			}
145 			else {
146 				if (logger.isDebugEnabled()) {
147 					logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe");
148 				}
149 				results = new CachedIntrospectionResults(beanClass);
150 				classCache.put(beanClass, new WeakReference<CachedIntrospectionResults>(results));
151 			}
152 		}
153 		return results;
154 	}
155 
156 	/**
157 	 * Check whether this CachedIntrospectionResults class is configured
158 	 * to accept the given ClassLoader.
159 	 * @param classLoader the ClassLoader to check
160 	 * @return whether the given ClassLoader is accepted
161 	 * @see #acceptClassLoader
162 	 */
isClassLoaderAccepted(ClassLoader classLoader)163 	private static boolean isClassLoaderAccepted(ClassLoader classLoader) {
164 		// Iterate over array copy in order to avoid synchronization for the entire
165 		// ClassLoader check (avoiding a synchronized acceptedClassLoaders Iterator).
166 		ClassLoader[] acceptedLoaderArray =
167 				acceptedClassLoaders.toArray(new ClassLoader[acceptedClassLoaders.size()]);
168 		for (ClassLoader registeredLoader : acceptedLoaderArray) {
169 			if (isUnderneathClassLoader(classLoader, registeredLoader)) {
170 				return true;
171 			}
172 		}
173 		return false;
174 	}
175 
176 	/**
177 	 * Check whether the given ClassLoader is underneath the given parent,
178 	 * that is, whether the parent is within the candidate's hierarchy.
179 	 * @param candidate the candidate ClassLoader to check
180 	 * @param parent the parent ClassLoader to check for
181 	 */
isUnderneathClassLoader(ClassLoader candidate, ClassLoader parent)182 	private static boolean isUnderneathClassLoader(ClassLoader candidate, ClassLoader parent) {
183 		if (candidate == parent) {
184 			return true;
185 		}
186 		if (candidate == null) {
187 			return false;
188 		}
189 		ClassLoader classLoaderToCheck = candidate;
190 		while (classLoaderToCheck != null) {
191 			classLoaderToCheck = classLoaderToCheck.getParent();
192 			if (classLoaderToCheck == parent) {
193 				return true;
194 			}
195 		}
196 		return false;
197 	}
198 
199 
200 	/** The BeanInfo object for the introspected bean class */
201 	private final BeanInfo beanInfo;
202 
203 	/** PropertyDescriptor objects keyed by property name String */
204 	private final Map<String, PropertyDescriptor> propertyDescriptorCache;
205 
206 
207 	/**
208 	 * Create a new CachedIntrospectionResults instance for the given class.
209 	 * @param beanClass the bean class to analyze
210 	 * @throws BeansException in case of introspection failure
211 	 */
CachedIntrospectionResults(Class beanClass)212 	private CachedIntrospectionResults(Class beanClass) throws BeansException {
213 		try {
214 			if (logger.isTraceEnabled()) {
215 				logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
216 			}
217 			BeanInfo originalBeanInfo = Introspector.getBeanInfo(beanClass);
218 			BeanInfo extendedBeanInfo = null;
219 			for (Method method : beanClass.getMethods()) {
220 				if (ExtendedBeanInfo.isCandidateWriteMethod(method)) {
221 					extendedBeanInfo = new ExtendedBeanInfo(originalBeanInfo);
222 					break;
223 				}
224 			}
225 			this.beanInfo = extendedBeanInfo != null ? extendedBeanInfo : originalBeanInfo;
226 
227 			// Immediately remove class from Introspector cache, to allow for proper
228 			// garbage collection on class loader shutdown - we cache it here anyway,
229 			// in a GC-friendly manner. In contrast to CachedIntrospectionResults,
230 			// Introspector does not use WeakReferences as values of its WeakHashMap!
231 			Class classToFlush = beanClass;
232 			do {
233 				Introspector.flushFromCaches(classToFlush);
234 				classToFlush = classToFlush.getSuperclass();
235 			}
236 			while (classToFlush != null);
237 
238 			if (logger.isTraceEnabled()) {
239 				logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]");
240 			}
241 			this.propertyDescriptorCache = new LinkedHashMap<String, PropertyDescriptor>();
242 
243 			// This call is slow so we do it once.
244 			PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
245 			for (PropertyDescriptor pd : pds) {
246 				if (Class.class.equals(beanClass) && "classLoader".equals(pd.getName())) {
247 					// Ignore Class.getClassLoader() method - nobody needs to bind to that
248 					continue;
249 				}
250 				if (logger.isTraceEnabled()) {
251 					logger.trace("Found bean property '" + pd.getName() + "'" +
252 							(pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : "") +
253 							(pd.getPropertyEditorClass() != null ?
254 									"; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));
255 				}
256 				pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
257 				this.propertyDescriptorCache.put(pd.getName(), pd);
258 			}
259 		}
260 		catch (IntrospectionException ex) {
261 			throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex);
262 		}
263 	}
264 
getBeanInfo()265 	BeanInfo getBeanInfo() {
266 		return this.beanInfo;
267 	}
268 
getBeanClass()269 	Class getBeanClass() {
270 		return this.beanInfo.getBeanDescriptor().getBeanClass();
271 	}
272 
getPropertyDescriptor(String name)273 	PropertyDescriptor getPropertyDescriptor(String name) {
274 		PropertyDescriptor pd = this.propertyDescriptorCache.get(name);
275 		if (pd == null && StringUtils.hasLength(name)) {
276 			// Same lenient fallback checking as in PropertyTypeDescriptor...
277 			pd = this.propertyDescriptorCache.get(name.substring(0, 1).toLowerCase() + name.substring(1));
278 			if (pd == null) {
279 				pd = this.propertyDescriptorCache.get(name.substring(0, 1).toUpperCase() + name.substring(1));
280 			}
281 		}
282 		return (pd == null || pd instanceof GenericTypeAwarePropertyDescriptor ? pd :
283 				buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd));
284 	}
285 
getPropertyDescriptors()286 	PropertyDescriptor[] getPropertyDescriptors() {
287 		PropertyDescriptor[] pds = new PropertyDescriptor[this.propertyDescriptorCache.size()];
288 		int i = 0;
289 		for (PropertyDescriptor pd : this.propertyDescriptorCache.values()) {
290 			pds[i] = (pd instanceof GenericTypeAwarePropertyDescriptor ? pd :
291 					buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd));
292 			i++;
293 		}
294 		return pds;
295 	}
296 
buildGenericTypeAwarePropertyDescriptor(Class beanClass, PropertyDescriptor pd)297 	private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class beanClass, PropertyDescriptor pd) {
298 		try {
299 			return new GenericTypeAwarePropertyDescriptor(beanClass, pd.getName(), pd.getReadMethod(),
300 					pd.getWriteMethod(), pd.getPropertyEditorClass());
301 		}
302 		catch (IntrospectionException ex) {
303 			throw new FatalBeanException("Failed to re-introspect class [" + beanClass.getName() + "]", ex);
304 		}
305 	}
306 
307 }
308