All Downloads are FREE. Search and download functionalities are using the official Maven repository.

infra.beans.CachedIntrospectionResults Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017 - 2024 the original author or authors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see [https://www.gnu.org/licenses/]
 */

package infra.beans;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import infra.lang.Nullable;
import infra.lang.TodayStrategies;
import infra.logging.Logger;
import infra.logging.LoggerFactory;
import infra.util.ClassUtils;
import infra.util.ConcurrentReferenceHashMap;
import infra.util.StringUtils;

/**
 * Internal class that caches JavaBeans {@link java.beans.PropertyDescriptor}
 * information for a Java class. Not intended for direct use by application code.
 *
 * 

Necessary for Framework's own caching of bean descriptors within the application * {@link ClassLoader}, rather than relying on the JDK's system-wide {@link BeanInfo} * cache (in order to avoid leaks on individual application shutdown in a shared JVM). * *

Information is cached statically, so we don't need to create new * objects of this class for every JavaBean we manipulate. Hence, this class * implements the factory design pattern, using a private constructor and * a static {@link #forClass(Class)} factory method to obtain instances. * *

Note that for caching to work effectively, some preconditions need to be met: * Prefer an arrangement where the Framework jars live in the same ClassLoader as the * application classes, which allows for clean caching along with the application's * lifecycle in any case. * *

In case of a non-clean ClassLoader arrangement without a cleanup listener having * been set up, this class will fall back to a weak-reference-based caching model that * recreates much-requested entries every time the garbage collector removed them. In * such a scenario, consider the {@link #IGNORE_BEANINFO_PROPERTY_NAME} system property. * * @author Rod Johnson * @author Juergen Hoeller * @author Harry Yang * @see #acceptClassLoader(ClassLoader) * @see #clearClassLoader(ClassLoader) * @see #forClass(Class) * @since 4.0 2022/2/23 11:10 */ public final class CachedIntrospectionResults { /** * System property that instructs Framework to use the {@link Introspector#IGNORE_ALL_BEANINFO} * mode when calling the JavaBeans {@link Introspector}: "infra.beaninfo.ignore", with a * value of "true" skipping the search for {@code BeanInfo} classes (typically for scenarios * where no such classes are being defined for beans in the application in the first place). *

The default is "false", considering all {@code BeanInfo} metadata classes, like for * standard {@link Introspector#getBeanInfo(Class)} calls. Consider switching this flag to * "true" if you experience repeated ClassLoader access for non-existing {@code BeanInfo} * classes, in case such access is expensive on startup or on lazy loading. *

Note that such an effect may also indicate a scenario where caching doesn't work * effectively: Prefer an arrangement where the Framework jars live in the same ClassLoader * as the application classes, which allows for clean caching along with the application's * lifecycle in any case. * in case of a multi-ClassLoader layout, which will allow for effective caching as well. * * @see Introspector#getBeanInfo(Class, int) */ public static final String IGNORE_BEANINFO_PROPERTY_NAME = "infra.beaninfo.ignore"; private static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTOR_ARRAY = {}; private static final boolean shouldIntrospectorIgnoreBeanInfoClasses = TodayStrategies.getFlag(IGNORE_BEANINFO_PROPERTY_NAME); /** Stores the BeanInfoFactory instances. */ private static final BeanInfoFactory[] beanInfoFactories; private static final Logger logger = LoggerFactory.getLogger(CachedIntrospectionResults.class); /** * Set of ClassLoaders that this CachedIntrospectionResults class will always * accept classes from, even if the classes do not qualify as cache-safe. */ static final Set acceptedClassLoaders = ConcurrentHashMap.newKeySet(16); /** * Map keyed by Class containing CachedIntrospectionResults, strongly held. * This variant is being used for cache-safe bean classes. */ static final ConcurrentMap, CachedIntrospectionResults> strongClassCache = new ConcurrentHashMap<>(64); /** * Map keyed by Class containing CachedIntrospectionResults, softly held. * This variant is being used for non-cache-safe bean classes. */ static final ConcurrentMap, CachedIntrospectionResults> softClassCache = new ConcurrentReferenceHashMap<>(64); /** The BeanInfo object for the introspected bean class. */ private final BeanInfo beanInfo; /** PropertyDescriptor objects keyed by property name String. */ private final Map propertyDescriptors; static { var factories = TodayStrategies.find( BeanInfoFactory.class, CachedIntrospectionResults.class.getClassLoader()); factories.add(new ExtendedBeanInfoFactory()); beanInfoFactories = factories.toArray(new BeanInfoFactory[0]); } /** * Create a new CachedIntrospectionResults instance for the given class. * * @param beanClass the bean class to analyze * @throws BeansException in case of introspection failure */ public CachedIntrospectionResults(Class beanClass) throws BeansException { try { if (logger.isTraceEnabled()) { logger.trace("Getting BeanInfo for class [{}]", beanClass.getName()); } this.beanInfo = getBeanInfo(beanClass); if (logger.isTraceEnabled()) { logger.trace("Caching PropertyDescriptors for class [{}]", beanClass.getName()); } this.propertyDescriptors = new LinkedHashMap<>(); HashSet readMethodNames = new HashSet<>(); boolean isClass = Class.class == beanClass; // This call is slow so we do it once. PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors(); for (PropertyDescriptor pd : pds) { String name = pd.getName(); if (isClass && ("classLoader".equals(name) || "protectionDomain".equals(name))) { // Only allow all name variants of Class properties continue; } if (logger.isTraceEnabled()) { logger.trace("Found bean property '{}'{}{}", name, (pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : ""), (pd.getPropertyEditorClass() != null ? "; editor [" + pd.getPropertyEditorClass().getName() + "]" : "")); } pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd); propertyDescriptors.put(name, pd); Method readMethod = pd.getReadMethod(); if (readMethod != null) { readMethodNames.add(readMethod.getName()); } } // Explicitly check implemented interfaces for setter/getter methods as well, // in particular for Java 8 default methods... Class currClass = beanClass; while (currClass != null && currClass != Object.class) { introspectInterfaces(beanClass, currClass, readMethodNames); currClass = currClass.getSuperclass(); } // Check for record-style accessors without prefix: e.g. "lastName()" // - accessor method directly referring to instance field of same name // - same convention for component accessors of Java 15 record classes introspectPlainAccessors(beanClass, readMethodNames); } catch (IntrospectionException ex) { throw new FatalBeanException("Failed to obtain BeanInfo for class [%s]".formatted(beanClass.getName()), ex); } } private void introspectInterfaces(Class beanClass, Class currClass, Set readMethodNames) throws IntrospectionException { for (Class ifc : currClass.getInterfaces()) { if (!ClassUtils.isJavaLanguageInterface(ifc)) { for (PropertyDescriptor pd : getBeanInfo(ifc).getPropertyDescriptors()) { PropertyDescriptor existingPd = propertyDescriptors.get(pd.getName()); if (existingPd == null || (existingPd.getReadMethod() == null && pd.getReadMethod() != null)) { // GenericTypeAwarePropertyDescriptor leniently resolves a set* write method // against a declared read method, so we prefer read method descriptors here. pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd); propertyDescriptors.put(pd.getName(), pd); Method readMethod = pd.getReadMethod(); if (readMethod != null) { readMethodNames.add(readMethod.getName()); } } } introspectInterfaces(ifc, ifc, readMethodNames); } } } private void introspectPlainAccessors(Class beanClass, HashSet readMethodNames) throws IntrospectionException { Map descriptors = this.propertyDescriptors; for (Method method : beanClass.getMethods()) { if (!descriptors.containsKey(method.getName()) && !readMethodNames.contains((method.getName())) && isPlainAccessor(method)) { descriptors.put(method.getName(), new GenericTypeAwarePropertyDescriptor(beanClass, method.getName(), method, null, null)); readMethodNames.add(method.getName()); } } } private boolean isPlainAccessor(Method method) { if (method.getParameterCount() > 0 || method.getReturnType() == void.class || method.getDeclaringClass() == Object.class || Modifier.isStatic(method.getModifiers())) { return false; } try { // Accessor method referring to instance field of same name? method.getDeclaringClass().getDeclaredField(method.getName()); return true; } catch (Exception ex) { return false; } } public BeanInfo getBeanInfo() { return this.beanInfo; } public Class getBeanClass() { return this.beanInfo.getBeanDescriptor().getBeanClass(); } @Nullable public PropertyDescriptor getPropertyDescriptor(String name) { PropertyDescriptor pd = this.propertyDescriptors.get(name); if (pd == null && StringUtils.isNotEmpty(name)) { // Same lenient fallback checking as in Property... pd = this.propertyDescriptors.get(StringUtils.uncapitalize(name)); if (pd == null) { pd = this.propertyDescriptors.get(StringUtils.capitalize(name)); } } return pd; } public PropertyDescriptor[] getPropertyDescriptors() { return this.propertyDescriptors.values().toArray(EMPTY_PROPERTY_DESCRIPTOR_ARRAY); } private PropertyDescriptor buildGenericTypeAwarePropertyDescriptor(Class beanClass, PropertyDescriptor pd) { try { return new GenericTypeAwarePropertyDescriptor(beanClass, pd.getName(), pd.getReadMethod(), pd.getWriteMethod(), pd.getPropertyEditorClass()); } catch (IntrospectionException ex) { throw new FatalBeanException("Failed to re-introspect class [%s]".formatted(beanClass.getName()), ex); } } // static /** * Accept the given ClassLoader as cache-safe, even if its classes would * not qualify as cache-safe in this CachedIntrospectionResults class. *

This configuration method is only relevant in scenarios where the Framework * classes reside in a 'common' ClassLoader (e.g. the system ClassLoader) * whose lifecycle is not coupled to the application. In such a scenario, * CachedIntrospectionResults would by default not cache any of the application's * classes, since they would create a leak in the common ClassLoader. *

Any {@code acceptClassLoader} call at application startup should * be paired with a {@link #clearClassLoader} call at application shutdown. * * @param classLoader the ClassLoader to accept */ public static void acceptClassLoader(@Nullable ClassLoader classLoader) { if (classLoader != null) { acceptedClassLoaders.add(classLoader); } } /** * Clear the introspection cache for the given ClassLoader, removing the * introspection results for all classes underneath that ClassLoader, and * removing the ClassLoader (and its children) from the acceptance list. * * @param classLoader the ClassLoader to clear the cache for */ public static void clearClassLoader(@Nullable ClassLoader classLoader) { acceptedClassLoaders.removeIf(registeredLoader -> isUnderneathClassLoader(registeredLoader, classLoader)); strongClassCache.keySet().removeIf(beanClass -> isUnderneathClassLoader(beanClass.getClassLoader(), classLoader)); softClassCache.keySet().removeIf(beanClass -> isUnderneathClassLoader(beanClass.getClassLoader(), classLoader)); } /** * Create (or get from cache) CachedIntrospectionResults for the given bean class. * * @param beanClass the bean class to analyze * @return the corresponding CachedIntrospectionResults * @throws BeansException in case of introspection failure */ public static CachedIntrospectionResults forClass(Class beanClass) throws BeansException { CachedIntrospectionResults results = strongClassCache.get(beanClass); if (results != null) { return results; } results = softClassCache.get(beanClass); if (results != null) { return results; } results = new CachedIntrospectionResults(beanClass); ConcurrentMap, CachedIntrospectionResults> classCacheToUse; if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) || isClassLoaderAccepted(beanClass.getClassLoader())) { classCacheToUse = strongClassCache; } else { if (logger.isDebugEnabled()) { logger.debug("Not strongly caching class [{}] because it is not cache-safe", beanClass.getName()); } classCacheToUse = softClassCache; } CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results); return existing != null ? existing : results; } /** * Check whether this CachedIntrospectionResults class is configured * to accept the given ClassLoader. * * @param classLoader the ClassLoader to check * @return whether the given ClassLoader is accepted * @see #acceptClassLoader */ private static boolean isClassLoaderAccepted(ClassLoader classLoader) { for (ClassLoader acceptedLoader : acceptedClassLoaders) { if (isUnderneathClassLoader(classLoader, acceptedLoader)) { return true; } } return false; } /** * Check whether the given ClassLoader is underneath the given parent, * that is, whether the parent is within the candidate's hierarchy. * * @param candidate the candidate ClassLoader to check * @param parent the parent ClassLoader to check for */ private static boolean isUnderneathClassLoader(@Nullable ClassLoader candidate, @Nullable ClassLoader parent) { if (candidate == parent) { return true; } if (candidate == null) { return false; } ClassLoader classLoaderToCheck = candidate; while (classLoaderToCheck != null) { classLoaderToCheck = classLoaderToCheck.getParent(); if (classLoaderToCheck == parent) { return true; } } return false; } /** * Retrieve a {@link BeanInfo} descriptor for the given target class. * * @param beanClass the target class to introspect * @return the resulting {@code BeanInfo} descriptor (never {@code null}) * @throws IntrospectionException from the underlying {@link Introspector} */ private static BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { for (BeanInfoFactory beanInfoFactory : beanInfoFactories) { BeanInfo beanInfo = beanInfoFactory.getBeanInfo(beanClass); if (beanInfo != null) { return beanInfo; } } // fallback to default BeanInfo beanInfo = shouldIntrospectorIgnoreBeanInfoClasses ? Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) : Introspector.getBeanInfo(beanClass); // Immediately remove class from Introspector cache to allow for proper garbage // collection on class loader shutdown; we cache it in CachedIntrospectionResults // in a GC-friendly manner. This is necessary (again) for the JDK ClassInfo cache. Class classToFlush = beanClass; do { Introspector.flushFromCaches(classToFlush); classToFlush = classToFlush.getSuperclass(); } while (classToFlush != null && classToFlush != Object.class); return beanInfo; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy