33 * Copyright Red Hat Inc. and Hibernate Authors
44 */
55package org .hibernate .internal .util .beans ;
6- import java .beans .BeanInfo ;
7- import java .beans .IntrospectionException ;
8- import java .beans .Introspector ;
6+
97import java .lang .reflect .InvocationTargetException ;
8+ import java .lang .reflect .Method ;
9+ import java .lang .reflect .Modifier ;
10+ import java .util .LinkedHashMap ;
11+ import java .util .Map ;
12+
13+ import static org .hibernate .internal .util .StringHelper .decapitalize ;
1014
1115/**
12- * Utility for helping deal with {@link BeanInfo}
16+ * Utility for helping deal with {@link BeanInfo}.
17+ * <p>
18+ * This is a simplified reimplementation to avoid dependency on the {@code java.desktop} module.
19+ * Unlike the JDK's {@code java.beans.Introspector}, this implementation:
20+ * <ul>
21+ * <li>Does not validate type compatibility between getters and setters</li>
22+ * <li>Does not introspect methods from implemented interfaces (only class hierarchy)</li>
23+ * <li>Takes the first matching setter when overloads exist</li>
24+ * </ul>
25+ * These simplifications are acceptable for Hibernate's use case of property-name-based injection.
26+ * <p>
27+ * Caching is implemented using {@link ClassValue}, which is the JVM's built-in mechanism for
28+ * associating computed values with classes. Unlike the JDK's {@code Introspector} cache, this
29+ * approach is inherently GC-friendly: when a class is unloaded, the associated {@link BeanInfo}
30+ * is automatically released, preventing classloader leaks without requiring explicit cache flushing.
1331 *
1432 * @author Steve Ebersole
1533 */
1634public class BeanInfoHelper {
35+
36+ /**
37+ * Cache of BeanInfo instances, keyed by class.
38+ * Uses ClassValue which automatically handles class unloading - when a class is GC'd,
39+ * its associated BeanInfo is also released, preventing classloader leaks.
40+ * This cache is for the common case where stopClass is null (introspect up to Object).
41+ */
42+ private static final ClassValue <BeanInfo > BEAN_INFO_CACHE = new ClassValue <>() {
43+ @ Override
44+ protected BeanInfo computeValue (final Class <?> type ) {
45+ return computeBeanInfo ( type , null );
46+ }
47+ };
48+
1749 public interface BeanInfoDelegate {
1850 void processBeanInfo (BeanInfo beanInfo ) throws Exception ;
1951 }
@@ -48,7 +80,7 @@ public static void visitBeanInfo(Class<?> beanClass, BeanInfoDelegate delegate)
4880
4981 public static void visitBeanInfo (Class <?> beanClass , Class <?> stopClass , BeanInfoDelegate delegate ) {
5082 try {
51- BeanInfo info = Introspector . getBeanInfo ( beanClass , stopClass );
83+ final BeanInfo info = getBeanInfo ( beanClass , stopClass );
5284 try {
5385 delegate .processBeanInfo ( info );
5486 }
@@ -61,11 +93,11 @@ public static void visitBeanInfo(Class<?> beanClass, Class<?> stopClass, BeanInf
6193 catch ( Exception e ) {
6294 throw new BeanIntrospectionException ( "Error delegating bean info use" , e );
6395 }
64- finally {
65- Introspector .flushFromCaches ( beanClass );
66- }
6796 }
68- catch ( IntrospectionException e ) {
97+ catch ( BeanIntrospectionException e ) {
98+ throw e ;
99+ }
100+ catch ( Exception e ) {
69101 throw new BeanIntrospectionException ( "Unable to determine bean info from class [" + beanClass .getName () + "]" , e );
70102 }
71103 }
@@ -76,7 +108,7 @@ public static <T> T visitBeanInfo(Class<?> beanClass, ReturningBeanInfoDelegate<
76108
77109 public static <T > T visitBeanInfo (Class <?> beanClass , Class <?> stopClass , ReturningBeanInfoDelegate <T > delegate ) {
78110 try {
79- BeanInfo info = Introspector . getBeanInfo ( beanClass , stopClass );
111+ final BeanInfo info = getBeanInfo ( beanClass , stopClass );
80112 try {
81113 return delegate .processBeanInfo ( info );
82114 }
@@ -89,14 +121,126 @@ public static <T> T visitBeanInfo(Class<?> beanClass, Class<?> stopClass, Return
89121 catch ( Exception e ) {
90122 throw new BeanIntrospectionException ( "Error delegating bean info use" , e );
91123 }
92- finally {
93- Introspector .flushFromCaches ( beanClass );
94- }
95124 }
96- catch ( IntrospectionException e ) {
125+ catch ( BeanIntrospectionException e ) {
126+ throw e ;
127+ }
128+ catch ( Exception e ) {
97129 throw new BeanIntrospectionException ( "Unable to determine bean info from class [" + beanClass .getName () + "]" , e );
98130 }
99131 }
100132
133+ /**
134+ * Introspect a JavaBean and return a BeanInfo object describing its properties.
135+ * This method walks up the class hierarchy to collect all properties.
136+ * <p>
137+ * Results are cached using {@link ClassValue} for the common case where stopClass is null.
138+ * The cache is GC-friendly and does not cause classloader leaks.
139+ *
140+ * @param beanClass The class to introspect
141+ * @param stopClass The base class at which to stop the analysis. Any methods
142+ * declared in the stopClass or its superclasses will be ignored.
143+ * May be null.
144+ * @return A BeanInfo object describing the target bean
145+ */
146+ public static BeanInfo getBeanInfo (final Class <?> beanClass , final Class <?> stopClass ) {
147+ // Use cache for the common case (stopClass == null means introspect up to Object)
148+ if ( stopClass == null ) {
149+ return BEAN_INFO_CACHE .get ( beanClass );
150+ }
151+ // Non-null stopClass is rare; compute without caching to keep cache simple
152+ return computeBeanInfo ( beanClass , stopClass );
153+ }
154+
155+ private static BeanInfo computeBeanInfo (final Class <?> beanClass , final Class <?> stopClass ) {
156+ // LinkedHashMap for reproducible ordering (important for build-time code)
157+ final Map <String , PropertyDescriptor > properties = new LinkedHashMap <>();
158+
159+ // Walk from subclass to superclass; subclass properties take precedence
160+ Class <?> currentClass = beanClass ;
161+ while ( currentClass != null && currentClass != stopClass && currentClass != Object .class ) {
162+ introspectClass ( currentClass , properties );
163+ currentClass = currentClass .getSuperclass ();
164+ }
165+
166+ final PropertyDescriptor [] descriptors = properties .values ().toArray ( new PropertyDescriptor [0 ] );
167+ return new SimpleBeanInfo ( descriptors );
168+ }
101169
170+ /**
171+ * Simple implementation of {@link BeanInfo} that holds a fixed array of property descriptors.
172+ */
173+ private static class SimpleBeanInfo implements BeanInfo {
174+ private final PropertyDescriptor [] propertyDescriptors ;
175+
176+ SimpleBeanInfo (PropertyDescriptor [] propertyDescriptors ) {
177+ this .propertyDescriptors = propertyDescriptors ;
178+ }
179+
180+ @ Override
181+ public PropertyDescriptor [] getPropertyDescriptors () {
182+ return propertyDescriptors ;
183+ }
184+ }
185+
186+ private static void introspectClass (final Class <?> clazz , final Map <String , PropertyDescriptor > properties ) {
187+ for ( final Method method : clazz .getDeclaredMethods () ) {
188+ final int modifiers = method .getModifiers ();
189+ // Skip static, non-public, bridge, and synthetic methods
190+ if ( Modifier .isStatic ( modifiers )
191+ || !Modifier .isPublic ( modifiers )
192+ || method .isBridge ()
193+ || method .isSynthetic () ) {
194+ continue ;
195+ }
196+
197+ final String methodName = method .getName ();
198+ final int paramCount = method .getParameterCount ();
199+ final Class <?> returnType = method .getReturnType ();
200+
201+ // Check for getter: getXxx() or isXxx()
202+ if ( paramCount == 0 && returnType != void .class ) {
203+ String propertyName = null ;
204+ if ( methodName .startsWith ( "get" ) && methodName .length () > 3 ) {
205+ propertyName = decapitalize ( methodName .substring ( 3 ) );
206+ }
207+ else if ( methodName .startsWith ( "is" ) && methodName .length () > 2
208+ && ( returnType == boolean .class || returnType == Boolean .class ) ) {
209+ propertyName = decapitalize ( methodName .substring ( 2 ) );
210+ }
211+
212+ if ( propertyName != null && !propertyName .isEmpty () ) {
213+ final PropertyDescriptor existing = properties .get ( propertyName );
214+ if ( existing == null ) {
215+ properties .put ( propertyName , new PropertyDescriptor ( propertyName , method , null ) );
216+ }
217+ else if ( existing .getReadMethod () == null ) {
218+ // Merge: we had a setter, now add the getter
219+ properties .put ( propertyName ,
220+ new PropertyDescriptor ( propertyName , method , existing .getWriteMethod () ) );
221+ }
222+ // else: subclass already defined a getter, keep it (subclass precedence)
223+ }
224+ }
225+
226+ // Check for setter: setXxx(value)
227+ // Note: if overloaded setters exist, we take the first one found.
228+ // This is acceptable for name-based property matching.
229+ if ( paramCount == 1 && methodName .startsWith ( "set" ) && methodName .length () > 3 ) {
230+ final String propertyName = decapitalize ( methodName .substring ( 3 ) );
231+ if ( !propertyName .isEmpty () ) {
232+ final PropertyDescriptor existing = properties .get ( propertyName );
233+ if ( existing == null ) {
234+ properties .put ( propertyName , new PropertyDescriptor ( propertyName , null , method ) );
235+ }
236+ else if ( existing .getWriteMethod () == null ) {
237+ // Merge: we had a getter, now add the setter
238+ properties .put ( propertyName ,
239+ new PropertyDescriptor ( propertyName , existing .getReadMethod (), method ) );
240+ }
241+ // else: subclass already defined a setter, keep it (subclass precedence)
242+ }
243+ }
244+ }
245+ }
102246}
0 commit comments