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

bbende 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 0548338cb0a NIFI-16201: Reload Parameter-driven extra classpath for 
Connector Methods (#11574)
0548338cb0a is described below

commit 0548338cb0ac5f17559b8661d24f73b5c56c1553
Author: Mark Payne <[email protected]>
AuthorDate: Mon Aug 24 11:33:22 2026 -0400

    NIFI-16201: Reload Parameter-driven extra classpath for Connector Methods 
(#11574)
    
    * NIFI-16201: Reload Parameter-driven extra classpath for Connector Methods
    
    Stopped components keep a stale InstanceClassLoader after Parameter/asset 
classpath changes, so Connector Method discovery and invocation fail with 
LinkageError and can hang async verification.
    
    * NIFI-16201: Wrap ClasspathVerifyIT lines that exceeded Checkstyle length
    
    * NIFI-16201: Reload recursive referencers and wrap TypeNotPresentException
    
    Parameter updates can change a Controller Service isolation key, so stopped 
referencing components need the same recursive ClassLoader reload as 
setProperties(). Connector Method argument resolution can also throw 
TypeNotPresentException when a Class annotation member is missing from the 
component ClassLoader.
---
 .../nifi/controller/StandardProcessorNode.java     |   8 +-
 .../service/StandardControllerServiceNode.java     |   8 +-
 .../nifi/controller/AbstractComponentNode.java     |  54 ++--
 .../org/apache/nifi/controller/ProcessorNode.java  |   2 +-
 .../controller/service/ControllerServiceNode.java  |   2 +-
 .../StandaloneParameterContextFacade.java          |  14 +-
 .../StandaloneParameterContextFacadeTest.java      | 110 ++++++++
 .../web/api/concurrent/AsyncRequestManager.java    |   8 +-
 .../pom.xml                                        |  19 +-
 .../dynamicclasspath/DynamicallyLoadedType.java    |  21 ++
 .../tests/system/VerifyMethodSignatureService.java |  64 +++++
 .../org.apache.nifi.controller.ControllerService   |   1 +
 .../nifi-system-test-extensions/pom.xml            |  23 +-
 .../tests/system/ClasspathVerifyConnector.java     | 223 ++++++++++++++++
 .../system/MethodSignatureVerifyConnector.java     | 217 ++++++++++++++++
 .../tests/system/VerifyClasspathResource.java      | 125 +++++++++
 .../system/VerifyMethodSignatureResource.java      |  75 ++++++
 .../org.apache.nifi.components.connector.Connector |   4 +-
 .../services/org.apache.nifi.processor.Processor   |   2 +
 .../flows/method-signature-verify-connector.json   |  88 +++++++
 .../nifi-system-test-extensions-bundle/pom.xml     |   1 +
 nifi-system-tests/nifi-system-test-suite/pom.xml   |  13 +
 .../tests/system/connectors/ClasspathVerifyIT.java | 279 +++++++++++++++++++++
 23 files changed, 1319 insertions(+), 42 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
index 9fa0e0b4b2f..e24af1afea7 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/StandardProcessorNode.java
@@ -2023,7 +2023,7 @@ public class StandardProcessorNode extends ProcessorNode 
implements Connectable
     }
 
     @Override
-    public List<ConnectorMethod> getConnectorMethods() {
+    public List<ConnectorMethod> getConnectorMethods() throws 
InvocationFailedException {
         return getConnectorMethods(getProcessor().getClass());
     }
 
@@ -2082,6 +2082,12 @@ public class StandardProcessorNode extends ProcessorNode 
implements Connectable
             } catch (final Exception e) {
                 throw new InvocationFailedException(e);
             }
+        } catch (final TypeNotPresentException | LinkageError e) {
+            // MethodArgument.type() throws TypeNotPresentException when an 
annotation Class member is absent from the component ClassLoader.
+            // Resolving the implementation Method's parameter types can still 
throw LinkageError. Discovery already wraps LinkageError, and
+            // Errors thrown from the invoked method body are wrapped by 
Method.invoke as InvocationTargetException.
+            throw new InvocationFailedException("Failed to invoke Connector 
Method '" + methodName + "' on " + this
+                + " because a class required by the component could not be 
loaded from the component's ClassLoader", e);
         }
     }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
index 32c402dca95..b2010072027 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceNode.java
@@ -992,11 +992,17 @@ public class StandardControllerServiceNode extends 
AbstractComponentNode impleme
             } catch (final Exception e) {
                 throw new InvocationFailedException(e);
             }
+        } catch (final TypeNotPresentException | LinkageError e) {
+            // MethodArgument.type() throws TypeNotPresentException when an 
annotation Class member is absent from the component ClassLoader.
+            // Resolving the implementation Method's parameter types can still 
throw LinkageError. Discovery already wraps LinkageError, and
+            // Errors thrown from the invoked method body are wrapped by 
Method.invoke as InvocationTargetException.
+            throw new InvocationFailedException("Failed to invoke Connector 
Method '" + methodName + "' on " + this
+                + " because a class required by the component could not be 
loaded from the component's ClassLoader", e);
         }
     }
 
     @Override
-    public List<ConnectorMethod> getConnectorMethods() {
+    public List<ConnectorMethod> getConnectorMethods() throws 
InvocationFailedException {
         return 
getConnectorMethods(getControllerServiceImplementation().getClass());
     }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
index fd419477174..2e8ce97da57 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
@@ -32,6 +32,7 @@ import org.apache.nifi.components.ConfigurableComponent;
 import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.components.ValidationContext;
 import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.components.connector.InvocationFailedException;
 import org.apache.nifi.components.connector.components.ConnectorMethod;
 import org.apache.nifi.components.resource.ResourceContext;
 import org.apache.nifi.components.resource.ResourceReferenceFactory;
@@ -1561,30 +1562,47 @@ public abstract class AbstractComponentNode implements 
ComponentNode {
         }
     }
 
