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

exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new f90656f980 NIFI-15388 Fixed Reference Leak in ReflectionUtils 
annotation cache (#10691)
f90656f980 is described below

commit f90656f9809cb0dbfe323aa5cab7369933931765
Author: Bob Paulin <[email protected]>
AuthorDate: Thu Jan 8 15:04:21 2026 -0600

    NIFI-15388 Fixed Reference Leak in ReflectionUtils annotation cache (#10691)
    
    - WeakHashMap does not work properly to remove keys and values due to 
strong reference to the Key found in the Cache Value
    - Keys should be programatically invalidated when the ClassLoader is closed 
to allow classes to be Garbage Collected
    - Moved ReflectionUtils to nifi-nar-utils so it can be shared by 
nifi-framework-nar-utils and nifi-framework-components
    
    Signed-off-by: David Handermann <[email protected]>
---
 .../apache/nifi/FlowRegistryClientInitializer.java |   2 +-
 .../nifi/init/ControllerServiceInitializer.java    |   1 +
 .../nifi/init/FlowAnalysisRuleInitializer.java     |   1 +
 .../nifi/init/ParameterProviderInitializer.java    |   1 +
 .../org/apache/nifi/init/ProcessorInitializer.java |   1 +
 .../java/org/apache/nifi/init/ReflectionUtils.java | 119 ---------------------
 .../apache/nifi/init/ReportingTaskInitializer.java |   1 +
 .../org/apache/nifi/nar/InstanceClassLoader.java   |   2 +
 .../java/org/apache/nifi/util/ReflectionUtils.java |  27 ++---
 9 files changed, 22 insertions(+), 133 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/FlowRegistryClientInitializer.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/FlowRegistryClientInitializer.java
index c9152badd2..1040eb7623 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/FlowRegistryClientInitializer.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/FlowRegistryClientInitializer.java
@@ -19,7 +19,6 @@ package org.apache.nifi;
 import org.apache.nifi.annotation.lifecycle.OnShutdown;
 import org.apache.nifi.components.ConfigurableComponent;
 import org.apache.nifi.init.ConfigurableComponentInitializer;
-import org.apache.nifi.init.ReflectionUtils;
 import org.apache.nifi.mock.MockComponentLogger;
 import org.apache.nifi.mock.MockConfigurationContext;
 import org.apache.nifi.mock.MockFlowRegistryClientInitializationContext;
@@ -28,6 +27,7 @@ import org.apache.nifi.nar.NarCloseable;
 import org.apache.nifi.registry.flow.FlowRegistryClient;
 import org.apache.nifi.registry.flow.FlowRegistryClientInitializationContext;
 import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.util.ReflectionUtils;
 
 /**
  * Initializes a FlowRegistryClient using a 
MockFlowRegistryClientInitializationContext;
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ControllerServiceInitializer.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ControllerServiceInitializer.java
index ebb225bb33..8b39e0d593 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ControllerServiceInitializer.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ControllerServiceInitializer.java
@@ -27,6 +27,7 @@ import 
org.apache.nifi.mock.MockControllerServiceInitializationContext;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.NarCloseable;
 import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.util.ReflectionUtils;
 
 /**
  * Initializes a ControllerService using a 
MockControllerServiceInitializationContext
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/FlowAnalysisRuleInitializer.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/FlowAnalysisRuleInitializer.java
index 7e17a983a0..fc2d5b8c21 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/FlowAnalysisRuleInitializer.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/FlowAnalysisRuleInitializer.java
@@ -26,6 +26,7 @@ import 
org.apache.nifi.mock.MockFlowAnalysisRuleInitializationContext;
 import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.NarCloseable;
 import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.util.ReflectionUtils;
 
 /**
  * Initializes a FlowAnalysisRule using a 
MockFlowAnalysisRuleInitializationContext;
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ParameterProviderInitializer.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ParameterProviderInitializer.java
index dea6174df0..ce9708b9a0 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ParameterProviderInitializer.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ParameterProviderInitializer.java
@@ -26,6 +26,7 @@ import org.apache.nifi.nar.NarCloseable;
 import org.apache.nifi.parameter.ParameterProvider;
 import org.apache.nifi.parameter.ParameterProviderInitializationContext;
 import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.util.ReflectionUtils;
 
 /**
  * Initializes a ParameterProvider using a MockReportingInitializationContext;
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ProcessorInitializer.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ProcessorInitializer.java
index 06b0fe4fd5..3c874aee96 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ProcessorInitializer.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ProcessorInitializer.java
@@ -26,6 +26,7 @@ import org.apache.nifi.nar.ExtensionManager;
 import org.apache.nifi.nar.NarCloseable;
 import org.apache.nifi.processor.Processor;
 import org.apache.nifi.processor.ProcessorInitializationContext;
+import org.apache.nifi.util.ReflectionUtils;
 
 /**
  * Initializes a Processor using a MockProcessorInitializationContext
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ReflectionUtils.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ReflectionUtils.java
deleted file mode 100644
index 96366e0530..0000000000
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ReflectionUtils.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.nifi.init;
-
-import org.apache.nifi.logging.ComponentLog;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.lang.annotation.Annotation;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-/**
- * This class is a copy of org.apache.nifi.util.ReflectionUtils. Ultimately the
- * documentation generation component should be moved to a place where it can
- * depend on this directly instead of copying it in.
- *
- *
- */
-public class ReflectionUtils {
-
-    private static final Logger LOG = 
LoggerFactory.getLogger(ReflectionUtils.class);
-
-    /**
-     * Invokes all methods on the given instance that have been annotated with
-     * the given annotation. If the signature of the method that is defined in
-     * <code>instance</code> uses 1 or more parameters, those parameters must 
be
-     * specified by the <code>args</code> parameter. However, if more arguments
-     * are supplied by the <code>args</code> parameter than needed, the extra
-     * arguments will be ignored.
-     *
-     * @param annotation annotation
-     * @param instance instance
-     * @param logger the ComponentLog to use for logging any errors. If null,
-     * will use own logger, but that will not generate bulletins or easily tie
-     * to the Processor's log messages.
-     * @param args args
-     * @return <code>true</code> if all appropriate methods were invoked and
-     * returned without throwing an Exception, <code>false</code> if one of the
-     * methods threw an Exception or could not be invoked; if 
<code>false</code>
-     * is returned, an error will have been logged.
-     */
-    public static boolean quietlyInvokeMethodsWithAnnotation(final Class<? 
extends Annotation> annotation, final Object instance, final ComponentLog 
logger, final Object... args) {
-
-        for (final Method method : instance.getClass().getMethods()) {
-            if (method.isAnnotationPresent(annotation)) {
-                method.setAccessible(true);
-                final Class<?>[] argumentTypes = method.getParameterTypes();
-                if (argumentTypes.length > args.length) {
-                    if (logger == null) {
-                        LOG.error("Unable to invoke method {} on {} because 
method expects {} parameters but only {} were given", method.getName(), 
instance, argumentTypes.length, args.length);
-                    } else {
-                        logger.error("Unable to invoke method {} on {} because 
method expects {} parameters but only {} were given", method.getName(), 
instance, argumentTypes.length, args.length);
-                    }
-
-                    return false;
-                }
-
-                for (int i = 0; i < argumentTypes.length; i++) {
-                    final Class<?> argType = argumentTypes[i];
-                    if (!argType.isAssignableFrom(args[i].getClass())) {
-                        if (logger == null) {
-                            LOG.error("Unable to invoke method {} on {} 
because method parameter {} is expected to be of type {} but argument passed 
was of type {}",
-                                    method.getName(), instance, i, argType, 
args[i].getClass());
-                        } else {
-                            logger.error("Unable to invoke method {} on {} 
because method parameter {} is expected to be of type {} but argument passed 
was of type {}",
-                                    method.getName(), instance, i, argType, 
args[i].getClass());
-                        }
-
-                        return false;
-                    }
-                }
-
-                try {
-                    if (argumentTypes.length == args.length) {
-                        method.invoke(instance, args);
-                    } else {
-                        final Object[] argsToPass = new 
Object[argumentTypes.length];
-                        System.arraycopy(args, 0, argsToPass, 0, 
argsToPass.length);
-
-                        method.invoke(instance, argsToPass);
-                    }
-                } catch (final InvocationTargetException ite) {
-                    if (logger == null) {
-                        LOG.error("Unable to invoke method {} on {}", 
method.getName(), instance, ite.getCause());
-                        LOG.error("", ite.getCause());
-                    } else {
-                        logger.error("Unable to invoke method {} on {}", 
method.getName(), instance, ite.getCause());
-                    }
-                } catch (final IllegalAccessException | 
IllegalArgumentException t) {
-                    if (logger == null) {
-                        LOG.error("Unable to invoke method {} on {}", 
method.getName(), instance, t);
-                        LOG.error("", t);
-                    } else {
-                        logger.error("Unable to invoke method {} on {}", 
method.getName(), instance, t);
-                    }
-
-                    return false;
-                }
-            }
-        }
-
-        return true;
-    }
-}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ReportingTaskInitializer.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ReportingTaskInitializer.java
index 848529363c..aa586bf9d1 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ReportingTaskInitializer.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/init/ReportingTaskInitializer.java
@@ -26,6 +26,7 @@ import org.apache.nifi.nar.NarCloseable;
 import org.apache.nifi.reporting.InitializationException;
 import org.apache.nifi.reporting.ReportingInitializationContext;
 import org.apache.nifi.reporting.ReportingTask;
+import org.apache.nifi.util.ReflectionUtils;
 
 /**
  * Initializes a ReportingTask using a MockReportingInitializationContext;
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/InstanceClassLoader.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/InstanceClassLoader.java
index 9d5430f7a3..f938724eb4 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/InstanceClassLoader.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-nar-utils/src/main/java/org/apache/nifi/nar/InstanceClassLoader.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.nar;
 
+import org.apache.nifi.util.ReflectionUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -79,6 +80,7 @@ public class InstanceClassLoader extends 
AbstractNativeLibHandlingClassLoader {
         if (parent instanceof SharedInstanceClassLoader) {
             ((SharedInstanceClassLoader) parent).close();
         }
+        ReflectionUtils.invalidateCacheForClassLoader(this);
     }
 
     /**
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/ReflectionUtils.java
 
b/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/main/java/org/apache/nifi/util/ReflectionUtils.java
similarity index 95%
rename from 
nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/ReflectionUtils.java
rename to 
nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/main/java/org/apache/nifi/util/ReflectionUtils.java
index d2f54af296..93ce739f27 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/util/ReflectionUtils.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/main/java/org/apache/nifi/util/ReflectionUtils.java
@@ -25,18 +25,19 @@ import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.TreeSet;
-import java.util.WeakHashMap;
 import java.util.concurrent.ConcurrentHashMap;
 
 public class ReflectionUtils {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(ReflectionUtils.class);
-    private static Map<Class<?>, Map<Annotations, List<Method>>> 
annotationCache = new WeakHashMap<>();
+    private static Map<Class<?>, Map<Annotations, List<Method>>> 
annotationCache = new ConcurrentHashMap<>();
 
     /**
      * Invokes all methods on the given instance that have been annotated with 
the given Annotation. If the signature of the method that is defined in 
<code>instance</code> uses 1 or more parameters,
@@ -166,13 +167,11 @@ public class ReflectionUtils {
         // This can add up to several seconds very easily.
         final Annotations annotations = new Annotations(annotationClasses);
 
-        synchronized (annotationCache) {
-            final Map<Annotations, List<Method>> innerMap = 
annotationCache.get(clazz);
-            if (innerMap != null) {
-                final List<Method> methods = innerMap.get(annotations);
-                if (methods != null) {
-                    return methods;
-                }
+        final Map<Annotations, List<Method>> innerMap = 
annotationCache.get(clazz);
+        if (innerMap != null) {
+            final List<Method> methods = innerMap.get(annotations);
+            if (methods != null) {
+                return methods;
             }
         }
 
@@ -180,14 +179,16 @@ public class ReflectionUtils {
         final List<Method> methods = discoverMethodsWithAnnotations(clazz, 
annotationClasses);
 
         // Store the discovered methods in our cache so that they are 
available next time.
-        synchronized (annotationCache) {
-            final Map<Annotations, List<Method>> innerMap = 
annotationCache.computeIfAbsent(clazz, key -> new ConcurrentHashMap<>());
-            innerMap.putIfAbsent(annotations, methods);
-        }
+        final Map<Annotations, List<Method>> discoveredInnerMap = 
annotationCache.computeIfAbsent(clazz, key -> new 
ConcurrentHashMap<>(Collections.singletonMap(annotations, methods)));
+        discoveredInnerMap.putIfAbsent(annotations, methods);
 
         return methods;
     }
 
+    public static void invalidateCacheForClassLoader(final ClassLoader 
classLoader) {
+        annotationCache.keySet().removeIf((clazz -> 
Objects.equals(clazz.getClassLoader(), classLoader)));
+    }
+
     private static List<Method> discoverMethodsWithAnnotations(final Class<?> 
clazz, final Class<? extends Annotation>[] annotations) {
         // Consider two methods equal if they have the same name and same 
parameter types.
         final Comparator<Method> comparator = 
Comparator.comparing(Method::getName).thenComparing(new Comparator<>() {

Reply via email to