Repository: eclipse-vertx/vert.x

Problem statement:
Verticle deployment class loader
Verticle deployment has been using the implicit thread context classloader for the `VerticleFactory` and the `io.vertx.core.Context` class loader. This classloader is used indirectly by code that needs to resolve file in some places (e.g `HttpServerResponse#sendFile(String)`). Until now the only possibility to use a specific classloader was the set a temporary thread context classloader in some places (such as deploying a verticle or sending a file).

The `DeploymentOptions` will now hold an optional classloader that will be passed to the `VerticleFactory` and associated with the `Context`. When no classloader is provided, the current thread context classloader is used.


Hints:

Patch:
diff --git a/src/main/java/io/vertx/core/DeploymentOptions.java b/src/main/java/io/vertx/core/DeploymentOptions.java
--- a/src/main/java/io/vertx/core/DeploymentOptions.java
+++ b/src/main/java/io/vertx/core/DeploymentOptions.java
@@ -45,6 +45,7 @@ public class DeploymentOptions {
   private int instances;
   private List<String> isolatedClasses;
   private TimeUnit maxWorkerExecuteTimeUnit;
+  private ClassLoader classLoader;
 
   /**
    * Default constructor
@@ -360,6 +361,31 @@ public DeploymentOptions setMaxWorkerExecuteTimeUnit(TimeUnit maxWorkerExecuteTi
     return this;
   }
 
+  /**
+   * @return the classloader used for deploying the Verticle
+   */
+  public ClassLoader getClassLoader() {
+    return classLoader;
+  }
+
+  /**
+   * Set the classloader to use for deploying the Verticle.
+   *
+   * <p> The {@code VerticleFactory} will use this classloader for creating the Verticle
+   * and the Verticle {@link io.vertx.core.Context} will set this classloader as context
+   * classloader for the tasks execution on context.
+   *
+   * <p> By default no classloader is required and the deployment will use the current thread context
+   * classloader.
+   *
+   * @param classLoader the loader to use
+   * @return a reference to this, so the API can be used fluently
+   */
+  public DeploymentOptions setClassLoader(ClassLoader classLoader) {
+    this.classLoader = classLoader;
+    return this;
+  }
+
   /**
    * Throw {@code IllegalArgumentException} when loader isolation configuration has been defined.
    */
diff --git a/src/main/java/io/vertx/core/impl/DeploymentManager.java b/src/main/java/io/vertx/core/impl/DeploymentManager.java
--- a/src/main/java/io/vertx/core/impl/DeploymentManager.java
+++ b/src/main/java/io/vertx/core/impl/DeploymentManager.java
@@ -62,7 +62,13 @@ public Future<String> deployVerticle(Callable<Verticle> verticleSupplier, Deploy
     }
     options.checkIsolationNotDefined();
     ContextInternal currentContext = vertx.getOrCreateContext();
-    ClassLoader cl = getCurrentClassLoader();
+    ClassLoader cl = options.getClassLoader();
+    if (cl == null) {
+      cl = Thread.currentThread().getContextClassLoader();
+      if (cl == null) {
+        cl = getClass().getClassLoader();
+      }
+    }
     return doDeploy(options, v -> "java:" + v.getClass().getName(), currentContext, currentContext, cl, verticleSupplier)
       .map(Deployment::deploymentID);
   }
@@ -116,14 +122,6 @@ public Future<Void> undeployAll() {
     }
   }
 
