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.LinkedHashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Set; 028import java.util.concurrent.ConcurrentHashMap; 029import java.util.concurrent.ConcurrentMap; 030 031import org.apache.commons.logging.Log; 032import org.apache.commons.logging.LogFactory; 033 034import org.springframework.core.SpringProperties; 035import org.springframework.core.convert.TypeDescriptor; 036import org.springframework.core.io.support.SpringFactoriesLoader; 037import org.springframework.lang.Nullable; 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 final 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 private static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTOR_ARRAY = {}; 096 097 098 private static final boolean shouldIntrospectorIgnoreBeaninfoClasses = 099 SpringProperties.getFlag(IGNORE_BEANINFO_PROPERTY_NAME); 100 101 /** Stores the BeanInfoFactory instances. */ 102 private static final List<BeanInfoFactory> beanInfoFactories = SpringFactoriesLoader.loadFactories( 103 BeanInfoFactory.class, CachedIntrospectionResults.class.getClassLoader()); 104 105 private static final Log logger = LogFactory.getLog(CachedIntrospectionResults.class); 106 107 /** 108 * Set of ClassLoaders that this CachedIntrospectionResults class will always 109 * accept classes from, even if the classes do not qualify as cache-safe. 110 */ 111 static final Set<ClassLoader> acceptedClassLoaders = 112 Collections.newSetFromMap(new ConcurrentHashMap<>(16)); 113 114 /** 115 * Map keyed by Class containing CachedIntrospectionResults, strongly held. 116 * This variant is being used for cache-safe bean classes. 117 */ 118 static final ConcurrentMap<Class<?>, CachedIntrospectionResults> strongClassCache = 119 new ConcurrentHashMap<>(64); 120 121 /** 122 * Map keyed by Class containing CachedIntrospectionResults, softly held. 123 * This variant is being used for non-cache-safe bean classes. 124 */ 125 static final ConcurrentMap<Class<?>, CachedIntrospectionResults> softClassCache = 126 new ConcurrentReferenceHashMap<>(64); 127 128 129 /** 130 * Accept the given ClassLoader as cache-safe, even if its classes would 131 * not qualify as cache-safe in this CachedIntrospectionResults class. 132 * <p>This configuration method is only relevant in scenarios where the Spring 133 * classes reside in a 'common' ClassLoader (e.g. the system ClassLoader) 134 * whose lifecycle is not coupled to the application. In such a scenario, 135 * CachedIntrospectionResults would by default not cache any of the application's 136 * classes, since they would create a leak in the common ClassLoader. 137 * <p>Any {@code acceptClassLoader} call at application startup should 138 * be paired with a {@link #clearClassLoader} call at application shutdown. 139 * @param classLoader the ClassLoader to accept 140 */ 141 public static void acceptClassLoader(@Nullable ClassLoader classLoader) { 142 if (classLoader != null) { 143 acceptedClassLoaders.add(classLoader); 144 } 145 } 146 147 /** 148 * Clear the introspection cache for the given ClassLoader, removing the 149 * introspection results for all classes underneath that ClassLoader, and 150 * removing the ClassLoader (and its children) from the acceptance list. 151 * @param classLoader the ClassLoader to clear the cache for 152 */ 153 public static void clearClassLoader(@Nullable ClassLoader classLoader) { 154 acceptedClassLoaders.removeIf(registeredLoader -> 155 isUnderneathClassLoader(registeredLoader, classLoader)); 156 strongClassCache.keySet().removeIf(beanClass -> 157 isUnderneathClassLoader(beanClass.getClassLoader(), classLoader)); 158 softClassCache.keySet().removeIf(beanClass -> 159 isUnderneathClassLoader(beanClass.getClassLoader(), classLoader)); 160 } 161 162 /** 163 * Create CachedIntrospectionResults for the given bean class. 164 * @param beanClass the bean class to analyze 165 * @return the corresponding CachedIntrospectionResults 166 * @throws BeansException in case of introspection failure 167 */ 168 static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException { 169 CachedIntrospectionResults results = strongClassCache.get(beanClass); 170 if (results != null) { 171 return results; 172 } 173 results = softClassCache.get(beanClass); 174 if (results != null) { 175 return results; 176 } 177 178 results = new CachedIntrospectionResults(beanClass); 179 ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse; 180 181 if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) || 182 isClassLoaderAccepted(beanClass.getClassLoader())) { 183 classCacheToUse = strongClassCache; 184 } 185 else { 186 if (logger.isDebugEnabled()) { 187 logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe"); 188 } 189 classCacheToUse = softClassCache; 190 } 191 192 CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results); 193 return (existing != null ? existing : results); 194 } 195 196 /** 197 * Check whether this CachedIntrospectionResults class is configured 198 * to accept the given ClassLoader. 199 * @param classLoader the ClassLoader to check 200 * @return whether the given ClassLoader is accepted 201 * @see #acceptClassLoader 202 */ 203 private static boolean isClassLoaderAccepted(ClassLoader classLoader) { 204 for (ClassLoader acceptedLoader : acceptedClassLoaders) { 205 if (isUnderneathClassLoader(classLoader, acceptedLoader)) { 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(@Nullable ClassLoader candidate, @Nullable ClassLoader parent) { 219 if (candidate == parent) { 220 return true; 221 } 222 if (candidate == null) { 223 return false; 224 } 225 ClassLoader classLoaderToCheck = candidate; 226 while (classLoaderToCheck != null) { 227 classLoaderToCheck = classLoaderToCheck.getParent(); 228 if (classLoaderToCheck == parent) { 229 return true; 230 } 231 } 232 return false; 233 } 234 235 /** 236 * Retrieve a {@link BeanInfo} descriptor for the given target class. 237 * @param beanClass the target class to introspect 238 * @return the resulting {@code BeanInfo} descriptor (never {@code null}) 239 * @throws IntrospectionException from the underlying {@link Introspector} 240 */ 241 private static BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException { 242 for (BeanInfoFactory beanInfoFactory : beanInfoFactories) { 243 BeanInfo beanInfo = beanInfoFactory.getBeanInfo(beanClass); 244 if (beanInfo != null) { 245 return beanInfo; 246 } 247 } 248 return (shouldIntrospectorIgnoreBeaninfoClasses ? 249 Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) : 250 Introspector.getBeanInfo(beanClass)); 251 } 252 253 254 /** The BeanInfo object for the introspected bean class. */ 255 private final BeanInfo beanInfo; 256 257 /** PropertyDescriptor objects keyed by property name String. */ 258 private final Map<String, PropertyDescriptor> propertyDescriptors; 259 260 /** TypeDescriptor objects keyed by PropertyDescriptor. */ 261 private final ConcurrentMap<PropertyDescriptor, TypeDescriptor> typeDescriptorCache; 262 263 264 /** 265 * Create a new CachedIntrospectionResults instance for the given class. 266 * @param beanClass the bean class to analyze 267 * @throws BeansException in case of introspection failure 268 */ 269 private CachedIntrospectionResults(Class<?> beanClass) throws BeansException { 270 try { 271 if (logger.isTraceEnabled()) { 272 logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]"); 273 } 274 this.beanInfo = getBeanInfo(beanClass); 275 276 if (logger.isTraceEnabled()) { 277 logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]"); 278 } 279 this.propertyDescriptors = new LinkedHashMap<>(); 280 281 // This call is slow so we do it once. 282 PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors(); 283 for (PropertyDescriptor pd : pds) { 284 if (Class.class == beanClass && 285 ("classLoader".equals(pd.getName()) || "protectionDomain".equals(pd.getName()))) { 286 // Ignore Class.getClassLoader() and getProtectionDomain() methods - nobody needs to bind to those 287 continue; 288 } 289 if (logger.isTraceEnabled()) { 290 logger.trace("Found bean property '" + pd.getName() + "'" + 291 (pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : "") + 292 (pd.getPropertyEditorClass() != null ? 293 "; editor [" + pd.getPropertyEditorClass().getName() + "]" : "")); 294 } 295 pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd); 296 this.propertyDescriptors.put(pd.getName(), pd); 297 } 298 299 // Explicitly check implemented interfaces for setter/getter methods as well, 300 // in particular for Java 8 default methods... 301 Class<?> currClass = beanClass; 302 while (currClass != null && currClass != Object.class) { 303 introspectInterfaces(beanClass, currClass); 304 currClass = currClass.getSuperclass(); 305 } 306 307 this.typeDescriptorCache = new ConcurrentReferenceHashMap<>(); 308 } 309 catch (IntrospectionException ex) { 310 throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex); 311 } 312 } 313 314 private void introspectInterfaces(Class<?> beanClass, Class<?> currClass) throws IntrospectionException { 315 for (Class<?> ifc : currClass.getInterfaces()) { 316 if (!ClassUtils.isJavaLanguageInterface(ifc)) { 317 for (PropertyDescriptor pd : getBeanInfo(ifc).getPropertyDescriptors()) { 318 PropertyDescriptor existingPd = this.propertyDescriptors.get(pd.getName()); 319 if (existingPd == null || 320 (existingPd.getReadMethod() == null && pd.getReadMethod() != null)) { 321 // GenericTypeAwarePropertyDescriptor leniently resolves a set* write method 322 // against a declared read method, so we prefer read method descriptors here. 323 pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd); 324 this.propertyDescriptors.put(pd.getName(), pd); 325 } 326 } 327 introspectInterfaces(ifc, ifc); 328 } 329 } 330 } 331 332 333 BeanInfo getBeanInfo() { 334 return this.beanInfo; 335 } 336 337 Class<?> getBeanClass() { 338 return this.beanInfo.getBeanDescriptor().getBeanClass(); 339 } 340 341 @Nullable 342 PropertyDescriptor getPropertyDescriptor(String name) { 343 PropertyDescriptor pd = this.propertyDescriptors.get(name); 344 if (pd == null && StringUtils.hasLength(name)) { 345 // Same lenient fallback checking as in Property... 346 pd = this.propertyDescriptors.get(StringUtils.uncapitalize(name)); 347 if (pd == null) { 348 pd = this.propertyDescriptors.get(StringUtils.capitalize(name)); 349 } 350 } 351 return pd; 352 } 353 354 PropertyDescriptor[] getPropertyDescriptors() { 355 return this.propertyDescriptors.values().toArray(EMPTY_PROPERTY_DESCRIPTOR_ARRAY); 356 } 357 358 private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class<?> beanClass, PropertyDescriptor pd) { 359 try { 360 return new GenericTypeAwarePropertyDescriptor(beanClass, pd.getName(), pd.getReadMethod(), 361 pd.getWriteMethod(), pd.getPropertyEditorClass()); 362 } 363 catch (IntrospectionException ex) { 364 throw new FatalBeanException("Failed to re-introspect class [" + beanClass.getName() + "]", ex); 365 } 366 } 367 368 TypeDescriptor addTypeDescriptor(PropertyDescriptor pd, TypeDescriptor td) { 369 TypeDescriptor existing = this.typeDescriptorCache.putIfAbsent(pd, td); 370 return (existing != null ? existing : td); 371 } 372 373 @Nullable 374 TypeDescriptor getTypeDescriptor(PropertyDescriptor pd) { 375 return this.typeDescriptorCache.get(pd); 376 } 377 378}