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