You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by ma...@apache.org on 2020/06/08 20:04:45 UTC

[nifi] branch master updated: NIFI-7511 In ControllerServiceProxyWrapper extended documentation. Minor refactor in StandardControllerServiceInvocationHandler. Also removed an unused import from NiFiSystemIT.

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

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


The following commit(s) were added to refs/heads/master by this push:
     new aa7c5e2  NIFI-7511 In ControllerServiceProxyWrapper extended documentation. Minor refactor in StandardControllerServiceInvocationHandler. Also removed an unused import from NiFiSystemIT.
aa7c5e2 is described below

commit aa7c5e21782cf4d2f4b10ae63851c53f7c47a80f
Author: Tamas Palfy <ta...@gmail.com>
AuthorDate: Fri Jun 5 21:43:49 2020 +0200

    NIFI-7511 In ControllerServiceProxyWrapper extended documentation. Minor refactor in StandardControllerServiceInvocationHandler. Also removed an unused import from NiFiSystemIT.
    
    This closes #4317.
    
    Signed-off-by: Mark Payne <ma...@hotmail.com>
---
 .../controller/ControllerServiceProxyWrapper.java  | 117 +++++++++++++++++----
 ...StandardControllerServiceInvocationHandler.java |  78 +++++++-------
 2 files changed, 133 insertions(+), 62 deletions(-)