-  private ClassLoader getCurrentClassLoader() {
-    ClassLoader cl = Thread.currentThread().getContextClassLoader();
-    if (cl == null) {
-      cl = getClass().getClassLoader();
-    }
-    return cl;
-  }
-
   private <T> void reportFailure(Throwable t, Context context, Handler<AsyncResult<T>> completionHandler) {
     if (completionHandler != null) {
       reportResult(context, completionHandler, Future.failedFuture(t));
diff --git a/src/main/java/io/vertx/core/impl/VerticleManager.java b/src/main/java/io/vertx/core/impl/VerticleManager.java
--- a/src/main/java/io/vertx/core/impl/VerticleManager.java
+++ b/src/main/java/io/vertx/core/impl/VerticleManager.java
@@ -31,7 +31,7 @@
  * @author <a href="http://tfox.org">Tim Fox</a>
  */
 public class VerticleManager {
-  
+
   private final VertxInternal vertx;
   private final DeploymentManager deploymentManager;
   private final LoaderManager loaderManager = new LoaderManager();
@@ -145,8 +145,14 @@ private static String getSuffix(int pos, String str) {
   public Future<Deployment> deployVerticle(String identifier,
                                        DeploymentOptions options) {
     ContextInternal callingContext = vertx.getOrCreateContext();
-    ClassLoaderHolder holder = loaderManager.getClassLoader(options);
-    ClassLoader loader = holder != null ? holder.loader : getCurrentClassLoader();
+    ClassLoaderHolder holder;
+    ClassLoader loader = options.getClassLoader();
+    if (loader == null) {
+      holder = loaderManager.getClassLoader(options);
+      loader = holder != null ? holder.loader : getCurrentClassLoader();
+    } else {
+      holder = null;
+    }
     Future<Deployment> deployment = doDeployVerticle(identifier, options, callingContext, callingContext, loader);
     if (holder != null) {
       deployment.onComplete(ar -> {
diff --git a/src/main/java11/io/vertx/core/DeploymentOptions.java b/src/main/java11/io/vertx/core/DeploymentOptions.java
--- a/src/main/java11/io/vertx/core/DeploymentOptions.java
+++ b/src/main/java11/io/vertx/core/DeploymentOptions.java
@@ -41,6 +41,7 @@ public class DeploymentOptions {
   private boolean ha;
   private int instances;
   private TimeUnit maxWorkerExecuteTimeUnit;
+  private ClassLoader classLoader;
 
   /**
    * Default constructor
@@ -267,6 +268,30 @@ public DeploymentOptions setMaxWorkerExecuteTimeUnit(TimeUnit maxWorkerExecuteTi
     return this;
   }
 
+  /**
+   * @return the classloader used for deploying the Verticle
+   */
+  public ClassLoader getClassLoader() {
+    return classLoader;
+  }
+
+  /**
+   * Set the classloader to use for deploying the Verticle.
+   *
+   * <p> The {@code VerticleFactory} will use this classloader for creating the Verticle
+   * and the Verticle {@link io.vertx.core.Context} will set this classloader as context
+   * classloader for the tasks execution on context.
+   *
+   * <p> By default no classloader is required and the deployment will use the current thread context
+   * classloader.
+   *
+   * @param classLoader the loader to use
+   * @return a reference to this, so the API can be used fluently
+   */
+  public DeploymentOptions setClassLoader(ClassLoader classLoader) {
+    this.classLoader = classLoader;
+    return this;
+  }
   /**
    * Does nothing.
    */


Test patch:
diff --git a/src/test/java/io/vertx/core/DeploymentTest.java b/src/test/java/io/vertx/core/DeploymentTest.java
--- a/src/test/java/io/vertx/core/DeploymentTest.java
+++ b/src/test/java/io/vertx/core/DeploymentTest.java
@@ -1590,6 +1590,41 @@ private void testIsolationGroup(String group1, String group2, int count1, int co
     }
   }
 
+  @Test
+  public void testContextClassLoader() throws Exception {
+    File tmp = File.createTempFile("vertx-", ".txt");
+    tmp.deleteOnExit();
+    Files.write(tmp.toPath(), "hello".getBytes());
+    URL url = tmp.toURI().toURL();
+    AtomicBoolean used = new AtomicBoolean();
+    ClassLoader cl = new ClassLoader(Thread.currentThread().getContextClassLoader()) {
+      @Override
+      public URL getResource(String name) {
+        if (name.equals("/foo.txt")) {
+          used.set(true);
+          return url;
+        }
+        return super.getResource(name);
+      }
+    };
+    vertx.deployVerticle(new AbstractVerticle() {
+      @Override
+      public void start() {
+        assertSame(cl, Thread.currentThread().getContextClassLoader());
+        assertSame(cl, ((ContextInternal)context).classLoader());
+        vertx.fileSystem().props("/foo.txt", onSuccess(props -> {
+          assertEquals(5, props.size());
+          assertTrue(used.get());
+          vertx.undeploy(context.deploymentID(), onSuccess(v -> {
+              testComplete();
+          }));
+        }));
+      }
+    }, new DeploymentOptions().setClassLoader(cl), onSuccess(id -> {
+    }));
+    await();
+  }
+
   private void assertDeployment(int instances, MyVerticle verticle, JsonObject config, AsyncResult<String> ar) {
     assertTrue(ar.succeeded());
     assertEquals(vertx, verticle.getVertx());
diff --git a/src/test/java/io/vertx/core/VerticleFactoryTest.java b/src/test/java/io/vertx/core/VerticleFactoryTest.java
--- a/src/test/java/io/vertx/core/VerticleFactoryTest.java
+++ b/src/test/java/io/vertx/core/VerticleFactoryTest.java
@@ -16,6 +16,7 @@
 import org.junit.Test;
 
 import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * @author <a href="http://tfox.org">Tim Fox</a>
@@ -385,4 +386,28 @@ public void stop() throws Exception {
 
     }
   }
-}
+
+  @Test
+  public void testClassLoader() {
+    ClassLoader loader = new ClassLoader(Thread.currentThread().getContextClassLoader()) {
+    };
+    AtomicReference<ClassLoader> createClassLoader = new AtomicReference<>();
+    VerticleFactory factory = new VerticleFactory() {
+      @Override
+      public String prefix() {
+        return "test";
+      }
+      @Override
+      public void createVerticle(String verticleName, ClassLoader classLoader, Promise<Callable<Verticle>> promise) {
+        createClassLoader.set(classLoader);
+        promise.complete(() -> new AbstractVerticle() {
+        });
+      }
+    };
+    vertx.registerVerticleFactory(factory);
+    vertx.deployVerticle("test:foo", new DeploymentOptions().setClassLoader(loader), onSuccess(id -> {
+      assertSame(loader, createClassLoader.get());
+      testComplete();
+    }));
+    await();
+  }}