001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.beans;
018:
019: import java.beans.BeanInfo;
020: import java.beans.IntrospectionException;
021: import java.beans.Introspector;
022: import java.beans.PropertyDescriptor;
023: import java.lang.ref.Reference;
024: import java.lang.ref.WeakReference;
025: import java.util.Collections;
026: import java.util.HashMap;
027: import java.util.HashSet;
028: import java.util.Iterator;
029: import java.util.Map;
030: import java.util.Set;
031: import java.util.WeakHashMap;
032:
033: import org.apache.commons.logging.Log;
034: import org.apache.commons.logging.LogFactory;
035:
036: /**
037: * Internal class that caches JavaBeans {@link java.beans.PropertyDescriptor}
038: * information for a Java class. Not intended for direct use by application code.
039: *
040: * <p>Necessary as {@link java.beans.Introspector#getBeanInfo()} in JDK 1.3 will
041: * return a new deep copy of the BeanInfo every time we ask for it. We take the
042: * opportunity to cache property descriptors by method name for fast lookup.
043: * Furthermore, we do our own caching of descriptors here, rather than rely on
044: * the JDK's system-wide BeanInfo cache (to avoid leaks on ClassLoader shutdown).
045: *
046: * <p>Information is cached statically, so we don't need to create new
047: * objects of this class for every JavaBean we manipulate. Hence, this class
048: * implements the factory design pattern, using a private constructor and
049: * a static {@link #forClass(Class)} factory method to obtain instances.
050: *
051: * @author Rod Johnson
052: * @author Juergen Hoeller
053: * @since 05 May 2001
054: * @see #acceptClassLoader(ClassLoader)
055: * @see #clearClassLoader(ClassLoader)
056: * @see #forClass(Class)
057: */
058: public class CachedIntrospectionResults {
059:
060: private static final Log logger = LogFactory
061: .getLog(CachedIntrospectionResults.class);
062:
063: /**
064: * Set of ClassLoaders that this CachedIntrospectionResults class will always
065: * accept classes from, even if the classes do not qualify as cache-safe.
066: */
067: static final Set acceptedClassLoaders = Collections
068: .synchronizedSet(new HashSet());
069:
070: /**
071: * Map keyed by class containing CachedIntrospectionResults.
072: * Needs to be a WeakHashMap with WeakReferences as values to allow
073: * for proper garbage collection in case of multiple class loaders.
074: */
075: static final Map classCache = Collections
076: .synchronizedMap(new WeakHashMap());
077:
078: /**
079: * Accept the given ClassLoader as cache-safe, even if its classes would
080: * not qualify as cache-safe in this CachedIntrospectionResults class.
081: * <p>This configuration method is only relevant in scenarios where the Spring
082: * classes reside in a 'common' ClassLoader (e.g. the system ClassLoader)
083: * whose lifecycle is not coupled to the application. In such a scenario,
084: * CachedIntrospectionResults would by default not cache any of the application's
085: * classes, since they would create a leak in the common ClassLoader.
086: * <p>Any <code>acceptClassLoader</code> call at application startup should
087: * be paired with a {@link #clearClassLoader} call at application shutdown.
088: * @param classLoader the ClassLoader to accept
089: */
090: public static void acceptClassLoader(ClassLoader classLoader) {
091: if (classLoader != null) {
092: acceptedClassLoaders.add(classLoader);
093: }
094: }
095:
096: /**
097: * Clear the introspection cache for the given ClassLoader, removing the
098: * introspection results for all classes underneath that ClassLoader,
099: * and deregistering the ClassLoader (and any of its children) from the
100: * acceptance list.
101: * @param classLoader the ClassLoader to clear the cache for
102: */
103: public static void clearClassLoader(ClassLoader classLoader) {
104: if (classLoader == null) {
105: return;
106: }
107: synchronized (classCache) {
108: for (Iterator it = classCache.keySet().iterator(); it
109: .hasNext();) {
110: Class beanClass = (Class) it.next();
111: if (isUnderneathClassLoader(beanClass.getClassLoader(),
112: classLoader)) {
113: it.remove();
114: }
115: }
116: }
117: synchronized (acceptedClassLoaders) {
118: for (Iterator it = acceptedClassLoaders.iterator(); it
119: .hasNext();) {
120: ClassLoader registeredLoader = (ClassLoader) it.next();
121: if (isUnderneathClassLoader(registeredLoader,
122: classLoader)) {
123: it.remove();
124: }
125: }
126: }
127: }
128:
129: /**
130: * Create CachedIntrospectionResults for the given bean class.
131: * <P>We don't want to use synchronization here. Object references are atomic,
132: * so we can live with doing the occasional unnecessary lookup at startup only.
133: * @param beanClass the bean class to analyze
134: * @return the corresponding CachedIntrospectionResults
135: * @throws BeansException in case of introspection failure
136: */
137: static CachedIntrospectionResults forClass(Class beanClass)
138: throws BeansException {
139: CachedIntrospectionResults results = null;
140: Object value = classCache.get(beanClass);
141: if (value instanceof Reference) {
142: Reference ref = (Reference) value;
143: results = (CachedIntrospectionResults) ref.get();
144: } else {
145: results = (CachedIntrospectionResults) value;
146: }
147: if (results == null) {
148: // can throw BeansException
149: results = new CachedIntrospectionResults(beanClass);
150: if (isCacheSafe(beanClass)
151: || isClassLoaderAccepted(beanClass.getClassLoader())) {
152: classCache.put(beanClass, results);
153: } else {
154: if (logger.isDebugEnabled()) {
155: logger.debug("Not strongly caching class ["
156: + beanClass.getName()
157: + "] because it is not cache-safe");
158: }
159: classCache.put(beanClass, new WeakReference(results));
160: }
161: }
162: return results;
163: }
164:
165: /**
166: * Check whether this CachedIntrospectionResults class is configured
167: * to accept the given ClassLoader.
168: * @param classLoader the ClassLoader to check
169: * @return whether the given ClassLoader is accepted
170: * @see #acceptClassLoader
171: */
172: private static boolean isClassLoaderAccepted(ClassLoader classLoader) {
173: // Iterate over array copy in order to avoid synchronization for the entire
174: // ClassLoader check (avoiding a synchronized acceptedClassLoaders Iterator).
175: Object[] acceptedLoaderArray = acceptedClassLoaders.toArray();
176: for (int i = 0; i < acceptedLoaderArray.length; i++) {
177: ClassLoader registeredLoader = (ClassLoader) acceptedLoaderArray[i];
178: if (isUnderneathClassLoader(classLoader, registeredLoader)) {
179: return true;
180: }
181: }
182: return false;
183: }
184:
185: /**
186: * Check whether the given class is cache-safe,
187: * i.e. whether it is loaded by the same class loader as the
188: * CachedIntrospectionResults class or a parent of it.
189: * <p>Many thanks to Guillaume Poirier for pointing out the
190: * garbage collection issues and for suggesting this solution.
191: * @param clazz the class to analyze
192: */
193: private static boolean isCacheSafe(Class clazz) {
194: ClassLoader target = clazz.getClassLoader();
195: if (target == null) {
196: return false;
197: }
198: ClassLoader cur = CachedIntrospectionResults.class
199: .getClassLoader();
200: if (cur == target) {
201: return true;
202: }
203: while (cur != null) {
204: cur = cur.getParent();
205: if (cur == target) {
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(
219: ClassLoader candidate, ClassLoader parent) {
220: if (candidate == null) {
221: return false;
222: }
223: if (candidate == parent) {
224: return true;
225: }
226: ClassLoader classLoaderToCheck = candidate;
227: while (classLoaderToCheck != null) {
228: classLoaderToCheck = classLoaderToCheck.getParent();
229: if (classLoaderToCheck == parent) {
230: return true;
231: }
232: }
233: return false;
234: }
235:
236: /** The BeanInfo object for the introspected bean class */
237: private final BeanInfo beanInfo;
238:
239: /** PropertyDescriptor objects keyed by property name String */
240: private final Map propertyDescriptorCache;
241:
242: /**
243: * Create a new CachedIntrospectionResults instance for the given class.
244: * @param beanClass the bean class to analyze
245: * @throws BeansException in case of introspection failure
246: */
247: private CachedIntrospectionResults(Class beanClass)
248: throws BeansException {
249: try {
250: if (logger.isTraceEnabled()) {
251: logger.trace("Getting BeanInfo for class ["
252: + beanClass.getName() + "]");
253: }
254: this .beanInfo = Introspector.getBeanInfo(beanClass);
255:
256: // Immediately remove class from Introspector cache, to allow for proper
257: // garbage collection on class loader shutdown - we cache it here anyway,
258: // in a GC-friendly manner. In contrast to CachedIntrospectionResults,
259: // Introspector does not use WeakReferences as values of its WeakHashMap!
260: Class classToFlush = beanClass;
261: do {
262: Introspector.flushFromCaches(classToFlush);
263: classToFlush = classToFlush.getSuperclass();
264: } while (classToFlush != null);
265:
266: if (logger.isTraceEnabled()) {
267: logger.trace("Caching PropertyDescriptors for class ["
268: + beanClass.getName() + "]");
269: }
270: this .propertyDescriptorCache = new HashMap();
271:
272: // This call is slow so we do it once.
273: PropertyDescriptor[] pds = this .beanInfo
274: .getPropertyDescriptors();
275: for (int i = 0; i < pds.length; i++) {
276: PropertyDescriptor pd = pds[i];
277: if (logger.isTraceEnabled()) {
278: logger
279: .trace("Found bean property '"
280: + pd.getName()
281: + "'"
282: + (pd.getPropertyType() != null ? " of type ["
283: + pd.getPropertyType()
284: .getName() + "]"
285: : "")
286: + (pd.getPropertyEditorClass() != null ? "; editor ["
287: + pd
288: .getPropertyEditorClass()
289: .getName() + "]"
290: : ""));
291: }
292: this .propertyDescriptorCache.put(pd.getName(), pd);
293: }
294: } catch (IntrospectionException ex) {
295: throw new FatalBeanException(
296: "Cannot get BeanInfo for object of class ["
297: + beanClass.getName() + "]", ex);
298: }
299: }
300:
301: BeanInfo getBeanInfo() {
302: return this .beanInfo;
303: }
304:
305: Class getBeanClass() {
306: return this .beanInfo.getBeanDescriptor().getBeanClass();
307: }
308:
309: PropertyDescriptor getPropertyDescriptor(String propertyName) {
310: return (PropertyDescriptor) this.propertyDescriptorCache
311: .get(propertyName);
312: }
313:
314: }
|