This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-beanutils.git


The following commit(s) were added to refs/heads/master by this push:
     new 80070f67 Clear descriptor caches when bean introspectors change (#435)
80070f67 is described below

commit 80070f6794923c96e46d274442aaa6d27233c213
Author: Naveed Khan <[email protected]>
AuthorDate: Sat Aug 22 20:39:51 2026 +0000

    Clear descriptor caches when bean introspectors change (#435)
    
    * clear descriptor caches when bean introspectors change
    
    the per-class descriptorsCache is populated once and was never invalidated 
when addBeanIntrospector, removeBeanIntrospector or resetBeanIntrospectors 
changed the introspector set, so a SuppressPropertiesBeanIntrospector 
registered after a class was introspected had no effect.
    
    * add tests for reset, mapped cache and sequential introspector changes
    
    cover resetBeanIntrospectors() re-evaluating a warmed cache, the 
mappedDescriptorsCache entry being re-created rather than served stale, and 
multiple sequential adds/removes each taking effect. all three fail without the 
cache invalidation in PropertyUtilsBean.
---
 .../commons/beanutils2/PropertyUtilsBean.java      | 17 +++-
 .../commons/beanutils2/PropertyUtilsTest.java      | 99 ++++++++++++++++++++++
 2 files changed, 115 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java 
b/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java
index 36c653f0..e9f98d5d 100644
--- a/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java
+++ b/src/main/java/org/apache/commons/beanutils2/PropertyUtilsBean.java
@@ -136,6 +136,7 @@ public class PropertyUtilsBean {
      */
     public void addBeanIntrospector(final BeanIntrospector introspector) {
         introspectors.add(Objects.requireNonNull(introspector, 
"introspector"));
+        clearDescriptorCaches();
     }
 
     /**
@@ -148,6 +149,15 @@ public class PropertyUtilsBean {
         Introspector.flushCaches();
     }
 
+    /**
+     * Discards the memoized introspection results after the registered {@link 
BeanIntrospector} set has changed. Unlike {@link #clearDescriptors()} this 
leaves
+     * the JVM-global {@link Introspector} cache untouched.
+     */
+    private void clearDescriptorCaches() {
+        descriptorsCache.clear();
+        mappedDescriptorsCache.clear();
+    }
+
     /**
      * <p>
      * Copy property values from the "origin" bean to the "destination" bean 
for all cases where the property names are the same (even though the actual 
getter
@@ -1222,7 +1232,11 @@ public class PropertyUtilsBean {
      * @since 1.9
      */
     public boolean removeBeanIntrospector(final BeanIntrospector introspector) 
{
-        return introspectors.remove(introspector);
+        final boolean removed = introspectors.remove(introspector);
+        if (removed) {
+            clearDescriptorCaches();
+        }
+        return removed;
     }
 
     /**
@@ -1236,6 +1250,7 @@ public class PropertyUtilsBean {
         introspectors.add(DefaultBeanIntrospector.INSTANCE);
         introspectors.add(SuppressPropertiesBeanIntrospector.SUPPRESS_CLASS);
         
introspectors.add(SuppressPropertiesBeanIntrospector.SUPPRESS_DECLARING_CLASS);
+        clearDescriptorCaches();
     }
 
     /**
diff --git a/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java 
b/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java
index 8e10a0ec..0d1753d1 100644
--- a/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java
+++ b/src/test/java/org/apache/commons/beanutils2/PropertyUtilsTest.java
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -346,6 +347,104 @@ class PropertyUtilsTest {
         assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), "A 
null suppressed entry must not hide unrelated properties");
     }
 
+    /**
+     * Registering a {@link SuppressPropertiesBeanIntrospector} must take 
effect for classes already introspected. The per-class descriptor cache was 
populated
+     * before the introspector was added and was never invalidated, so a 
property suppressed for hardening stayed readable and writable when its class 
had been
+     * introspected earlier (the common case with the shared {@code 
PropertyUtils} singleton).
+     */
+    @Test
+    void testAddBeanIntrospectorInvalidatesCache() throws Exception {
+        final PropertyUtilsBean pub = new PropertyUtilsBean();
+
+        // Warm the descriptor cache for TestBean before the suppression is 
configured.
+        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Property should be visible before suppression");
+
+        final SuppressPropertiesBeanIntrospector suppressor = new 
SuppressPropertiesBeanIntrospector(Arrays.asList("stringProperty"));
+        pub.addBeanIntrospector(suppressor);
+
+        assertNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Suppressed property should have no descriptor after the introspector is 
added");
+        assertThrows(NoSuchMethodException.class, () -> pub.getProperty(bean, 
"stringProperty"), "Suppressed property must not be readable");
+        assertThrows(NoSuchMethodException.class, () -> pub.setProperty(bean, 
"stringProperty", "changed"), "Suppressed property must not be writable");
+
+        // Removing the introspector must likewise invalidate the cache so the 
property becomes visible again.
+        pub.removeBeanIntrospector(suppressor);
+        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Property should be visible again after the introspector is removed");
+    }
+
+    /**
+     * {@code resetBeanIntrospectors()} must invalidate the descriptor caches 
so that introspection results produced by a previously registered custom
+     * introspector are re-evaluated against the restored default set.
+     */
+    @Test
+    void testResetBeanIntrospectorsInvalidatesCache() throws Exception {
+        final PropertyUtilsBean pub = new PropertyUtilsBean();
+        pub.addBeanIntrospector(new 
SuppressPropertiesBeanIntrospector(Arrays.asList("stringProperty")));
+
+        // Warm the descriptor cache with the suppression in effect.
+        assertNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Suppressed property should have no descriptor");
+
+        pub.resetBeanIntrospectors();
+
+        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"Property should be re-evaluated and visible after the reset");
+        assertEquals(bean.getStringProperty(), pub.getProperty(bean, 
"stringProperty"), "Property should be readable after the reset");
+    }
+
+    /**
+     * Changing the introspector set must also invalidate the mapped 
descriptor cache. The suppression guard in {@code getPropertyDescriptor} 
already hides a
+     * suppressed mapped property, so this checks the cached {@link 
MappedPropertyDescriptor} itself is dropped and re-created rather than served 
stale.
+     */
+    @Test
+    void testIntrospectorChangeInvalidatesMappedDescriptorCache() throws 
Exception {
+        final PropertyUtilsBean pub = new PropertyUtilsBean();
+
+        // Warm the mapped descriptor cache for TestBean.
+        final PropertyDescriptor first = pub.getPropertyDescriptor(bean, 
"mappedProperty");
+        assertNotNull(first, "Mapped property should be visible before 
suppression");
+
+        final SuppressPropertiesBeanIntrospector suppressor = new 
SuppressPropertiesBeanIntrospector(Arrays.asList("mappedProperty"));
+        pub.addBeanIntrospector(suppressor);
+
+        assertNull(pub.getPropertyDescriptor(bean, "mappedProperty"), 
"Suppressed mapped property should have no descriptor");
+        assertThrows(NoSuchMethodException.class, () -> pub.getProperty(bean, 
"mappedProperty(First Key)"), "Suppressed mapped property must not be 
readable");
+
+        pub.removeBeanIntrospector(suppressor);
+
+        final PropertyDescriptor second = pub.getPropertyDescriptor(bean, 
"mappedProperty");
+        assertNotNull(second, "Mapped property should be visible again after 
the introspector is removed");
+        assertNotSame(first, second, "Mapped descriptor must be re-created, 
not served from the stale cache");
+        assertEquals("First Value", pub.getProperty(bean, 
"mappedProperty(First Key)"), "Mapped property should be readable again");
+    }
+
+    /**
+     * Each add and remove in a sequence of introspector changes must 
invalidate the caches, so the visible property set always reflects the currently
+     * registered introspectors.
+     */
+    @Test
+    void testSequentialIntrospectorChangesInvalidateCache() throws Exception {
+        final PropertyUtilsBean pub = new PropertyUtilsBean();
+        final SuppressPropertiesBeanIntrospector suppressString = new 
SuppressPropertiesBeanIntrospector(Arrays.asList("stringProperty"));
+        final SuppressPropertiesBeanIntrospector suppressInt = new 
SuppressPropertiesBeanIntrospector(Arrays.asList("intProperty"));
+
+        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should be visible initially");
+        assertNotNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should be visible initially");
+
+        pub.addBeanIntrospector(suppressString);
+        assertNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should be suppressed after the first add");
+        assertNotNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should be unaffected by the first add");
+
+        pub.addBeanIntrospector(suppressInt);
+        assertNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should stay suppressed after the second add");
+        assertNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should be suppressed after the second add");
+
+        pub.removeBeanIntrospector(suppressString);
+        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should be visible again after its introspector is removed");
+        assertNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should stay suppressed after the unrelated remove");
+
+        pub.removeBeanIntrospector(suppressInt);
+        assertNotNull(pub.getPropertyDescriptor(bean, "stringProperty"), 
"stringProperty should stay visible after the last remove");
+        assertNotNull(pub.getPropertyDescriptor(bean, "intProperty"), 
"intProperty should be visible again after its introspector is removed");
+    }
+
     /**
      * Test the describe() method.
      */

Reply via email to