diff --git a/nifi-framework-api/src/main/java/org/apache/nifi/controller/ControllerServiceProxyWrapper.java b/nifi-framework-api/src/main/java/org/apache/nifi/controller/ControllerServiceProxyWrapper.java
index 3a7bd75..b00e218 100644
--- a/nifi-framework-api/src/main/java/org/apache/nifi/controller/ControllerServiceProxyWrapper.java
+++ b/nifi-framework-api/src/main/java/org/apache/nifi/controller/ControllerServiceProxyWrapper.java
@@ -18,35 +18,110 @@
 package org.apache.nifi.controller;
 
 /**
- * An interface that can be added to a Proxy object for a Controller Service in order to get the underlying object that is being proxied.
- * This is done so that any object that is returned by a Controller Service
- * can be unwrapped if it's passed back to the Controller Service. This is needed in order to accommodate for the following scenario.
- * Consider a Controller Service that has two methods:
- *
- * <pre><code>
- * public MyObject createObject();
- * public void useObject(MyObject object);
+ * The purpose of this interface is to help handle the following scenario:
+ *
+ * A Controller Service method returns a value which is wrapped in a proxy by the framework.
+ * Another method of the Controller Service receives the same proxy as an argument. (The Controller Service gets back the object.)
+ *  This method expects the argument to be of the concrete type of the real return value.
+ *  Since the proxy only preserves the interface of the real return value but not the concrete type, the method fails.
+ *
+ * To fix this, this interface is added to the proxy (along with the original interface) and the framework will get the real value via {@link #getWrapped()}
+ *  so the Controller Service will receive the real object.
+ *
+ * E.g.:
+ *
+ * <pre><code>public interface IConnectionProviderService {
+ *     IConnection getConnection();
+ *     void closeConnection(IConnection);
+ * }
+ *
+ * public class ConnectionProviderServiceImpl {
+ *     IConnection getConnection() {
+ *         return new SimpleConnection();
+ *     }
+ *
+ *     void closeConnection(IConnection) {
+ *         if (connection instanceof SimpleConnection) {
+ *             ...
+ *         } else {
+ *             throw new InvalidArgumentException();
+ *         }
+ *     }
+ * }
+ *
+ * public class ConnectionUserProcessor {
+ *     IConnectionProviderService service; #Set to ConnectionProviderServiceImpl
+ *
+ *     void onTrigger() {
+ *         IConnection connection = service.getConnection();
+ *
+ *         # 'connection' at this point is a proxy of a 'SimpleConnection' object
+ *         # So '(connection instanceof IConnection)' is true, but
+ *         # '(connection instanceof SimpleConnection)' is false
+ *
+ *         ...
+ *
+ *         service.closeConnection(connection); # !! This would have thrown InvalidArgumentException
+ *     }
+ * }
  * </code></pre>
  *
- * And further consider that MyObject is an interface with multiple implementations.
- * If the {@code useObject} method is implemented using logic such as:
+ * But why wrap the return value in a proxy in the first place? It is needed to handle the following scenario:
+ *
+ * A Controller Service method returns an object to a Processor.
+ * A method is called on the returned object int the Processor.
+ * This method tries to load a class that is in the same package as the return object.
+ * Since it tries to use the Processor classloader, it fails.
+ *
+ * E.g.:
+ * <pre><code>package root.interface;
+ *
+ * public interface IReportService {
+ *     IReport getReport();
+ * }
+ * public interface IReport {
+ *     void submit();
+ * }
+ *
+ *
+ * package root.service;
+ *
+ * public class ReportServiceImpl {
+ *     IReport getReport() {
+ *         return new Report();
+ *     }
+ * }
+ * public class ReportImpl {
+ *     void submit() {
+ *         Class.forName("roo.service.OtherClass");
+ *         ...
+ *     }
+ * }
+ * public class OtherClass {}
+ *
+ *
+ * package root.processor;
+ *
+ * public class ReportProcessor {
+ *     IReportService service; #Set to ReportServiceImpl
  *
- * <pre><code>
- * public void useObject(final MyObject object) {
- *       if (object instanceof SomeObject) {
- *           // perform some action
- *       }
+ *     void onTrigger() {
+ *         IReport report = service.getReport();
+ *         ...
+ *         report.submit(); # !! This would have thrown ClassNotFoundException
+ *     }
  * }
  * </code></pre>
  *
- * In this case, if the {@code createObject} method does in fact create an instance of {@code SomeObject}, the proxied object that is returned will not be of type {@code SomeObject}
- * because {@code SomeObject} is a class, not an interface. So the proxy implements the {@code MyObject} interface, but it is not an instance of {@code SomeObject}.
- * As a result, the instanceof check would return {@code false} but the service implementor should reasonably expect it to return {@code true}.
- * In order to accommodate this behavior, this interface can be added to the proxy and then the underlying object can be "unwrapped" when being provided to the Controller Service.
+ * So in general there is a barrier between the Controller Service and the Processor (or another Controller Service) due to the fact that they have
+ * their own classloaders.
+ * When an object crosses the barrier, it needs to be proxied to be able to use its original classloader.
+ * Also when it crosses the barrier back again, it needs to be unproxied to preserve specific type information.
  *
- * The {@link java.lang.reflect.InvocationHandler InvocationHandler} is then able to implement the method in order to unwrap the object.
+ * Note that wrapping may not be necessary if the class is loaded by a classloader that is parent to both the Controller Service and the Processor,
+ * as in this case the 'barrier' not really there for instances of that class.
  *
- * @param <T> the type of the wrapped object
+ * @param <T> the type of the wrapped/proxied object
  */
 public interface ControllerServiceProxyWrapper<T> {
     /**
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceInvocationHandler.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceInvocationHandler.java
index ef0cdf1..f6217b8 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceInvocationHandler.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/service/StandardControllerServiceInvocationHandler.java
@@ -97,22 +97,12 @@ public class StandardControllerServiceInvocationHandler implements ControllerSer
                 + serviceNodeHolder.get().getIdentifier() + " because the Controller Service's State is currently " + state);
         }
 
-        final ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
+        final ClassLoader callerClassLoader = Thread.currentThread().getContextClassLoader();
         try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(extensionManager, originalService.getClass(), originalService.getIdentifier())) {
             // If any objects are proxied, unwrap them so that we provide the unproxied object to the Controller Service.
-            final Object[] unwrapped = unwrapProxies(args, Thread.currentThread().getContextClassLoader(), method);
+            ClassLoader serviceClassLoader = Thread.currentThread().getContextClassLoader();
 
-            // Invoke the method on the underlying implementation
-            final Object returnedFromImpl = method.invoke(originalService, unwrapped);
-
-            // If the return object is known to the caller, it can be returned directly. Otherwise, proxy the object so that
-            // calls into the proxy are called through the appropriate ClassLoader.
-            if (returnedFromImpl == null || isInHierarchy(returnedFromImpl.getClass().getClassLoader(), originalClassLoader)) {
-                return returnedFromImpl;
-            }
-
-            // Proxy the return object, if necessary, and return the proxy.
-            return proxy(returnedFromImpl, method.getReturnType());
+            return invoke(originalService, method, args, serviceClassLoader, callerClassLoader);
         } catch (final InvocationTargetException e) {
             // If the ControllerService throws an Exception, it'll be wrapped in an InvocationTargetException. We want
             // to instead re-throw what the ControllerService threw, so we pull it out of the InvocationTargetException.
@@ -130,8 +120,8 @@ public class StandardControllerServiceInvocationHandler implements ControllerSer
         return isInHierarchy(objectClassLoader, classLoaderHierarchy.getParent());
     }
 
-    private Object proxy(final Object toProxy, final Class<?> declaredType) {
-        if (toProxy == null) {
+    private Object proxy(final Object bareObject, final Class<?> declaredType) {
+        if (bareObject == null) {
             return null;
         }
 
@@ -139,12 +129,12 @@ public class StandardControllerServiceInvocationHandler implements ControllerSer
         // was invoked as being an interface. For example, if a method is expected to return a java.lang.String,
         // we do not want to instead return a proxy because the Proxy won't be a String.
         if (declaredType == null || !declaredType.isInterface()) {
-            return toProxy;
+            return bareObject;
         }
 
         // If the ClassLoader is null, we have a primitive type, which we can't proxy.
-        if (toProxy.getClass().getClassLoader() == null) {
-            return toProxy;
+        if (bareObject.getClass().getClassLoader() == null) {
+            return bareObject;
         }
 
         // The proxy that is to be returned needs to ensure that it implements all interfaces that are defined by the
@@ -155,9 +145,9 @@ public class StandardControllerServiceInvocationHandler implements ControllerSer
         //
         // final javax.jms.Message myMessage = controllerService.getMessage();
         // final boolean isBytes = myMessage instanceof javax.jms.BytesMessage;
-        final List<Class<?>> interfaces = ClassUtils.getAllInterfaces(toProxy.getClass());
+        final List<Class<?>> interfaces = ClassUtils.getAllInterfaces(bareObject.getClass());
         if (interfaces == null || interfaces.isEmpty()) {
-            return toProxy;
+            return bareObject;
         }
 
         // Add the ControllerServiceProxyWrapper to the List of interfaces to implement. See javadocs for ControllerServiceProxyWrapper
@@ -167,8 +157,8 @@ public class StandardControllerServiceInvocationHandler implements ControllerSer
         }
 
         final Class<?>[] interfaceTypes = interfaces.toArray(new Class<?>[0]);
-        final InvocationHandler invocationHandler = new ProxiedReturnObjectInvocationHandler(toProxy);
-        return Proxy.newProxyInstance(toProxy.getClass().getClassLoader(), interfaceTypes, invocationHandler);
+        final InvocationHandler invocationHandler = new ProxiedReturnObjectInvocationHandler(bareObject);
+        return Proxy.newProxyInstance(bareObject.getClass().getClassLoader(), interfaceTypes, invocationHandler);
     }
 
     private Object[] unwrapProxies(final Object[] values, final ClassLoader expectedClassLoader, final Method method) {
@@ -229,40 +219,46 @@ public class StandardControllerServiceInvocationHandler implements ControllerSer
     }
 
     private class ProxiedReturnObjectInvocationHandler implements InvocationHandler {
-        private final Object toProxy;
-        private final ClassLoader classLoaderForProxy;
+        private final Object bareObject;
+        private ClassLoader bareObjectClassLoader;
 
-        public ProxiedReturnObjectInvocationHandler(final Object toProxy) {
-            this.toProxy = toProxy;
-            this.classLoaderForProxy = toProxy.getClass().getClassLoader();
+        public ProxiedReturnObjectInvocationHandler(final Object bareObject) {
+            this.bareObject = bareObject;
+            this.bareObjectClassLoader = bareObject.getClass().getClassLoader();
         }
 
         @Override
         public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
             if (PROXY_WRAPPER_GET_WRAPPED_METHOD.equals(method)) {
-                return toProxy;
+                return this.bareObject;
             }
 
-            final Object[] unwrapped = unwrapProxies(args, classLoaderForProxy, method);
-
-            final ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
+            final ClassLoader callerClassLoader = Thread.currentThread().getContextClassLoader();
             try {
-                Thread.currentThread().setContextClassLoader(classLoaderForProxy);
-                final Object returnedFromImpl = method.invoke(toProxy, unwrapped);
-
-                // If the return object is known to the caller, it can be returned directly. Otherwise, proxy the object so that
-                // calls into the proxy are called through the appropriate ClassLoader.
-                if (returnedFromImpl == null || isInHierarchy(returnedFromImpl.getClass().getClassLoader(), originalClassLoader)) {
-                    return returnedFromImpl;
-                }
+                Thread.currentThread().setContextClassLoader(this.bareObjectClassLoader);
 
-                return proxy(returnedFromImpl, method.getReturnType());
+                return StandardControllerServiceInvocationHandler.this.invoke(this.bareObject, method, args, this.bareObjectClassLoader, callerClassLoader);
             } catch (final InvocationTargetException ite) {
                 throw ite.getCause();
             } finally {
-                Thread.currentThread().setContextClassLoader(originalClassLoader);
+                Thread.currentThread().setContextClassLoader(callerClassLoader);
             }
         }
+    }
+
+    private Object invoke(Object bareObject, Method method, Object[] args, ClassLoader bareObjectClassLoader, ClassLoader callerClassLoader) throws IllegalAccessException, InvocationTargetException {
+        // If any objects are proxied, unwrap them so that we provide the unproxied object to the Controller Service.
+        final Object[] unwrappedArgs = unwrapProxies(args, bareObjectClassLoader, method);
+
+        // Invoke the method on the underlying implementation
+        final Object returnedFromBareObject = method.invoke(bareObject, unwrappedArgs);
+
+        // If the return object is known to the caller, it can be returned directly. Otherwise, proxy the object so that
+        // calls into the proxy are called through the appropriate ClassLoader.
+        if (returnedFromBareObject == null || isInHierarchy(returnedFromBareObject.getClass().getClassLoader(), callerClassLoader)) {
+            return returnedFromBareObject;
+        }
 
+        return proxy(returnedFromBareObject, method.getReturnType());
     }
 }