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.Iterator; 025import java.util.LinkedHashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029import java.util.concurrent.ConcurrentHashMap; 030import java.util.concurrent.ConcurrentMap; 031 032import org.apache.commons.logging.Log; 033import org.apache.commons.logging.LogFactory; 034 035import org.springframework.core.SpringProperties; 036import org.springframework.core.convert.TypeDescriptor; 037import org.springframework.core.io.support.SpringFactoriesLoader; 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 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 096 private static final boolean shouldIntrospectorIgnoreBeaninfoClasses = 097 SpringProperties.getFlag(IGNORE_BEANINFO_PROPERTY_NAME); 098 099 /** Stores the BeanInfoFactory instances */ 100 private static final 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 == 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 // Explicitly check implemented interfaces for setter/getter methods as well, 307 // in particular for Java 8 default methods... 308 Class<?> clazz = beanClass; 309 while (clazz != null) { 310 Class<?>[] ifcs = clazz.getInterfaces(); 311 for (Class<?> ifc : ifcs) { 312 BeanInfo ifcInfo = Introspector.getBeanInfo(ifc, Introspector.IGNORE_ALL_BEANINFO); 313 PropertyDescriptor[] ifcPds = ifcInfo.getPropertyDescriptors(); 314 for (PropertyDescriptor pd : ifcPds) { 315 if (!this.propertyDescriptorCache.containsKey(pd.getName())) { 316 pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd); 317 this.propertyDescriptorCache.put(pd.getName(), pd); 318 } 319 } 320 } 321 clazz = clazz.getSuperclass(); 322 } 323 324 this.typeDescriptorCache = new ConcurrentReferenceHashMap<PropertyDescriptor, TypeDescriptor>(); 325 } 326 catch (IntrospectionException ex) { 327 throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex); 328 } 329 } 330 331 BeanInfo getBeanInfo() { 332 return this.beanInfo; 333 } 334 335 Class<?> getBeanClass() { 336 return this.beanInfo.getBeanDescriptor().getBeanClass(); 337 } 338 339 PropertyDescriptor getPropertyDescriptor(String name) { 340 PropertyDescriptor pd = this.propertyDescriptorCache.get(name); 341 if (pd == null && StringUtils.hasLength(name)) { 342 // Same lenient fallback checking as in Property... 343 pd = this.propertyDescriptorCache.get(StringUtils.uncapitalize(name)); 344 if (pd == null) { 345 pd = this.propertyDescriptorCache.get(StringUtils.capitalize(name)); 346 } 347 } 348 return (pd == null || pd instanceof GenericTypeAwarePropertyDescriptor ? pd : 349 buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd)); 350 } 351 352 PropertyDescriptor[] getPropertyDescriptors() { 353 PropertyDescriptor[] pds = new PropertyDescriptor[this.propertyDescriptorCache.size()]; 354 int i = 0; 355 for (PropertyDescriptor pd : this.propertyDescriptorCache.values()) { 356 pds[i] = (pd instanceof GenericTypeAwarePropertyDescriptor ? pd : 357 buildGenericTypeAwarePropertyDescriptor(getBeanClass(), pd)); 358 i++; 359 } 360 return pds; 361 } 362 363 private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class<?> beanClass, PropertyDescriptor pd) { 364 try { 365 return new GenericTypeAwarePropertyDescriptor(beanClass, pd.getName(), pd.getReadMethod(), 366 pd.getWriteMethod(), pd.getPropertyEditorClass()); 367 } 368 catch (IntrospectionException ex) { 369 throw new FatalBeanException("Failed to re-introspect class [" + beanClass.getName() + "]", ex); 370 } 371 } 372 373 TypeDescriptor addTypeDescriptor(PropertyDescriptor pd, TypeDescriptor td) { 374 TypeDescriptor existing = this.typeDescriptorCache.putIfAbsent(pd, td); 375 return (existing != null ? existing : td); 376 } 377 378 TypeDescriptor getTypeDescriptor(PropertyDescriptor pd) { 379 return this.typeDescriptorCache.get(pd); 380 } 381 382}