-    protected List<ConnectorMethod> getConnectorMethods(final Class<?> 
componentClass) {
-        final List<ConnectorMethod> connectorMethods = new ArrayList<>();
-        for (final Method method : componentClass.getDeclaredMethods()) {
-            final ConnectorMethod annotation = 
method.getAnnotation(ConnectorMethod.class);
-            connectorMethods.add(annotation);
-        }
+    protected List<ConnectorMethod> getConnectorMethods(final Class<?> 
componentClass) throws InvocationFailedException {
+        try {
+            final List<ConnectorMethod> connectorMethods = new ArrayList<>();
+            for (final Method method : componentClass.getDeclaredMethods()) {
+                final ConnectorMethod annotation = 
method.getAnnotation(ConnectorMethod.class);
+                if (annotation != null) {
+                    connectorMethods.add(annotation);
+                }
+            }
 
-        return connectorMethods;
+            final Class<?> superClass = componentClass.getSuperclass();
+            if (superClass != null && !Object.class.equals(superClass)) {
+                connectorMethods.addAll(getConnectorMethods(superClass));
+            }
+
+            return connectorMethods;
+        } catch (final LinkageError e) {
+            throw new InvocationFailedException("Failed to discover Connector 
Methods on " + componentClass.getName()
+                + " because a class required by the component could not be 
loaded from the component's ClassLoader", e);
+        }
     }
 
-    protected Method discoverConnectorMethod(final Class<?> componentClass, 
final String connectorMethodName) {
-        for (final Method method : componentClass.getDeclaredMethods()) {
-            final ConnectorMethod annotation = 
method.getAnnotation(ConnectorMethod.class);
-            if (annotation != null && 
annotation.name().equals(connectorMethodName)) {
-                return method;
+    protected Method discoverConnectorMethod(final Class<?> componentClass, 
final String connectorMethodName) throws InvocationFailedException {
+        try {
+            for (final Method method : componentClass.getDeclaredMethods()) {
+                final ConnectorMethod annotation = 
method.getAnnotation(ConnectorMethod.class);
+                if (annotation != null && 
annotation.name().equals(connectorMethodName)) {
+                    return method;
+                }
             }
-        }
 
-        final Class<?> superClass = componentClass.getSuperclass();
-        if (superClass != null && !Object.class.equals(superClass)) {
-            return discoverConnectorMethod(superClass, connectorMethodName);
-        }
+            final Class<?> superClass = componentClass.getSuperclass();
+            if (superClass != null && !Object.class.equals(superClass)) {
+                return discoverConnectorMethod(superClass, 
connectorMethodName);
+            }
 
-        return null;
+            return null;
+        } catch (final LinkageError e) {
+            throw new InvocationFailedException("Failed to discover Connector 
Method '" + connectorMethodName + "' on " + componentClass.getName()
+                + " because a class required by the component could not be 
loaded from the component's ClassLoader", e);
+        }
     }
 
     protected void setAdditionalResourcesFingerprint(String 
additionalResourcesFingerprint) {
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java
index e3db0b42de5..b72859f75ce 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/ProcessorNode.java
@@ -353,5 +353,5 @@ public abstract class ProcessorNode extends 
AbstractComponentNode implements Con
 
     public abstract String invokeConnectorMethod(String methodName, 
Map<String, String> jsonArguments, ProcessContext processContext) throws 
InvocationFailedException;
 
-    public abstract List<ConnectorMethod> getConnectorMethods();
+    public abstract List<ConnectorMethod> getConnectorMethods() throws 
InvocationFailedException;
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
index 0f1bd09535f..13dcb393234 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/service/ControllerServiceNode.java
@@ -278,5 +278,5 @@ public interface ControllerServiceNode extends 
ComponentNode, VersionedComponent
 
     String invokeConnectorMethod(String methodName, Map<String, String> 
jsonArguments, ConfigurationContext configurationContext) throws 
InvocationFailedException;
 
-    List<ConnectorMethod> getConnectorMethods();
+    List<ConnectorMethod> getConnectorMethods() throws 
InvocationFailedException;
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacade.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacade.java
index b4387c74503..b7f4d027072 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacade.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacade.java
@@ -20,6 +20,7 @@ package 
org.apache.nifi.components.connector.facades.standalone;
 import org.apache.nifi.asset.Asset;
 import org.apache.nifi.components.connector.components.ParameterContextFacade;
 import org.apache.nifi.components.connector.components.ParameterValue;
+import org.apache.nifi.controller.ComponentNode;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.ProcessorNode;
 import org.apache.nifi.controller.serialization.AffectedComponentSet;
@@ -95,8 +96,19 @@ public class StandaloneParameterContextFacade implements 
ParameterContextFacade
         final Map<String, Parameter> updatedParameters = 
createParameterMap(updatedValues);
         
managedProcessGroup.getParameterContext().setParameters(updatedParameters);
 
-        allReferencingProcessors.forEach(ProcessorNode::resetValidationState);
+        // A component whose additional classpath resources are supplied by a 
Parameter must be reloaded so that its ClassLoader reflects the
+        // updated Parameter values. Components that are not running are not 
restarted below, so reloading here is the only point at which their
+        // ClassLoader is rebuilt. Changing a Controller Service property can 
also change the Classloader Isolation Key of components that
+        // reference that service, matching 
StandardControllerServiceNode.setProperties().
+        for (final ControllerServiceNode serviceNode : allReferencingServices) 
{
+            serviceNode.reloadAdditionalResourcesIfNecessary();
+            
serviceNode.getReferences().findRecursiveReferences(ComponentNode.class).forEach(ComponentNode::reloadAdditionalResourcesIfNecessary);
+        }
+
+        
allReferencingProcessors.forEach(ProcessorNode::reloadAdditionalResourcesIfNecessary);
+
         
allReferencingServices.forEach(ControllerServiceNode::resetValidationState);
+        allReferencingProcessors.forEach(ProcessorNode::resetValidationState);
 
         logger.info("Parameter Context updated {} parameter. Restarting {} 
affected components.", updatedValues.size(), activeSet.getComponentCount());
         activeSet.start();
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacadeTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacadeTest.java
new file mode 100644
index 00000000000..919b414044a
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/test/java/org/apache/nifi/components/connector/facades/standalone/StandaloneParameterContextFacadeTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.components.connector.facades.standalone;
+
+import org.apache.nifi.components.connector.components.ParameterValue;
+import org.apache.nifi.controller.ComponentNode;
+import org.apache.nifi.controller.FlowController;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.controller.flow.FlowManager;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.controller.service.ControllerServiceReference;
+import org.apache.nifi.controller.service.ControllerServiceState;
+import org.apache.nifi.flow.ExecutionEngine;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Predicate;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class StandaloneParameterContextFacadeTest {
+
+    private static final String PARAMETER_NAME = "Classpath Resource";
+
+    @Test
+    public void 
testUpdateParametersReloadsStoppedProcessorsThatReferenceUpdatedServices() {
+        final FlowController flowController = mock(FlowController.class);
+        final FlowManager flowManager = mock(FlowManager.class);
+        final ControllerServiceProvider controllerServiceProvider = 
mock(ControllerServiceProvider.class);
+        when(flowController.getFlowManager()).thenReturn(flowManager);
+        
when(flowController.getControllerServiceProvider()).thenReturn(controllerServiceProvider);
+
+        final ParameterContext parameterContext = mock(ParameterContext.class);
+        final ParameterDescriptor parameterDescriptor = new 
ParameterDescriptor.Builder()
+                .name(PARAMETER_NAME)
+                .sensitive(false)
+                .build();
+        final Parameter existingParameter = new Parameter.Builder()
+                .descriptor(parameterDescriptor)
+                .value("old-asset")
+                .build();
+        
when(parameterContext.getParameter(PARAMETER_NAME)).thenReturn(Optional.of(existingParameter));
+
+        final ProcessorNode referencingProcessor = mock(ProcessorNode.class);
+        
when(referencingProcessor.getPhysicalScheduledState()).thenReturn(ScheduledState.STOPPED);
+        when(referencingProcessor.isRunning()).thenReturn(false);
+        
when(referencingProcessor.isReferencingParameter(PARAMETER_NAME)).thenReturn(false);
+
+        final ControllerServiceReference serviceReferences = 
mock(ControllerServiceReference.class);
+        
when(serviceReferences.findRecursiveReferences(ComponentNode.class)).thenReturn(List.of(referencingProcessor));
+
+        final ControllerServiceNode referencingService = 
mock(ControllerServiceNode.class);
+        
when(referencingService.isReferencingParameter(PARAMETER_NAME)).thenReturn(true);
+        
when(referencingService.getState()).thenReturn(ControllerServiceState.DISABLED);
+        when(referencingService.getReferences()).thenReturn(serviceReferences);
+
+        final ProcessGroup processGroup = mock(ProcessGroup.class);
+        when(processGroup.getParameterContext()).thenReturn(parameterContext);
+        
when(processGroup.getExecutionEngine()).thenReturn(ExecutionEngine.STANDARD);
+        
when(processGroup.referencesParameterContext(parameterContext)).thenReturn(true);
+        
when(processGroup.getControllerServices(false)).thenReturn(Set.of(referencingService));
+        when(processGroup.getProcessors()).thenReturn(Set.of());
+        when(processGroup.findAllProcessGroups(any())).thenAnswer(invocation 
-> {
+            final Predicate<ProcessGroup> predicate = 
invocation.getArgument(0);
+            if (predicate.test(processGroup)) {
+                return List.of(processGroup);
+            }
+            return List.of();
+        });
+        when(referencingProcessor.getProcessGroup()).thenReturn(processGroup);
+        when(referencingService.getProcessGroup()).thenReturn(processGroup);
+
+        final StandaloneParameterContextFacade facade = new 
StandaloneParameterContextFacade(flowController, processGroup);
+        final ParameterValue updatedParameter = new ParameterValue.Builder()
+                .name(PARAMETER_NAME)
+                .value("new-asset")
+                .sensitive(false)
+                .build();
+
+        facade.updateParameters(List.of(updatedParameter));
+
+        verify(referencingService).reloadAdditionalResourcesIfNecessary();
+        verify(referencingProcessor).reloadAdditionalResourcesIfNecessary();
+    }
+}
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
index 7d58dce0dc2..1eae970f6a3 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/concurrent/AsyncRequestManager.java
@@ -114,9 +114,11 @@ public class AsyncRequestManager<R, T> implements 
RequestManager<R, T> {
                 
SecurityContextHolder.getContext().setAuthentication(authentication);
 
                 task.accept(request);
-            } catch (final Exception e) {
-                logger.error("Failed to perform asynchronous task", e);
-                request.fail("Encountered unexpected error when performing 
asynchronous task: " + e);
+            } catch (final Throwable t) {
+                // Any Throwable must mark the request as failed. A Throwable 
that escapes this Runnable is captured by the Future returned from
+                // submit() and is never surfaced, which would leave the 
request incomplete and cause clients to poll it indefinitely.
+                logger.error("Failed to perform asynchronous task", t);
+                request.fail("Encountered unexpected error when performing 
asynchronous task: " + t);
             } finally {
                 // clear the authentication token
                 SecurityContextHolder.getContext().setAuthentication(null);
diff --git a/nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-dynamic-classpath/pom.xml
similarity index 59%
copy from nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml
copy to 
nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-dynamic-classpath/pom.xml
index bd7d295f8cf..9f3f529c3bf 100644
--- a/nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-dynamic-classpath/pom.xml
@@ -13,23 +13,14 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
-<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
     <parent>
-        <artifactId>nifi-system-tests</artifactId>
         <groupId>org.apache.nifi</groupId>
+        <artifactId>nifi-system-test-extensions-bundle</artifactId>
         <version>2.12.0-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
-    <artifactId>nifi-system-test-extensions-bundle</artifactId>
-    <packaging>pom</packaging>
-
-    <modules>
-        <module>nifi-system-test-extensions</module>
-        <module>nifi-system-test-extensions-nar</module>
-        <module>nifi-system-test-extensions-services</module>
-        <module>nifi-system-test-extensions-services-api</module>
-        <module>nifi-system-test-extensions-services-nar</module>
-        <module>nifi-system-test-extensions-services-api-nar</module>
-    </modules>
-</project>
\ No newline at end of file
+    <artifactId>nifi-system-test-dynamic-classpath</artifactId>
+</project>
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-dynamic-classpath/src/main/java/org/apache/nifi/tests/system/dynamicclasspath/DynamicallyLoadedType.java
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-dynamic-classpath/src/main/java/org/apache/nifi/tests/system/dynamicclasspath/DynamicallyLoadedType.java
new file mode 100644
index 00000000000..6206fe46591
--- /dev/null
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-dynamic-classpath/src/main/java/org/apache/nifi/tests/system/dynamicclasspath/DynamicallyLoadedType.java
@@ -0,0 +1,21 @@
+/*
+ * 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.tests.system.dynamicclasspath;
+
+public class DynamicallyLoadedType {
+}
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/java/org/apache/nifi/cs/tests/system/VerifyMethodSignatureService.java
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/java/org/apache/nifi/cs/tests/system/VerifyMethodSignatureService.java
new file mode 100644
index 00000000000..ad2566025f4
--- /dev/null
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/java/org/apache/nifi/cs/tests/system/VerifyMethodSignatureService.java
@@ -0,0 +1,64 @@
+/*
+ * 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.cs.tests.system;
+
+import org.apache.nifi.annotation.behavior.RequiresInstanceClassLoading;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.connector.components.ComponentState;
+import org.apache.nifi.components.connector.components.ConnectorMethod;
+import org.apache.nifi.components.connector.components.MethodArgument;
+import org.apache.nifi.components.resource.ResourceCardinality;
+import org.apache.nifi.components.resource.ResourceType;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.util.List;
+
+/**
+ * Controller Service used to verify that additional classpath resources are 
available during {@code @ConnectorMethod}
+ * invocation after the classpath resource is set as a Controller Service 
property.
+ */
+@RequiresInstanceClassLoading
+public class VerifyMethodSignatureService extends AbstractControllerService {
+
+    public static final PropertyDescriptor CLASSPATH_RESOURCE = new 
PropertyDescriptor.Builder()
+            .name("Classpath Resource")
+            .description("An external resource to add to the Controller 
Service classpath")
+            .required(false)
+            .dynamicallyModifiesClasspath(true)
+            .identifiesExternalResource(ResourceCardinality.SINGLE, 
ResourceType.FILE)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return List.of(CLASSPATH_RESOURCE);
+    }
+
+    @ConnectorMethod(
+            name = "loadClass",
+            description = "Attempts to load the given class from the 
Controller Service classpath",
+            allowedStates = {ComponentState.STOPPED},
+            arguments = {
+                    @MethodArgument(name = "className", type = String.class, 
description = "Fully-qualified class name to load", required = true)
+            }
+    )
+    public String loadClass(final String className) throws 
ClassNotFoundException {
+        final Class<?> clazz = Class.forName(className);
+        return clazz.getName();
+    }
+}
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
index 99b5498bfe4..3e376b35485 100644
--- 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions-services/src/main/resources/META-INF/services/org.apache.nifi.controller.ControllerService
@@ -26,3 +26,4 @@ org.apache.nifi.cs.tests.system.ModifyClasspathService
 org.apache.nifi.cs.tests.system.MockCSVReader
 org.apache.nifi.cs.tests.system.MockCSVWriter
 org.apache.nifi.cs.tests.system.VerifyLocalClusterStateService
+org.apache.nifi.cs.tests.system.VerifyMethodSignatureService
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/pom.xml
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/pom.xml
index 9ce5354f363..0c93f0d6658 100644
--- 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/pom.xml
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/pom.xml
@@ -24,6 +24,12 @@
     <artifactId>nifi-system-test-extensions</artifactId>
 
     <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-system-test-dynamic-classpath</artifactId>
+            <version>2.12.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-stateless-api</artifactId>
@@ -55,4 +61,19 @@
             <scope>provided</scope>
         </dependency>
     </dependencies>
-</project>
\ No newline at end of file
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes combine.children="append">
+                        <!-- Connector flow definition; JSON cannot carry an 
Apache license header -->
+                        
<exclude>src/main/resources/flows/method-signature-verify-connector.json</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/ClasspathVerifyConnector.java
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/ClasspathVerifyConnector.java
new file mode 100644
index 00000000000..2c7fe849803
--- /dev/null
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/ClasspathVerifyConnector.java
@@ -0,0 +1,223 @@
+/*
+ * 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.connectors.tests.system;
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.connector.AbstractConnector;
+import org.apache.nifi.components.connector.BundleCompatibility;
+import org.apache.nifi.components.connector.ConfigurationStep;
+import org.apache.nifi.components.connector.ConnectorConfigurationContext;
+import org.apache.nifi.components.connector.ConnectorPropertyDescriptor;
+import org.apache.nifi.components.connector.ConnectorPropertyGroup;
+import org.apache.nifi.components.connector.FlowUpdateException;
+import org.apache.nifi.components.connector.InvocationFailedException;
+import org.apache.nifi.components.connector.PropertyType;
+import org.apache.nifi.components.connector.components.FlowContext;
+import org.apache.nifi.components.connector.components.ProcessorFacade;
+import org.apache.nifi.components.connector.util.VersionedFlowUtils;
+import org.apache.nifi.flow.Bundle;
+import org.apache.nifi.flow.Position;
+import org.apache.nifi.flow.VersionedExternalFlow;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.processors.tests.system.VerifyClasspathResource;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test connector that exercises additional classpath loading during 
configuration verification.
+ * Supports processor verification with property overrides, class loading 
through a Connector Method,
+ * and consecutive Connector Method invocations that verify component instance 
state is preserved.
+ */
+public class ClasspathVerifyConnector extends AbstractConnector {
+
+    public static final String STEP_NAME = "Classpath Configuration";
+    public static final String STRATEGY_PROCESSOR_VERIFY = "Processor Verify";
+    public static final String STRATEGY_CONNECTOR_METHOD = "Connector Method";
+    public static final String STRATEGY_CONNECTOR_METHOD_STATE = "Connector 
Method State";
+    public static final String CONNECTOR_METHOD_STEP = "Invoke loadClass 
Connector Method";
+    public static final String CONNECTOR_METHOD_STATE_STEP = "Preserve 
Connector Method Component State";
+
+    public static final ConnectorPropertyDescriptor CLASSPATH_RESOURCE = new 
ConnectorPropertyDescriptor.Builder()
+            .name("Classpath Resource")
+            .description("An asset JAR to place on the processor classpath")
+            .required(true)
+            .type(PropertyType.ASSET)
+            .build();
+
+    public static final ConnectorPropertyDescriptor CLASS_TO_LOAD = new 
ConnectorPropertyDescriptor.Builder()
+            .name("Class to Load")
+            .description("Fully-qualified class name that must be loadable 
from the classpath resource")
+            .required(true)
+            .type(PropertyType.STRING)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
+
+    public static final ConnectorPropertyDescriptor VERIFICATION_STRATEGY = 
new ConnectorPropertyDescriptor.Builder()
+            .name("Verification Strategy")
+            .description("Whether verification should call 
ProcessorFacade.verify, invoke the loadClass Connector Method, or verify 
Connector Method component state")
+            .required(true)
+            .type(PropertyType.STRING)
+            .defaultValue(STRATEGY_PROCESSOR_VERIFY)
+            .allowableValues(STRATEGY_PROCESSOR_VERIFY, 
STRATEGY_CONNECTOR_METHOD, STRATEGY_CONNECTOR_METHOD_STATE)
+            .build();
+
+    private static final String PROCESSOR_TYPE = 
"org.apache.nifi.processors.tests.system.VerifyClasspathResource";
+    private static final String PROCESSOR_NAME = "Verify Classpath Resource";
+    private static final Bundle SYSTEM_TEST_EXTENSIONS_BUNDLE = new 
Bundle("org.apache.nifi", "nifi-system-test-extensions-nar", "2.8.0-SNAPSHOT");
+
+    private static final ConnectorPropertyGroup PROPERTY_GROUP = new 
ConnectorPropertyGroup.Builder()
+            .name("Classpath Settings")
+            .description("Classpath resource and verification strategy")
+            .properties(List.of(CLASSPATH_RESOURCE, CLASS_TO_LOAD, 
VERIFICATION_STRATEGY))
+            .build();
+
+    private static final ConfigurationStep CONFIGURATION_STEP = new 
ConfigurationStep.Builder()
+            .name(STEP_NAME)
+            .description("Configure classpath verification")
+            .propertyGroups(List.of(PROPERTY_GROUP))
+            .build();
+
+    @Override
+    public List<ConfigurationStep> getConfigurationSteps() {
+        return List.of(CONFIGURATION_STEP);
+    }
+
+    @Override
+    public VersionedExternalFlow getInitialFlow() {
+        return buildFlow(null, null);
+    }
+
+    @Override
+    public VersionedExternalFlow getActiveFlow(final FlowContext 
activeFlowContext) {
+        final ConnectorConfigurationContext configurationContext = 
activeFlowContext.getConfigurationContext();
+        final String classpathResource = 
configurationContext.getProperty(STEP_NAME, 
CLASSPATH_RESOURCE.getName()).getValue();
+        final String classToLoad = configurationContext.getProperty(STEP_NAME, 
CLASS_TO_LOAD.getName()).getValue();
+        return buildFlow(classpathResource, classToLoad);
+    }
+
+    @Override
+    protected void onStepConfigured(final String stepName, final FlowContext 
workingContext) throws FlowUpdateException {
+        final ConnectorConfigurationContext configurationContext = 
workingContext.getConfigurationContext();
+        final String classpathResource = 
configurationContext.getProperty(STEP_NAME, 
CLASSPATH_RESOURCE.getName()).getValue();
+        final String classToLoad = configurationContext.getProperty(STEP_NAME, 
CLASS_TO_LOAD.getName()).getValue();
+        getInitializationContext().updateFlow(workingContext, 
buildFlow(classpathResource, classToLoad), BundleCompatibility.RESOLVE_BUNDLE);
+    }
+
+    @Override
+    public void applyUpdate(final FlowContext workingContext, final 
FlowContext activeContext) throws FlowUpdateException {
+        final ConnectorConfigurationContext configurationContext = 
workingContext.getConfigurationContext();
+        final String classpathResource = 
configurationContext.getProperty(STEP_NAME, 
CLASSPATH_RESOURCE.getName()).getValue();
+        final String classToLoad = configurationContext.getProperty(STEP_NAME, 
CLASS_TO_LOAD.getName()).getValue();
+        getInitializationContext().updateFlow(activeContext, 
buildFlow(classpathResource, classToLoad), BundleCompatibility.RESOLVE_BUNDLE);
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verifyConfigurationStep(final String 
stepName, final Map<String, String> propertyValueOverrides, final FlowContext 
flowContext) {
+        final ConnectorConfigurationContext configurationContext = 
flowContext.getConfigurationContext().createWithOverrides(stepName, 
propertyValueOverrides);
+        final String classpathResource = 
configurationContext.getProperty(STEP_NAME, 
CLASSPATH_RESOURCE.getName()).getValue();
+        final String classToLoad = configurationContext.getProperty(STEP_NAME, 
CLASS_TO_LOAD.getName()).getValue();
+        final String strategy = configurationContext.getProperty(STEP_NAME, 
VERIFICATION_STRATEGY.getName()).getValue();
+
+        final ProcessorFacade processorFacade = 
flowContext.getRootGroup().getProcessors().stream()
+                .filter(processor -> 
processor.getDefinition().getType().endsWith("VerifyClasspathResource"))
+                .findFirst()
+                .orElseThrow(() -> new 
IllegalStateException("VerifyClasspathResource processor not found in flow"));
+
+        if (STRATEGY_CONNECTOR_METHOD.equals(strategy)) {
+            return invokeLoadClassMethod(processorFacade, classToLoad);
+        }
+
+        if (STRATEGY_CONNECTOR_METHOD_STATE.equals(strategy)) {
+            return verifyConnectorMethodState(processorFacade);
+        }
+
+        return verifyWithPropertyOverrides(processorFacade, classpathResource, 
classToLoad);
+    }
+
+    private List<ConfigVerificationResult> verifyWithPropertyOverrides(final 
ProcessorFacade processorFacade, final String classpathResource, final String 
classToLoad) {
+        final Map<String, String> propertyOverrides = new HashMap<>();
+        if (classpathResource != null) {
+            
propertyOverrides.put(VerifyClasspathResource.CLASSPATH_RESOURCE.getName(), 
classpathResource);
+        }
+
+        if (classToLoad != null) {
+            
propertyOverrides.put(VerifyClasspathResource.CLASS_TO_LOAD.getName(), 
classToLoad);
+        }
+
+        return processorFacade.verify(propertyOverrides, Map.of());
+    }
+
+    private List<ConfigVerificationResult> invokeLoadClassMethod(final 
ProcessorFacade processorFacade, final String classToLoad) {
+        try {
+            final String loadedClass = 
processorFacade.invokeConnectorMethod("loadClass", Map.of("className", 
classToLoad), String.class);
+            return List.of(new ConfigVerificationResult.Builder()
+                    .verificationStepName(CONNECTOR_METHOD_STEP)
+                    .outcome(Outcome.SUCCESSFUL)
+                    .explanation("Successfully loaded class " + loadedClass + 
" via ConnectorMethod")
+                    .build());
+        } catch (final InvocationFailedException e) {
+            return List.of(new ConfigVerificationResult.Builder()
+                    .verificationStepName(CONNECTOR_METHOD_STEP)
+                    .outcome(Outcome.FAILED)
+                    .explanation("Failed to load class " + classToLoad + " via 
ConnectorMethod: " + e.getMessage())
+                    .build());
+        }
+    }
+
+    private List<ConfigVerificationResult> verifyConnectorMethodState(final 
ProcessorFacade processorFacade) {
+        try {
+            final Integer firstInvocationCount = 
processorFacade.invokeConnectorMethod("incrementInvocationCount", Map.of(), 
Integer.class);
+            final Integer secondInvocationCount = 
processorFacade.invokeConnectorMethod("incrementInvocationCount", Map.of(), 
Integer.class);
+            final boolean statePreserved = firstInvocationCount == 1 && 
secondInvocationCount == 2;
+            return List.of(new ConfigVerificationResult.Builder()
+                    .verificationStepName(CONNECTOR_METHOD_STATE_STEP)
+                    .outcome(statePreserved ? Outcome.SUCCESSFUL : 
Outcome.FAILED)
+                    .explanation("Connector Method invocation counts were " + 
firstInvocationCount + " and " + secondInvocationCount)
+                    .build());
+        } catch (final InvocationFailedException e) {
+            return List.of(new ConfigVerificationResult.Builder()
+                    .verificationStepName(CONNECTOR_METHOD_STATE_STEP)
+                    .outcome(Outcome.FAILED)
+                    .explanation("Failed to invoke stateful Connector Method: 
" + e.getMessage())
+                    .build());
+        }
+    }
+
+    private VersionedExternalFlow buildFlow(final String classpathResource, 
final String classToLoad) {
+        final VersionedProcessGroup group = 
VersionedFlowUtils.createProcessGroup("classpath-verify-flow-id", "Classpath 
Verify Flow");
+        final VersionedProcessor processor = 
VersionedFlowUtils.addProcessor(group, PROCESSOR_TYPE, 
SYSTEM_TEST_EXTENSIONS_BUNDLE, PROCESSOR_NAME, new Position(0, 0));
+
+        if (classpathResource != null) {
+            
processor.getProperties().put(VerifyClasspathResource.CLASSPATH_RESOURCE.getName(),
 classpathResource);
+        }
+
+        if (classToLoad != null) {
+            
processor.getProperties().put(VerifyClasspathResource.CLASS_TO_LOAD.getName(), 
classToLoad);
+        }
+
+        final VersionedExternalFlow flow = new VersionedExternalFlow();
+        flow.setFlowContents(group);
+        flow.setParameterContexts(Map.of());
+        return flow;
+    }
+}
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/MethodSignatureVerifyConnector.java
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/MethodSignatureVerifyConnector.java
new file mode 100644
index 00000000000..02b416fb093
--- /dev/null
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/connectors/tests/system/MethodSignatureVerifyConnector.java
@@ -0,0 +1,217 @@
+/*
+ * 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.connectors.tests.system;
+
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.connector.AbstractConnector;
+import org.apache.nifi.components.connector.BundleCompatibility;
+import org.apache.nifi.components.connector.ConfigurationStep;
+import org.apache.nifi.components.connector.ConnectorConfigurationContext;
+import org.apache.nifi.components.connector.ConnectorPropertyDescriptor;
+import org.apache.nifi.components.connector.ConnectorPropertyGroup;
+import org.apache.nifi.components.connector.FlowUpdateException;
+import org.apache.nifi.components.connector.InvocationFailedException;
+import org.apache.nifi.components.connector.PropertyType;
+import org.apache.nifi.components.connector.components.ControllerServiceFacade;
+import org.apache.nifi.components.connector.components.FlowContext;
+import org.apache.nifi.components.connector.components.ProcessorFacade;
+import org.apache.nifi.components.connector.util.VersionedFlowUtils;
+import org.apache.nifi.flow.Bundle;
+import org.apache.nifi.flow.Position;
+import org.apache.nifi.flow.VersionedControllerService;
+import org.apache.nifi.flow.VersionedExternalFlow;
+import org.apache.nifi.flow.VersionedProcessGroup;
+import org.apache.nifi.flow.VersionedProcessor;
+import org.apache.nifi.processors.tests.system.VerifyMethodSignatureResource;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test connector that exercises additional-classpath method discovery. The 
Working flow contains either a
+ * processor or a Controller Service whose declared methods include a private 
method returning a type available only
+ * through the configured asset. Invoking the unrelated, constant-returning 
{@code returnConstant} Connector Method
+ * forces the framework to resolve every declared method signature, so the 
asset must be present on the component
+ * classpath even though the invoked method itself does not reference the 
dynamically loaded type.
+ */
+public class MethodSignatureVerifyConnector extends AbstractConnector {
+
+    public static final String STEP_NAME = "Method Signature Configuration";
+    public static final String APPLICATION_PARAMETER = "Parameter";
+    public static final String APPLICATION_PROCESSOR_PROPERTY = "Processor 
Property";
+    public static final String APPLICATION_CONTROLLER_SERVICE_PROPERTY = 
"Controller Service Property";
+    public static final String METHOD_SIGNATURE_STEP = "Discover Connector 
Method With Dynamic Signature";
+
+    public static final ConnectorPropertyDescriptor CLASSPATH_RESOURCE = new 
ConnectorPropertyDescriptor.Builder()
+            .name("Classpath Resource")
+            .description("An asset JAR to place on the component classpath")
+            .required(true)
+            .type(PropertyType.ASSET)
+            .build();
+
+    public static final ConnectorPropertyDescriptor CLASSPATH_APPLICATION = 
new ConnectorPropertyDescriptor.Builder()
+            .name("Classpath Application")
+            .description("Whether the classpath asset is applied through a 
Parameter, a Processor property, or a Controller Service property")
+            .required(true)
+            .type(PropertyType.STRING)
+            .defaultValue(APPLICATION_PARAMETER)
+            .allowableValues(APPLICATION_PARAMETER, 
APPLICATION_PROCESSOR_PROPERTY, APPLICATION_CONTROLLER_SERVICE_PROPERTY)
+            .build();
+
+    private static final String FLOW_JSON_PATH = 
"flows/method-signature-verify-connector.json";
+    private static final String CLASSPATH_PARAMETER_NAME = "Classpath 
Resource";
+    private static final String PROCESSOR_TYPE = 
"org.apache.nifi.processors.tests.system.VerifyMethodSignatureResource";
+    private static final String PROCESSOR_NAME = "Verify Method Signature 
Resource";
+    private static final String CONTROLLER_SERVICE_TYPE = 
"org.apache.nifi.cs.tests.system.VerifyMethodSignatureService";
+    private static final String CONTROLLER_SERVICE_NAME = "Verify Method 
Signature Service";
+    private static final String DYNAMIC_CLASSPATH_CLASS = 
"org.apache.nifi.tests.system.dynamicclasspath.DynamicallyLoadedType";
+    private static final Bundle SYSTEM_TEST_EXTENSIONS_BUNDLE = new 
Bundle("org.apache.nifi", "nifi-system-test-extensions-nar", "2.8.0-SNAPSHOT");
+    private static final Bundle SYSTEM_TEST_EXTENSIONS_SERVICES_BUNDLE = new 
Bundle("org.apache.nifi", "nifi-system-test-extensions-services-nar", 
"2.8.0-SNAPSHOT");
+
+    private static final ConnectorPropertyGroup PROPERTY_GROUP = new 
ConnectorPropertyGroup.Builder()
+            .name("Classpath Settings")
+            .description("Classpath resource providing the dynamically loaded 
method-signature type")
+            .properties(List.of(CLASSPATH_RESOURCE, CLASSPATH_APPLICATION))
+            .build();
+
+    private static final ConfigurationStep CONFIGURATION_STEP = new 
ConfigurationStep.Builder()
+            .name(STEP_NAME)
+            .description("Configure the classpath resource used for 
method-signature discovery")
+            .propertyGroups(List.of(PROPERTY_GROUP))
+            .build();
+
+    @Override
+    public List<ConfigurationStep> getConfigurationSteps() {
+        return List.of(CONFIGURATION_STEP);
+    }
+
+    @Override
+    public VersionedExternalFlow getInitialFlow() {
+        return buildFlow(null, APPLICATION_PARAMETER);
+    }
+
+    @Override
+    public VersionedExternalFlow getActiveFlow(final FlowContext 
activeFlowContext) {
+        return buildFlowFromContext(activeFlowContext);
+    }
+
+    @Override
+    protected void onStepConfigured(final String stepName, final FlowContext 
workingContext) throws FlowUpdateException {
+        getInitializationContext().updateFlow(workingContext, 
buildFlowFromContext(workingContext), BundleCompatibility.RESOLVE_BUNDLE);
+    }
+
+    @Override
+    public void applyUpdate(final FlowContext workingContext, final 
FlowContext activeContext) throws FlowUpdateException {
+        getInitializationContext().updateFlow(activeContext, 
buildFlowFromContext(workingContext), BundleCompatibility.RESOLVE_BUNDLE);
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verifyConfigurationStep(final String 
stepName, final Map<String, String> propertyValueOverrides, final FlowContext 
flowContext) {
+        final ConnectorConfigurationContext configurationContext = 
flowContext.getConfigurationContext().createWithOverrides(stepName, 
propertyValueOverrides);
+        final String classpathApplication = 
configurationContext.getProperty(STEP_NAME, 
CLASSPATH_APPLICATION.getName()).getValue();
+
+        try {
+            final String result;
+            if 
(APPLICATION_CONTROLLER_SERVICE_PROPERTY.equals(classpathApplication)) {
+                result = 
findControllerService(flowContext).invokeConnectorMethod("loadClass", 
Map.of("className", DYNAMIC_CLASSPATH_CLASS), String.class);
+            } else {
+                result = 
findProcessor(flowContext).invokeConnectorMethod("returnConstant", Map.of(), 
String.class);
+            }
+
+            return List.of(new ConfigVerificationResult.Builder()
+                    .verificationStepName(METHOD_SIGNATURE_STEP)
+                    .outcome(Outcome.SUCCESSFUL)
+                    .explanation("Successfully invoked ConnectorMethod 
returning " + result)
+                    .build());
+        } catch (final InvocationFailedException e) {
+            return List.of(new ConfigVerificationResult.Builder()
+                    .verificationStepName(METHOD_SIGNATURE_STEP)
+                    .outcome(Outcome.FAILED)
+                    .explanation("Failed to discover ConnectorMethod because a 
declared method signature type was unavailable: " + e)
+                    .build());
+        }
+    }
+
+    private VersionedExternalFlow buildFlowFromContext(final FlowContext 
flowContext) {
+        final ConnectorConfigurationContext configurationContext = 
flowContext.getConfigurationContext();
+        final String classpathResource = 
configurationContext.getProperty(STEP_NAME, 
CLASSPATH_RESOURCE.getName()).getValue();
+        final String classpathApplication = 
configurationContext.getProperty(STEP_NAME, 
CLASSPATH_APPLICATION.getName()).getValue();
+        return buildFlow(classpathResource, classpathApplication);
+    }
+
+    private VersionedExternalFlow buildFlow(final String classpathResource, 
final String classpathApplication) {
+        if (APPLICATION_PROCESSOR_PROPERTY.equals(classpathApplication)) {
+            return buildProcessorPropertyFlow(classpathResource);
+        }
+
+        if 
(APPLICATION_CONTROLLER_SERVICE_PROPERTY.equals(classpathApplication)) {
+            return buildControllerServicePropertyFlow(classpathResource);
+        }
+
+        return buildParameterFlow(classpathResource);
+    }
+
+    private VersionedExternalFlow buildParameterFlow(final String 
classpathResource) {
+        final VersionedExternalFlow flow = 
VersionedFlowUtils.loadFlowFromResource(FLOW_JSON_PATH);
+        if (classpathResource != null) {
+            VersionedFlowUtils.setParameterValues(flow, 
Map.of(CLASSPATH_PARAMETER_NAME, classpathResource));
+        }
+
+        return flow;
+    }
+
+    private VersionedExternalFlow buildProcessorPropertyFlow(final String 
classpathResource) {
+        final VersionedProcessGroup group = 
VersionedFlowUtils.createProcessGroup("method-signature-verify-flow-id", 
"Method Signature Verify Flow");
+        final VersionedProcessor processor = 
VersionedFlowUtils.addProcessor(group, PROCESSOR_TYPE, 
SYSTEM_TEST_EXTENSIONS_BUNDLE, PROCESSOR_NAME, new Position(0, 0));
+        if (classpathResource != null) {
+            
processor.getProperties().put(VerifyMethodSignatureResource.CLASSPATH_RESOURCE.getName(),
 classpathResource);
+        }
+
+        final VersionedExternalFlow flow = new VersionedExternalFlow();
+        flow.setFlowContents(group);
+        flow.setParameterContexts(Map.of());
+        return flow;
+    }
+
+    private VersionedExternalFlow buildControllerServicePropertyFlow(final 
String classpathResource) {
+        final VersionedProcessGroup group = 
VersionedFlowUtils.createProcessGroup("method-signature-verify-flow-id", 
"Method Signature Verify Flow");
+        final VersionedControllerService controllerService = 
VersionedFlowUtils.addControllerService(group, CONTROLLER_SERVICE_TYPE, 
SYSTEM_TEST_EXTENSIONS_SERVICES_BUNDLE, CONTROLLER_SERVICE_NAME);
+        if (classpathResource != null) {
+            controllerService.getProperties().put(CLASSPATH_PARAMETER_NAME, 
classpathResource);
+        }
+
+        final VersionedExternalFlow flow = new VersionedExternalFlow();
+        flow.setFlowContents(group);
+        flow.setParameterContexts(Map.of());
+        return flow;
+    }
+
+    private ProcessorFacade findProcessor(final FlowContext flowContext) {
+        return flowContext.getRootGroup().getProcessors().stream()
+                .filter(processor -> 
processor.getDefinition().getType().endsWith("VerifyMethodSignatureResource"))
+                .findFirst()
+                .orElseThrow(() -> new 
IllegalStateException("VerifyMethodSignatureResource processor not found in 
flow"));
+    }
+
+    private ControllerServiceFacade findControllerService(final FlowContext 
flowContext) {
+        return flowContext.getRootGroup().getControllerServices().stream()
+                .filter(controllerService -> 
controllerService.getDefinition().getType().endsWith("VerifyMethodSignatureService"))
+                .findFirst()
+                .orElseThrow(() -> new 
IllegalStateException("VerifyMethodSignatureService controller service not 
found in flow"));
+    }
+}
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyClasspathResource.java
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyClasspathResource.java
new file mode 100644
index 00000000000..4bf7bf06b61
--- /dev/null
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyClasspathResource.java
@@ -0,0 +1,125 @@
+/*
+ * 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.processors.tests.system;
+
+import org.apache.nifi.annotation.behavior.RequiresInstanceClassLoading;
+import org.apache.nifi.components.ConfigVerificationResult;
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.connector.components.ComponentState;
+import org.apache.nifi.components.connector.components.ConnectorMethod;
+import org.apache.nifi.components.connector.components.MethodArgument;
+import org.apache.nifi.components.resource.ResourceCardinality;
+import org.apache.nifi.components.resource.ResourceType;
+import org.apache.nifi.logging.ComponentLog;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.VerifiableProcessor;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Processor used to verify that additional classpath resources are available 
during configuration verification
+ * and {@code @ConnectorMethod} invocation.
+ */
+@RequiresInstanceClassLoading
+public class VerifyClasspathResource extends AbstractProcessor implements 
VerifiableProcessor {
+
+    public static final PropertyDescriptor CLASSPATH_RESOURCE = new 
PropertyDescriptor.Builder()
+            .name("Classpath Resource")
+            .description("An external resource to add to the processor 
classpath")
+            .required(false)
+            .dynamicallyModifiesClasspath(true)
+            .identifiesExternalResource(ResourceCardinality.SINGLE, 
ResourceType.FILE)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
+
+    public static final PropertyDescriptor CLASS_TO_LOAD = new 
PropertyDescriptor.Builder()
+            .name("Class to Load")
+            .description("The fully-qualified class name that must be loadable 
from the processor classpath")
+            .required(false)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
+
+    public static final String LOAD_CLASS_STEP = "Load Class From Classpath";
+
+    private final AtomicInteger connectorMethodInvocationCount = new 
AtomicInteger();
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return List.of(CLASSPATH_RESOURCE, CLASS_TO_LOAD);
+    }
+
+    @Override
+    public void onTrigger(final ProcessContext context, final ProcessSession 
session) throws ProcessException {
+    }
+
+    @Override
+    public List<ConfigVerificationResult> verify(final ProcessContext context, 
final ComponentLog verificationLogger, final Map<String, String> attributes) {
+        final String classToLoad = 
context.getProperty(CLASS_TO_LOAD).getValue();
+        if (classToLoad == null || classToLoad.isBlank()) {
+            return List.of(new ConfigVerificationResult.Builder()
+                    .verificationStepName(LOAD_CLASS_STEP)
+                    .outcome(Outcome.FAILED)
+                    .explanation("Class to Load is not configured")
+                    .build());
+        }
+
+        try {
+            loadClass(classToLoad);
+            return List.of(new ConfigVerificationResult.Builder()
+                    .verificationStepName(LOAD_CLASS_STEP)
+                    .outcome(Outcome.SUCCESSFUL)
+                    .explanation("Successfully loaded class " + classToLoad)
+                    .build());
+        } catch (final ClassNotFoundException e) {
+            return List.of(new ConfigVerificationResult.Builder()
+                    .verificationStepName(LOAD_CLASS_STEP)
+                    .outcome(Outcome.FAILED)
+                    .explanation("Failed to load class " + classToLoad + ": " 
+ e.getMessage())
+                    .build());
+        }
+    }
+
+    @ConnectorMethod(
+            name = "loadClass",
+            description = "Attempts to load the given class from the processor 
classpath",
+            allowedStates = {ComponentState.STOPPED, 
ComponentState.PROCESSOR_DISABLED},
+            arguments = {
+                    @MethodArgument(name = "className", type = String.class, 
description = "Fully-qualified class name to load", required = true)
+            }
+    )
+    public String loadClass(final String className) throws 
ClassNotFoundException {
+        final Class<?> clazz = Class.forName(className);
+        return clazz.getName();
+    }
+
+    @ConnectorMethod(
+            name = "incrementInvocationCount",
+            description = "Increments and returns the number of invocations on 
this Processor instance",
+            allowedStates = {ComponentState.STOPPED, 
ComponentState.PROCESSOR_DISABLED}
+    )
+    public int incrementInvocationCount() {
+        return connectorMethodInvocationCount.incrementAndGet();
+    }
+}
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyMethodSignatureResource.java
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyMethodSignatureResource.java
new file mode 100644
index 00000000000..cbc97799853
--- /dev/null
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/VerifyMethodSignatureResource.java
@@ -0,0 +1,75 @@
+/*
+ * 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.processors.tests.system;
+
+import org.apache.nifi.annotation.behavior.RequiresInstanceClassLoading;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.connector.components.ComponentState;
+import org.apache.nifi.components.connector.components.ConnectorMethod;
+import org.apache.nifi.components.resource.ResourceCardinality;
+import org.apache.nifi.components.resource.ResourceType;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.tests.system.dynamicclasspath.DynamicallyLoadedType;
+
+import java.util.List;
+
+/**
+ * Processor whose declared methods include a private method that returns a 
type available only through an additional
+ * classpath resource. Discovering any {@code @ConnectorMethod} on this 
processor forces the JVM to resolve every
+ * declared method signature, so the additional classpath resource must be 
present even to invoke the unrelated,
+ * constant-returning {@code returnConstant} method.
+ */
+@RequiresInstanceClassLoading
+public class VerifyMethodSignatureResource extends AbstractProcessor {
+
+    public static final PropertyDescriptor CLASSPATH_RESOURCE = new 
PropertyDescriptor.Builder()
+            .name("Classpath Resource")
+            .description("An external resource to add to the processor 
classpath")
+            .required(false)
+            .dynamicallyModifiesClasspath(true)
+            .identifiesExternalResource(ResourceCardinality.SINGLE, 
ResourceType.FILE)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .build();
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return List.of(CLASSPATH_RESOURCE);
+    }
+
+    @Override
+    public void onTrigger(final ProcessContext context, final ProcessSession 
session) throws ProcessException {
+    }
+
+    @ConnectorMethod(
+            name = "returnConstant",
+            description = "Returns a constant value without referencing any 
dynamically loaded class",
+            allowedStates = {ComponentState.STOPPED, 
ComponentState.PROCESSOR_DISABLED}
+    )
+    public String returnConstant() {
+        return "success";
+    }
+
+    @SuppressWarnings("unused")
+    private DynamicallyLoadedType dynamicallyLoadedMethod() {
+        return null;
+    }
+}
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.components.connector.Connector
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.components.connector.Connector
index c617a26993b..e6b2787790e 100644
--- 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.components.connector.Connector
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.components.connector.Connector
@@ -18,13 +18,15 @@ 
org.apache.nifi.connectors.tests.system.AsymmetricFailureMigrationConnector
 org.apache.nifi.connectors.tests.system.BacklogReportingTestConnector
 org.apache.nifi.connectors.tests.system.BundleResolutionConnector
 org.apache.nifi.connectors.tests.system.CalculateConnector
+org.apache.nifi.connectors.tests.system.ClasspathVerifyConnector
 org.apache.nifi.connectors.tests.system.ComponentLifecycleConnector
 org.apache.nifi.connectors.tests.system.DataQueuingConnector
 org.apache.nifi.connectors.tests.system.GatedDataQueuingConnector
 org.apache.nifi.connectors.tests.system.FailingConfigurationMigrationConnector
 org.apache.nifi.connectors.tests.system.FailingStateMigrationConnector
-org.apache.nifi.connectors.tests.system.MigrationTargetConnector
+org.apache.nifi.connectors.tests.system.MethodSignatureVerifyConnector
 org.apache.nifi.connectors.tests.system.MigratePropertiesConnector
+org.apache.nifi.connectors.tests.system.MigrationTargetConnector
 org.apache.nifi.connectors.tests.system.NestedProcessGroupConnector
 org.apache.nifi.connectors.tests.system.NonMigratingConnector
 org.apache.nifi.connectors.tests.system.NopConnector
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
index 2d0e1afa8ab..7d9389d9be3 100644
--- 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -63,8 +63,10 @@ org.apache.nifi.processors.tests.system.UpdateContent
 org.apache.nifi.processors.tests.system.UpdateMetric
 org.apache.nifi.processors.tests.system.UnzipFlowFile
 org.apache.nifi.processors.tests.system.ValidateFileExists
+org.apache.nifi.processors.tests.system.VerifyClasspathResource
 org.apache.nifi.processors.tests.system.VerifyContents
 org.apache.nifi.processors.tests.system.VerifyEvenThenOdd
+org.apache.nifi.processors.tests.system.VerifyMethodSignatureResource
 org.apache.nifi.processors.tests.system.WriteFlowFileCountToFile
 org.apache.nifi.processors.tests.system.WriteLifecycleEvents
 org.apache.nifi.processors.tests.system.WriteToFile
diff --git 
a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/flows/method-signature-verify-connector.json
 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/flows/method-signature-verify-connector.json
new file mode 100644
index 00000000000..9f9ba1439fd
--- /dev/null
+++ 
b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/flows/method-signature-verify-connector.json
@@ -0,0 +1,88 @@
+{
+    "flowContents": {
+        "identifier": "method-signature-verify-flow-id",
+        "name": "Method Signature Verify Flow",
+        "comments": "",
+        "position": {
+            "x": 0.0,
+            "y": 0.0
+        },
+        "processGroups": [],
+        "remoteProcessGroups": [],
+        "processors": [
+            {
+                "identifier": "5d3f8a2b-1c4e-4a6f-9b8d-2e7c1f0a3b5c",
+                "name": "Verify Method Signature Resource",
+                "comments": "",
+                "position": {
+                    "x": 0.0,
+                    "y": 0.0
+                },
+                "type": 
"org.apache.nifi.processors.tests.system.VerifyMethodSignatureResource",
+                "bundle": {
+                    "group": "org.apache.nifi",
+                    "artifact": "nifi-system-test-extensions-nar",
+                    "version": "2.8.0-SNAPSHOT"
+                },
+                "properties": {
+                    "Classpath Resource": "#{Classpath Resource}"
+                },
+                "propertyDescriptors": {},
+                "style": {},
+                "schedulingPeriod": "0 sec",
+                "schedulingStrategy": "TIMER_DRIVEN",
+                "executionNode": "ALL",
+                "penaltyDuration": "30 sec",
+                "yieldDuration": "1 sec",
+                "bulletinLevel": "WARN",
+                "runDurationMillis": 0,
+                "concurrentlySchedulableTaskCount": 1,
+                "autoTerminatedRelationships": [],
+                "scheduledState": "ENABLED",
+                "retryCount": 10,
+                "retriedRelationships": [],
+                "backoffMechanism": "PENALIZE_FLOWFILE",
+                "maxBackoffPeriod": "10 mins",
+                "componentType": "PROCESSOR",
+                "groupIdentifier": "method-signature-verify-flow-id"
+            }
+        ],
+        "inputPorts": [],
+        "outputPorts": [],
+        "connections": [],
+        "labels": [],
+        "funnels": [],
+        "controllerServices": [],
+        "parameterContextName": "Method Signature Verify Parameters",
+        "defaultFlowFileExpiration": "0 sec",
+        "defaultBackPressureObjectThreshold": 10000,
+        "defaultBackPressureDataSizeThreshold": "1 GB",
+        "scheduledState": "ENABLED",
+        "executionEngine": "INHERITED",
+        "maxConcurrentTasks": 1,
+        "statelessFlowTimeout": "1 min",
+        "flowFileOutboundPolicy": "STREAM_WHEN_AVAILABLE",
+        "flowFileConcurrency": "UNBOUNDED",
+        "componentType": "PROCESS_GROUP"
+    },
+    "externalControllerServices": {},
+    "parameterContexts": {
+        "Method Signature Verify Parameters": {
+            "name": "Method Signature Verify Parameters",
+            "parameters": [
+                {
+                    "name": "Classpath Resource",
+                    "description": "The asset JAR to place on the processor 
classpath",
+                    "sensitive": false,
+                    "provided": false
+                }
+            ],
+            "inheritedParameterContexts": [],
+            "description": "",
+            "componentType": "PARAMETER_CONTEXT"
+        }
+    },
+    "flowEncodingVersion": "1.0",
+    "parameterProviders": {},
+    "latest": false
+}
diff --git a/nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml 
b/nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml
index bd7d295f8cf..749e45d4274 100644
--- a/nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml
+++ b/nifi-system-tests/nifi-system-test-extensions-bundle/pom.xml
@@ -25,6 +25,7 @@
     <packaging>pom</packaging>
 
     <modules>
+        <module>nifi-system-test-dynamic-classpath</module>
         <module>nifi-system-test-extensions</module>
         <module>nifi-system-test-extensions-nar</module>
         <module>nifi-system-test-extensions-services</module>
diff --git a/nifi-system-tests/nifi-system-test-suite/pom.xml 
b/nifi-system-tests/nifi-system-test-suite/pom.xml
index 3064e9a4179..5de9717a222 100644
--- a/nifi-system-tests/nifi-system-test-suite/pom.xml
+++ b/nifi-system-tests/nifi-system-test-suite/pom.xml
@@ -173,6 +173,19 @@
             <version>2.12.0-SNAPSHOT</version>
             <scope>test</scope>
         </dependency>
+        <!--
+            Provided scope makes the dynamic-classpath jar resolvable on the 
test JVM classpath so ClasspathVerifyIT can
+            upload it as a Connector asset, without leaking 
DynamicallyLoadedType onto the NiFi server runtime. The
+            system-test runtime assembly (src/test/assembly/dependencies.xml) 
copies only runtime-scope artifacts named
+            in its includes allowlist, so this provided artifact never lands 
in nifi-lib-assembly. The type is therefore
+            available to the running component only through the uploaded 
asset's dynamic classpath.
+        -->
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-system-test-dynamic-classpath</artifactId>
+            <version>2.12.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
         <dependency>
             <groupId>org.glassfish.jersey.inject</groupId>
             <artifactId>jersey-hk2</artifactId>
diff --git 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClasspathVerifyIT.java
 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClasspathVerifyIT.java
new file mode 100644
index 00000000000..89f9aeab644
--- /dev/null
+++ 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/connectors/ClasspathVerifyIT.java
@@ -0,0 +1,279 @@
+/*
+ * 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.tests.system.connectors;
+
+import org.apache.nifi.components.ConfigVerificationResult.Outcome;
+import org.apache.nifi.tests.system.NiFiSystemIT;
+import org.apache.nifi.tests.system.dynamicclasspath.DynamicallyLoadedType;
+import org.apache.nifi.toolkit.client.NiFiClientException;
+import org.apache.nifi.web.api.dto.AssetReferenceDTO;
+import org.apache.nifi.web.api.dto.ConfigVerificationResultDTO;
+import org.apache.nifi.web.api.dto.ConnectorValueReferenceDTO;
+import org.apache.nifi.web.api.entity.AssetEntity;
+import org.apache.nifi.web.api.entity.ConnectorEntity;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * System tests that verify additional classpath resources are loaded during 
connector configuration verification,
+ * Connector Method discovery and invocation use the Working component 
classpath after Parameter updates and after
+ * Processor or Controller Service property updates, and consecutive 
invocations preserve component instance state.
+ */
+public class ClasspathVerifyIT extends NiFiSystemIT {
+
+    private static final String STEP_NAME = "Classpath Configuration";
+    private static final String METHOD_SIGNATURE_STEP_NAME = "Method Signature 
Configuration";
+    private static final String STRATEGY_PROCESSOR_VERIFY = "Processor Verify";
+    private static final String STRATEGY_CONNECTOR_METHOD = "Connector Method";
+    private static final String STRATEGY_CONNECTOR_METHOD_STATE = "Connector 
Method State";
+    private static final String LOAD_CLASS_STEP = "Load Class From Classpath";
+    private static final String CONNECTOR_METHOD_STEP = "Invoke loadClass 
Connector Method";
+    private static final String CONNECTOR_METHOD_STATE_STEP = "Preserve 
Connector Method Component State";
+    private static final String METHOD_SIGNATURE_STEP = "Discover Connector 
Method With Dynamic Signature";
+
+    private static final String CLASSPATH_RESOURCE = "Classpath Resource";
+    private static final String CLASS_TO_LOAD = "Class to Load";
+    private static final String VERIFICATION_STRATEGY = "Verification 
Strategy";
+    private static final String CLASSPATH_APPLICATION = "Classpath 
Application";
+    private static final String APPLICATION_PROCESSOR_PROPERTY = "Processor 
Property";
+    private static final String APPLICATION_CONTROLLER_SERVICE_PROPERTY = 
"Controller Service Property";
+
+    private static final String DYNAMIC_CLASSPATH_CLASS = 
DynamicallyLoadedType.class.getName();
+    private static final String UNKNOWN_CLASS = 
"org.apache.nifi.tests.system.DoesNotExistOnClasspath";
+
+    @Test
+    public void testProcessorVerifyUsesTemporaryClasspathWhenUncommitted() 
throws NiFiClientException, IOException, InterruptedException {
+        final ConnectorEntity connector = 
getClientUtil().createConnector("ClasspathVerifyConnector");
+        final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+        final List<ConfigVerificationResultDTO> results = 
getClientUtil().verifyConnectorStepConfigWithReferences(
+                connector.getId(), STEP_NAME, createVerifyReferences(assetId, 
DYNAMIC_CLASSPATH_CLASS, STRATEGY_PROCESSOR_VERIFY));
+
+        assertSuccessfulClasspathStep(results, LOAD_CLASS_STEP);
+    }
+
+    @Test
+    public void testConnectorMethodLoadsClassFromCommittedClasspath() throws 
NiFiClientException, IOException, InterruptedException {
+        final ConnectorEntity connector = 
getClientUtil().createConnector("ClasspathVerifyConnector");
+        final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+        getClientUtil().configureConnectorWithReferences(connector.getId(), 
STEP_NAME, createVerifyReferences(assetId, DYNAMIC_CLASSPATH_CLASS, 
STRATEGY_CONNECTOR_METHOD));
+        getClientUtil().applyConnectorUpdate(connector);
+        getClientUtil().waitForValidConnector(connector.getId());
+
+        final List<ConfigVerificationResultDTO> results = 
getClientUtil().verifyConnectorStepConfigWithReferences(
+                connector.getId(), STEP_NAME, createVerifyReferences(assetId, 
DYNAMIC_CLASSPATH_CLASS, STRATEGY_CONNECTOR_METHOD));
+
+        assertSuccessfulClasspathStep(results, CONNECTOR_METHOD_STEP);
+    }
+
+    @Test
+    public void testConnectorMethodPreservesWorkingComponentState() throws 
NiFiClientException, IOException, InterruptedException {
+        final ConnectorEntity connector = 
getClientUtil().createConnector("ClasspathVerifyConnector");
+        final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+        getClientUtil().configureConnectorWithReferences(connector.getId(), 
STEP_NAME, createVerifyReferences(assetId, DYNAMIC_CLASSPATH_CLASS, 
STRATEGY_CONNECTOR_METHOD_STATE));
+
+        final List<ConfigVerificationResultDTO> results = 
getClientUtil().verifyConnectorStepConfigWithReferences(
+                connector.getId(), STEP_NAME, createVerifyReferences(assetId, 
DYNAMIC_CLASSPATH_CLASS, STRATEGY_CONNECTOR_METHOD_STATE));
+
+        assertSuccessfulClasspathStep(results, CONNECTOR_METHOD_STATE_STEP);
+    }
+
+    @Test
+    public void testConnectorMethodLoadsAssetWithMethodSignatureDependency() 
throws NiFiClientException, IOException, InterruptedException {
+        final ConnectorEntity connector = 
getClientUtil().createConnector("MethodSignatureVerifyConnector");
+        final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+        getClientUtil().configureConnectorWithReferences(connector.getId(), 
METHOD_SIGNATURE_STEP_NAME, createAssetReferences(assetId));
+
+        final List<ConfigVerificationResultDTO> results = 
getClientUtil().verifyConnectorStepConfigWithReferences(connector.getId(), 
METHOD_SIGNATURE_STEP_NAME, createAssetReferences(assetId));
+
+        assertSuccessfulClasspathStep(results, METHOD_SIGNATURE_STEP);
+    }
+
+    @Test
+    public void testConnectorMethodLoadsAssetWhenSetAsProcessorProperty() 
throws NiFiClientException, IOException, InterruptedException {
+        final ConnectorEntity connector = 
getClientUtil().createConnector("MethodSignatureVerifyConnector");
+        final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+        getClientUtil().configureConnectorWithReferences(connector.getId(), 
METHOD_SIGNATURE_STEP_NAME, createMethodSignatureReferences(assetId, 
APPLICATION_PROCESSOR_PROPERTY));
+
+        final List<ConfigVerificationResultDTO> results = 
getClientUtil().verifyConnectorStepConfigWithReferences(
+                connector.getId(), METHOD_SIGNATURE_STEP_NAME, 
createMethodSignatureReferences(assetId, APPLICATION_PROCESSOR_PROPERTY));
+
+        assertSuccessfulClasspathStep(results, METHOD_SIGNATURE_STEP);
+    }
+
+    @Test
+    public void 
testConnectorMethodLoadsAssetWhenSetAsControllerServiceProperty() throws 
NiFiClientException, IOException, InterruptedException {
+        final ConnectorEntity connector = 
getClientUtil().createConnector("MethodSignatureVerifyConnector");
+        final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+        getClientUtil().configureConnectorWithReferences(connector.getId(), 
METHOD_SIGNATURE_STEP_NAME, createMethodSignatureReferences(assetId, 
APPLICATION_CONTROLLER_SERVICE_PROPERTY));
+
+        final List<ConfigVerificationResultDTO> results = 
getClientUtil().verifyConnectorStepConfigWithReferences(
+                connector.getId(), METHOD_SIGNATURE_STEP_NAME, 
createMethodSignatureReferences(assetId, 
APPLICATION_CONTROLLER_SERVICE_PROPERTY));
+
+        assertSuccessfulClasspathStep(results, METHOD_SIGNATURE_STEP);
+    }
+
+    @Test
+    public void testConnectorMethodFailsWhenClassMissingFromClasspath() throws 
NiFiClientException, IOException, InterruptedException {
+        final ConnectorEntity connector = 
getClientUtil().createConnector("ClasspathVerifyConnector");
+        final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+        getClientUtil().configureConnectorWithReferences(connector.getId(), 
STEP_NAME, createVerifyReferences(assetId, UNKNOWN_CLASS, 
STRATEGY_CONNECTOR_METHOD));
+        getClientUtil().applyConnectorUpdate(connector);
+        getClientUtil().waitForValidConnector(connector.getId());
+
+        final List<ConfigVerificationResultDTO> results = 
getClientUtil().verifyConnectorStepConfigWithReferences(
+                connector.getId(), STEP_NAME, createVerifyReferences(assetId, 
UNKNOWN_CLASS, STRATEGY_CONNECTOR_METHOD));
+
+        assertFailedClasspathStep(results, CONNECTOR_METHOD_STEP);
+    }
+
+    @Test
+    public void testProcessorVerifyUsesLiveClasspathWhenCommitted() throws 
NiFiClientException, IOException, InterruptedException {
+        final ConnectorEntity connector = 
getClientUtil().createConnector("ClasspathVerifyConnector");
+        final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+        getClientUtil().configureConnectorWithReferences(connector.getId(), 
STEP_NAME, createVerifyReferences(assetId, DYNAMIC_CLASSPATH_CLASS, 
STRATEGY_PROCESSOR_VERIFY));
+        getClientUtil().applyConnectorUpdate(connector);
+        getClientUtil().waitForValidConnector(connector.getId());
+
+        final List<ConfigVerificationResultDTO> results = 
getClientUtil().verifyConnectorStepConfigWithReferences(
+                connector.getId(), STEP_NAME, createVerifyReferences(assetId, 
DYNAMIC_CLASSPATH_CLASS, STRATEGY_PROCESSOR_VERIFY));
+
+        assertSuccessfulClasspathStep(results, LOAD_CLASS_STEP);
+    }
+
+    @Test
+    public void testProcessorVerifyFailsForUnknownClass() throws 
NiFiClientException, IOException, InterruptedException {
+        final ConnectorEntity connector = 
getClientUtil().createConnector("ClasspathVerifyConnector");
+        final String assetId = uploadDynamicClasspathAsset(connector.getId());
+
+        getClientUtil().configureConnectorWithReferences(connector.getId(), 
STEP_NAME, createVerifyReferences(assetId, DYNAMIC_CLASSPATH_CLASS, 
STRATEGY_PROCESSOR_VERIFY));
+        getClientUtil().applyConnectorUpdate(connector);
+        getClientUtil().waitForValidConnector(connector.getId());
+
+        final List<ConfigVerificationResultDTO> results = 
getClientUtil().verifyConnectorStepConfigWithReferences(
+                connector.getId(), STEP_NAME, createVerifyReferences(assetId, 
UNKNOWN_CLASS, STRATEGY_PROCESSOR_VERIFY));
+
+        assertFailedClasspathStep(results, LOAD_CLASS_STEP);
+    }
+
+    private String uploadDynamicClasspathAsset(final String connectorId) 
throws IOException, NiFiClientException {
+        // DynamicallyLoadedType is available on the test JVM classpath 
through a provided-scope dependency, which
+        // resolves to the module's built jar without adding the type to the 
NiFi server runtime assembly. Uploading that
+        // jar as the Connector asset makes the type available to the running 
component only through its dynamic classpath.
+        final File dynamicClasspathJar = locateDynamicClasspathJar();
+
+        final File assetCopy = new File("target/dynamic-classpath-" + 
connectorId + ".jar");
+        Files.copy(dynamicClasspathJar.toPath(), assetCopy.toPath(), 
StandardCopyOption.REPLACE_EXISTING);
+
+        final AssetEntity assetEntity = 
getNifiClient().getConnectorClient().createAsset(connectorId, 
assetCopy.getName(), assetCopy);
+        assertNotNull(assetEntity);
+        assertNotNull(assetEntity.getAsset());
+        return assetEntity.getAsset().getId();
+    }
+
+    private File locateDynamicClasspathJar() {
+        final URL location = 
DynamicallyLoadedType.class.getProtectionDomain().getCodeSource().getLocation();
+        final File jarFile;
+        try {
+            jarFile = new File(location.toURI());
+        } catch (final URISyntaxException e) {
+            throw new IllegalStateException("Could not resolve the dynamic 
classpath jar from location " + location, e);
+        }
+
+        if (!jarFile.isFile() || !jarFile.getName().endsWith(".jar")) {
+            throw new IllegalStateException("Expected the 
nifi-system-test-dynamic-classpath dependency to resolve to a jar file but 
found "
+                    + jarFile.getAbsolutePath() + "; ensure the module is 
installed so the provided-scope dependency resolves to a jar");
+        }
+
+        return jarFile;
+    }
+
+    private Map<String, ConnectorValueReferenceDTO> 
createVerifyReferences(final String assetId, final String classToLoad, final 
String strategy) {
+        final Map<String, ConnectorValueReferenceDTO> propertyValues = new 
HashMap<>();
+        propertyValues.put(CLASSPATH_RESOURCE, createAssetReference(assetId));
+        propertyValues.put(CLASS_TO_LOAD, 
createStringLiteralReference(classToLoad));
+        propertyValues.put(VERIFICATION_STRATEGY, 
createStringLiteralReference(strategy));
+        return propertyValues;
+    }
+
+    private Map<String, ConnectorValueReferenceDTO> 
createAssetReferences(final String assetId) {
+        final Map<String, ConnectorValueReferenceDTO> propertyValues = new 
HashMap<>();
+        propertyValues.put(CLASSPATH_RESOURCE, createAssetReference(assetId));
+        return propertyValues;
+    }
+
+    private Map<String, ConnectorValueReferenceDTO> 
createMethodSignatureReferences(final String assetId, final String 
classpathApplication) {
+        final Map<String, ConnectorValueReferenceDTO> propertyValues = 
createAssetReferences(assetId);
+        propertyValues.put(CLASSPATH_APPLICATION, 
createStringLiteralReference(classpathApplication));
+        return propertyValues;
+    }
+
+    private ConnectorValueReferenceDTO createAssetReference(final String 
assetId) {
+        final ConnectorValueReferenceDTO assetReference = new 
ConnectorValueReferenceDTO();
+        assetReference.setValueType("ASSET_REFERENCE");
+        assetReference.setAssetReferences(List.of(new 
AssetReferenceDTO(assetId)));
+        return assetReference;
+    }
+
+    private ConnectorValueReferenceDTO createStringLiteralReference(final 
String value) {
+        final ConnectorValueReferenceDTO valueReference = new 
ConnectorValueReferenceDTO();
+        valueReference.setValueType("STRING_LITERAL");
+        valueReference.setValue(value);
+        return valueReference;
+    }
+
+    private void assertSuccessfulClasspathStep(final 
List<ConfigVerificationResultDTO> results, final String stepName) {
+        assertNotNull(results);
+        final ConfigVerificationResultDTO classpathResult = findStep(results, 
stepName);
+        assertEquals(Outcome.SUCCESSFUL.name(), classpathResult.getOutcome(), 
classpathResult.getExplanation());
+    }
+
+    private void assertFailedClasspathStep(final 
List<ConfigVerificationResultDTO> results, final String stepName) {
+        assertNotNull(results);
+        final ConfigVerificationResultDTO classpathResult = findStep(results, 
stepName);
+        assertEquals(Outcome.FAILED.name(), classpathResult.getOutcome(), 
classpathResult.getExplanation());
+    }
+
+    private ConfigVerificationResultDTO findStep(final 
List<ConfigVerificationResultDTO> results, final String stepName) {
+        return results.stream()
+                .filter(result -> 
stepName.equals(result.getVerificationStepName()))
+                .findFirst()
+                .orElseThrow(() -> new AssertionError("Expected verification 
step '" + stepName + "' in results: " + results.stream()
+                        
.map(ConfigVerificationResultDTO::getVerificationStepName)
+                        .toList()));
+    }
+}

Reply via email to