Initial commit, tests failing
diff --git a/modules/vfs-class-loader/.gitignore b/modules/vfs-class-loader/.gitignore
new file mode 100644
index 0000000..3d5bdae
--- /dev/null
+++ b/modules/vfs-class-loader/.gitignore
@@ -0,0 +1,32 @@
+# 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.
+
+# Maven ignores
+/target/
+
+# IDE ignores
+/.settings/
+/.project
+/.classpath
+/.pydevproject
+/.idea
+/*.iml
+/*.ipr
+/*.iws
+/nbproject/
+/nbactions.xml
+/nb-configuration.xml
+.vscode/
+.factorypath
diff --git a/modules/vfs-class-loader/LICENSE b/modules/vfs-class-loader/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/modules/vfs-class-loader/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed 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.
diff --git a/modules/vfs-class-loader/README.md b/modules/vfs-class-loader/README.md
new file mode 100644
index 0000000..8b53cdb
--- /dev/null
+++ b/modules/vfs-class-loader/README.md
@@ -0,0 +1,36 @@
+<!--
+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.
+-->
+
+# VFS Reloading ClassLoader
+
+This module contains a [ClassLoader](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ClassLoader.html) implementation that can be used as the [System](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ClassLoader.html#getSystemClassLoader()) ClassLoader.
+
+## Configuration
+
+To use this ClassLoader as the System ClassLoader you must set the JVM system property **java.system.class.loader** to the fully qualified class name (org.apache.accumulo.classloader.vfs.ReloadingVFSClassLoader). This jar and it's dependent jars must be on the **java.class.path**. 
+
+To set the classpath for this ClassLoader you must define the system property **vfs.class.loader.classpath** and set it to locations that are supported by [Apache Commons VFS](http://commons.apache.org/proper/commons-vfs/filesystems.html).
+
+The ClassLoader monitors the classpath for changes at 5 minute intervals. To change this interval define the sytem property **vfs.classpath.monitor.seconds**.
+
+This ClassLoader follows the normal parent delegation model but can be set to load classes and resources first, before checking if the parent classloader can, by setting the system property **vfs.class.loader.delegation** to "post".
+
+Finally, this ClassLoader keeps a local cache of objects pulled from remote systems (via http, etc.). The default location for this cache directory is the value of the system property **java.io.tmpdir**. To change this location set the system property **vfs.cache.dir** to an existing directory.
+
+## Implementation
+
+This ClassLoader maintains a [VFSClassLoader](http://commons.apache.org/proper/commons-vfs/commons-vfs2/apidocs/org/apache/commons/vfs2/impl/VFSClassLoader.html) delegate that references the classpath (as specified by **vfs.class.loader.classpath**). The ReloadingVFSClassLoader implements [FileListener](http://commons.apache.org/proper/commons-vfs/commons-vfs2/apidocs/org/apache/commons/vfs2/FileListener.html) and creates a [DefaultFileMonitor](http://commons.apache.org/proper/commons-vfs/commons-vfs2/apidocs/org/apache/commons/vfs2/impl/DefaultFileMonitor.html) that checks for changes on the classpath at the interval specified by **vfs.classpath.monitor.seconds** and creates a new VFSClassLoader delegate. Future requests to load classes and resources will use this new delegate; the old delegate is no longer referenced (except by the classes it has loaded) and can be garbage collected.
diff --git a/modules/vfs-class-loader/license-header.txt b/modules/vfs-class-loader/license-header.txt
new file mode 100644
index 0000000..60b675e
--- /dev/null
+++ b/modules/vfs-class-loader/license-header.txt
@@ -0,0 +1,16 @@
+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.
diff --git a/modules/vfs-class-loader/pom.xml b/modules/vfs-class-loader/pom.xml
new file mode 100644
index 0000000..e727d2f
--- /dev/null
+++ b/modules/vfs-class-loader/pom.xml
@@ -0,0 +1,198 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    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.
+
+-->
+<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">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.accumulo</groupId>
+    <artifactId>classloader-extras</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+  <artifactId>vfs-reloading-classloader</artifactId>
+  <name>VFS Reloading ClassLoader</name>
+  <properties>
+    <eclipseFormatterStyle>../../contrib/Eclipse-Accumulo-Codestyle.xml</eclipseFormatterStyle>
+  </properties>
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-vfs2</artifactId>
+      <version>2.6.0</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-1.2-api</artifactId>
+      <version>2.13.1</version>
+    </dependency>
+    <dependency>
+      <groupId>com.google.code.gson</groupId>
+      <artifactId>gson</artifactId>
+      <version>2.8.6</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+      <version>2.7</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>commons-logging</groupId>
+      <artifactId>commons-logging</artifactId>
+      <version>1.2</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.accumulo</groupId>
+      <artifactId>accumulo-core</artifactId>
+      <version>2.1.0-SNAPSHOT</version>
+      <scope>provided</scope>
+      <exclusions>
+        <exclusion>
+          <groupId>org.apache.accumulo</groupId>
+          <artifactId>accumulo-start</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-client-api</artifactId>
+      <version>3.2.1</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+      <version>1.7.30</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.13</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.accumulo</groupId>
+      <artifactId>accumulo-start</artifactId>
+      <version>2.1.0-SNAPSHOT</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-client-minicluster</artifactId>
+      <version>3.2.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-slf4j-impl</artifactId>
+      <version>2.13.1</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+  <build>
+    <pluginManagement>
+      <plugins>
+        <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
+        <plugin>
+          <groupId>org.eclipse.m2e</groupId>
+          <artifactId>lifecycle-mapping</artifactId>
+          <version>1.0.0</version>
+          <configuration>
+            <lifecycleMappingMetadata>
+              <pluginExecutions>
+                <pluginExecution>
+                  <pluginExecutionFilter>
+                    <groupId>org.codehaus.mojo</groupId>
+                    <artifactId>exec-maven-plugin</artifactId>
+                    <versionRange>[3.0.0,)</versionRange>
+                    <goals>
+                      <goal>exec</goal>
+                    </goals>
+                  </pluginExecutionFilter>
+                  <action>
+                    <ignore />
+                  </action>
+                </pluginExecution>
+              </pluginExecutions>
+            </lifecycleMappingMetadata>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <plugins>
+      <plugin>
+        <groupId>com.mycila</groupId>
+        <artifactId>license-maven-plugin</artifactId>
+        <version>3.0</version>
+        <configuration>
+          <header>${project.basedir}/license-header.txt</header>
+          <excludes combine.children="append">
+            <exclude>**/DEPENDENCIES</exclude>
+            <exclude>**/LICENSE</exclude>
+            <exclude>**/NOTICE</exclude>
+            <exclude>**/target/**</exclude>
+            <exclude>contrib/javadoc11.patch</exclude>
+          </excludes>
+          <mapping combine.children="append">
+            <!-- general mappings; module-specific mappings appear in their respective pom -->
+            <Makefile>SCRIPT_STYLE</Makefile>
+            <c>SLASHSTAR_STYLE</c>
+            <cc>SLASHSTAR_STYLE</cc>
+            <css>SLASHSTAR_STYLE</css>
+            <h>SLASHSTAR_STYLE</h>
+            <java>SLASHSTAR_STYLE</java>
+            <proto>SLASHSTAR_STYLE</proto>
+            <thrift>SLASHSTAR_STYLE</thrift>
+          </mapping>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>exec-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>Build Test jars</id>
+            <goals>
+              <goal>exec</goal>
+            </goals>
+            <phase>process-test-classes</phase>
+            <configuration>
+              <executable>${project.basedir}/src/test/shell/makeTestJars.sh</executable>
+            </configuration>
+          </execution>
+          <execution>
+            <id>Build HelloWorld jars</id>
+            <goals>
+              <goal>exec</goal>
+            </goals>
+            <phase>process-test-classes</phase>
+            <configuration>
+              <executable>${project.basedir}/src/test/shell/makeHelloWorldJars.sh</executable>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/ClassPathPrinter.java b/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/ClassPathPrinter.java
new file mode 100644
index 0000000..7112d25
--- /dev/null
+++ b/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/ClassPathPrinter.java
@@ -0,0 +1,132 @@
+/*
+ * 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.accumulo.classloader.vfs;
+
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Collections;
+
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.impl.VFSClassLoader;
+
+public class ClassPathPrinter {
+
+  public interface Printer {
+    void print(String s);
+  }
+
+  public static void printClassPath(ClassLoader cl, boolean debug) {
+    printClassPath(cl, System.out::print, debug);
+  }
+
+  public static String getClassPath(ClassLoader cl, boolean debug) {
+    StringBuilder cp = new StringBuilder();
+    printClassPath(cl, cp::append, debug);
+    return cp.toString();
+  }
+
+  private static void printJar(Printer out, String jarPath, boolean debug, boolean sawFirst) {
+    if (debug) {
+      out.print("\t");
+    }
+    if (!debug && sawFirst) {
+      out.print(":");
+    }
+    out.print(jarPath);
+    if (debug) {
+      out.print("\n");
+    }
+  }
+
+  public static void printClassPath(ClassLoader cl, Printer out, boolean debug) {
+    try {
+      ArrayList<ClassLoader> classloaders = new ArrayList<>();
+
+      while (cl != null) {
+        classloaders.add(cl);
+        cl = cl.getParent();
+      }
+
+      Collections.reverse(classloaders);
+
+      int level = 0;
+
+      for (ClassLoader classLoader : classloaders) {
+
+        level++;
+
+        if (debug && level > 1) {
+          out.print("\n");
+        }
+        if (!debug && level < 2) {
+          continue;
+        }
+
+        boolean sawFirst = false;
+        if (classLoader.getClass().getName().startsWith("jdk.internal")) {
+          if (debug) {
+            out.print("Level " + level + ": " + classLoader.getClass().getName()
+                + " configuration not inspectable.\n");
+          }
+        } else if (classLoader instanceof URLClassLoader) {
+          if (debug) {
+            out.print("Level " + level + ": URL classpath, items are:\n");
+          }
+          for (URL u : ((URLClassLoader) classLoader).getURLs()) {
+            printJar(out, u.getFile(), debug, sawFirst);
+            sawFirst = true;
+          }
+        } else if (classLoader instanceof ReloadingVFSClassLoader) {
+          if (debug) {
+            out.print("Level " + level + ": ReloadingVFSClassLoader, classpath items are:\n");
+          }
+          @SuppressWarnings("resource")
+          ReloadingVFSClassLoader vcl = (ReloadingVFSClassLoader) classLoader;
+          ClassLoader delegate = vcl.getDelegateClassLoader();
+          if (delegate instanceof VFSClassLoaderWrapper) {
+            VFSClassLoaderWrapper wrapper = (VFSClassLoaderWrapper) delegate;
+            for (FileObject f : wrapper.getFileObjects()) {
+              printJar(out, f.getURL().getFile(), debug, sawFirst);
+              sawFirst = true;
+            }
+          }
+        } else if (classLoader instanceof VFSClassLoader) {
+          if (debug) {
+            out.print("Level " + level + ": VFSClassLoader, classpath items are:\n");
+          }
+          VFSClassLoader vcl = (VFSClassLoader) classLoader;
+          for (FileObject f : vcl.getFileObjects()) {
+            printJar(out, f.getURL().getFile(), debug, sawFirst);
+            sawFirst = true;
+          }
+        } else {
+          if (debug) {
+            out.print("Level " + level + ": Unknown classloader configuration "
+                + classLoader.getClass() + "\n");
+          }
+        }
+      }
+      out.print("\n");
+    } catch (Throwable t) {
+      throw new RuntimeException(t);
+    }
+  }
+
+}
diff --git a/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/ReloadingVFSClassLoader.java b/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/ReloadingVFSClassLoader.java
new file mode 100644
index 0000000..552076a
--- /dev/null
+++ b/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/ReloadingVFSClassLoader.java
@@ -0,0 +1,724 @@
+/*
+ * 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.accumulo.classloader.vfs;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+import org.apache.commons.vfs2.FileChangeEvent;
+import org.apache.commons.vfs2.FileListener;
+import org.apache.commons.vfs2.FileMonitor;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.impl.DefaultFileMonitor;
+import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
+import org.apache.commons.vfs2.provider.hdfs.HdfsFileObject;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.FileSystem;
+
+/**
+ * <p>
+ * A {@code ClassLoader} implementation that watches for changes in any of the files/directories in
+ * the classpath. When a change is noticed, this classloader will then load the new classes in
+ * subsequent calls to loadClass. This classloader supports both the normal classloader
+ * pre-delegation model and a post-delegation model. To enable the post-delegation feature set the
+ * system property <b>vfs.class.loader.delegation</b> to "post".
+ *
+ * <p>
+ * This classloader uses the following system properties:
+ *
+ * <ol>
+ * <li><b>vfs.cache.dir</b> - for specifying the directory to use for the local VFS cache (default
+ * is the system property <b>java.io.tmpdir</b></li>
+ * <li><b>vfs.classpath.monitor.seconds</b> - for specifying the file system monitor (default:
+ * 5m)</li>
+ * <li><b>vfs.class.loader.classpath</b> - for specifying the class path</li>
+ * <li><b>vfs.class.loader.delegation</b> - valid values are "pre" and "post" (default: pre)</li>
+ * </ol>
+ *
+ * <p>
+ * This class will attempt to perform substitution on any environment variables found in the values.
+ * For example, the system property <b>vfs.cache.dir</b> can be set to <b>$HOME/cache</b>.
+ */
+public class ReloadingVFSClassLoader extends ClassLoader implements Closeable, FileListener {
+
+  public static final String VFS_CLASSPATH_MONITOR_INTERVAL = "vfs.classpath.monitor.seconds";
+  public static final String VFS_CACHE_DIR_PROPERTY = "vfs.cache.dir";
+  public static final String VFS_CLASSLOADER_CLASSPATH = "vfs.class.loader.classpath";
+  public static final String VFS_CLASSLOADER_DELEGATION = "vfs.class.loader.delegation";
+  public static final String VFS_CLASSLOADER_DEBUG = "vfs.class.loader.debug";
+
+  private static final String VFS_CACHE_DIR_DEFAULT = "java.io.tmpdir";
+
+  // set to 5 mins. The rationale behind this large time is to avoid a gazillion tservers all asking
+  // the name node for info too frequently.
+  private static final long DEFAULT_TIMEOUT = TimeUnit.MINUTES.toMillis(5);
+
+  private static boolean DEBUG = false;
+  private static String CLASSPATH = null;
+  private static Boolean PRE_DELEGATION = null;
+  private static Long MONITOR_INTERVAL = null;
+  private static boolean VM_INITIALIZED = false;
+
+  private volatile long maxWaitInterval = 60000;
+  private volatile long maxRetries = -1;
+  private volatile long sleepInterval = 1000;
+  private volatile boolean vfsInitializing = false;
+
+  private final ThreadPoolExecutor executor;
+  private final ClassLoader parent;
+  private final ReentrantReadWriteLock updateLock = new ReentrantReadWriteLock(true);
+  private final String name;
+  private final String classpath;
+  private final Boolean preDelegation;
+  private final long monitorInterval;
+  private DefaultFileMonitor monitor;
+  private FileObject[] files;
+  private VFSClassLoaderWrapper cl = null;
+  private DefaultFileSystemManager vfs = null;
+
+  static {
+    DEBUG = Boolean.parseBoolean(System.getProperty(VFS_CLASSLOADER_DEBUG, "false"));
+    CLASSPATH = getClassPathProperty();
+    PRE_DELEGATION = getPreDelegationModelProperty();
+    MONITOR_INTERVAL = getMonitorIntervalProperty();
+  }
+
+  private static void printDebug(String msg) {
+    if (!DEBUG)
+      return;
+    System.out
+        .println(String.format("%d ReloadingVFSClassLoader: %s", System.currentTimeMillis(), msg));
+  }
+
+  private static void printError(String msg) {
+    System.err
+        .println(String.format("%d ReloadingVFSClassLoader: %s", System.currentTimeMillis(), msg));
+  }
+
+  /**
+   * Get the classpath value from the environment and resolve embedded env vars
+   *
+   * @return classpath value
+   */
+  private static String getClassPathProperty() {
+    String cp = System.getProperty(VFS_CLASSLOADER_CLASSPATH);
+    if (null == cp || cp.isBlank()) {
+      printError(VFS_CLASSLOADER_CLASSPATH + " system property not set, using default of \"\"");
+      cp = "";
+    }
+    String result = replaceEnvVars(cp, System.getenv());
+    printDebug("Classpath set to: " + result);
+    return result;
+  }
+
+  /**
+   * Get the delegation model
+   *
+   * @return true if pre delegaion, false if post delegation
+   */
+  private static boolean getPreDelegationModelProperty() {
+    String delegation = System.getProperty(VFS_CLASSLOADER_DELEGATION);
+    boolean preDelegation = true;
+    if (null != delegation && delegation.equalsIgnoreCase("post")) {
+      preDelegation = false;
+    }
+    printDebug("ClassLoader configured for pre-delegation: " + preDelegation);
+    return preDelegation;
+  }
+
+  /**
+   * Get the directory for the VFS cache
+   *
+   * @return VFS cache directory
+   */
+  static String getVFSCacheDir() {
+    // Get configuration properties from the environment variables
+    String vfsCacheDir = System.getProperty(VFS_CACHE_DIR_PROPERTY);
+    if (null == vfsCacheDir || vfsCacheDir.isBlank()) {
+      printError(VFS_CACHE_DIR_PROPERTY + " system property not set, using default of "
+          + VFS_CACHE_DIR_DEFAULT);
+      vfsCacheDir = System.getProperty(VFS_CACHE_DIR_DEFAULT);
+    }
+    String cache = replaceEnvVars(vfsCacheDir, System.getenv());
+    printDebug("VFS Cache Dir set to: " + cache);
+    return cache;
+  }
+
+  /**
+   * Replace environment variables in the string with their actual value
+   */
+  public static String replaceEnvVars(String classpath, Map<String,String> env) {
+    Pattern envPat = Pattern.compile("\\$[A-Za-z][a-zA-Z0-9_]*");
+    Matcher envMatcher = envPat.matcher(classpath);
+    while (envMatcher.find(0)) {
+      // name comes after the '$'
+      String varName = envMatcher.group().substring(1);
+      String varValue = env.get(varName);
+      if (varValue == null) {
+        varValue = "";
+      }
+      classpath = (classpath.substring(0, envMatcher.start()) + varValue
+          + classpath.substring(envMatcher.end()));
+      envMatcher.reset(classpath);
+    }
+    return classpath;
+  }
+
+  /**
+   * Get the file system monitor interval
+   *
+   * @return monitor interval in ms
+   */
+  private static long getMonitorIntervalProperty() {
+    String interval = System.getProperty(VFS_CLASSPATH_MONITOR_INTERVAL);
+    if (null != interval && !interval.isBlank()) {
+      try {
+        return TimeUnit.SECONDS.toMillis(Long.parseLong(interval));
+      } catch (NumberFormatException e) {
+        printError(VFS_CLASSPATH_MONITOR_INTERVAL + " system property not set, using default of "
+            + DEFAULT_TIMEOUT);
+        return DEFAULT_TIMEOUT;
+      }
+    }
+    return DEFAULT_TIMEOUT;
+  }
+
+  /**
+   * This task replaces the delegate classloader with a new instance when the filesystem has
+   * changed. This will orphan the old classloader and the only references to the old classloader
+   * are from the objects that it loaded.
+   */
+  private final Runnable refresher = new Runnable() {
+    @Override
+    public void run() {
+      while (!executor.isTerminating()) {
+        try {
+          printDebug("Recreating delegate classloader due to filesystem change event");
+          updateDelegateClassloader();
+          return;
+        } catch (Exception e) {
+          e.printStackTrace();
+          try {
+            Thread.sleep(getMonitorInterval());
+          } catch (InterruptedException ie) {
+            ie.printStackTrace();
+          }
+        }
+      }
+    }
+  };
+
+  public ReloadingVFSClassLoader(ClassLoader parent) {
+    super(ReloadingVFSClassLoader.class.getSimpleName(), parent);
+    printDebug("Parent ClassLoader: " + parent.getClass().getName());
+    this.name = ReloadingVFSClassLoader.class.getSimpleName();
+    this.parent = parent;
+    this.classpath = CLASSPATH;
+    this.preDelegation = PRE_DELEGATION;
+    this.monitorInterval = MONITOR_INTERVAL;
+
+    BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(2);
+    ThreadFactory factory = r -> {
+      Thread t = new Thread(r);
+      t.setDaemon(true);
+      return t;
+    };
+    executor = new ThreadPoolExecutor(1, 1, 1, SECONDS, queue, factory);
+  }
+
+  protected DefaultFileSystemManager getFileSystem() {
+    if (null == this.vfs) {
+      if (DEBUG) {
+        VFSManager.enableDebug();
+      }
+      try {
+        this.vfs = VFSManager.generateVfs();
+      } catch (FileSystemException e) {
+        printError("Error creating FileSystem: " + e.getMessage());
+        e.printStackTrace();
+      }
+      printDebug("VFS File System created.");
+    }
+    return this.vfs;
+  }
+
+  protected String getClassPath() {
+    return this.classpath;
+  }
+
+  protected boolean isPreDelegationModel() {
+    return this.preDelegation;
+  }
+
+  protected long getMonitorInterval() {
+    return this.monitorInterval;
+  }
+
+  private synchronized FileMonitor getFileMonitor() {
+    if (null == this.monitor) {
+      this.monitor = new DefaultFileMonitor(this);
+      monitor.setDelay(getMonitorInterval());
+      monitor.setRecursive(false);
+      monitor.start();
+      printDebug("Monitor started with interval set to: " + monitor.getDelay());
+    }
+    return this.monitor;
+  }
+
+  private void addFileToMonitor(FileObject file) throws RuntimeException {
+    try {
+      getFileMonitor().addFile(file);
+    } catch (RuntimeException re) {
+      if (re.getMessage().contains("files-cache"))
+        printDebug("files-cache error adding " + file.toString() + " to VFS monitor. "
+            + "There is no implementation for files-cache in VFS2");
+      else
+        printDebug("Runtime error adding " + file.toString() + " to VFS monitor");
+
+      re.printStackTrace();
+
+      throw re;
+    }
+  }
+
+  private synchronized void updateDelegateClassloader() throws Exception {
+    try {
+      updateLock.writeLock().lock();
+      // Re-resolve the files on the classpath, things may have changed.
+      long retries = 0;
+      long currentSleepMillis = sleepInterval;
+      FileObject[] classpathFiles = VFSManager.resolve(getFileSystem(), this.getClassPath());
+      if (classpathFiles.length == 0) {
+        while (classpathFiles.length == 0 && retryPermitted(retries)) {
+          try {
+            printDebug("VFS path was empty.  Waiting " + currentSleepMillis + " ms to retry");
+            Thread.sleep(currentSleepMillis);
+            classpathFiles = VFSManager.resolve(getFileSystem(), this.getClassPath());
+            retries++;
+            currentSleepMillis = Math.min(maxWaitInterval, currentSleepMillis + sleepInterval);
+          } catch (InterruptedException e) {
+            printError("VFS Retry Interruped");
+            e.printStackTrace();
+            throw new RuntimeException(e);
+          }
+        }
+      }
+      if (classpathFiles.length == 0) {
+        printError("ReloadingVFSClassLoader has no resources on classpath");
+      }
+      this.files = classpathFiles;
+      // There is a chance that the listener was removed from the top level directory or
+      // its children if they were deleted within some time window. Re-add files to be
+      // monitored. The Monitor will ignore files that are already/still being monitored.
+      // forEachCatchRTEs will capture a stream of thrown exceptions.
+      // and can collect them to list or reduce into one exception
+      forEachCatchRTEs(Arrays.stream(this.files), f -> {
+        addFileToMonitor(f);
+        printDebug("monitoring: " + f.toString());
+      });
+      // Create the new classloader delegate
+      printDebug("Rebuilding dynamic classloader using files: " + stringify(this.files));
+      VFSClassLoaderWrapper cl;
+      if (this.isPreDelegationModel()) {
+        // This is the normal classloader parent delegation model
+        cl = new VFSClassLoaderWrapper(this.files, getFileSystem(), parent);
+      } else {
+        // This delegates to the parent after we lookup locally first.
+        cl = new VFSClassLoaderWrapper(this.files, getFileSystem()) {
+          @Override
+          public synchronized Class<?> loadClass(String name, boolean resolve)
+              throws ClassNotFoundException {
+            Class<?> c = findLoadedClass(name);
+            if (c != null)
+              return c;
+            try {
+              // try finding this class here instead of parent
+              return findClass(name);
+            } catch (ClassNotFoundException e) {
+
+            }
+            return super.loadClass(name, resolve);
+          }
+        };
+      }
+      // An HDFS FileSystem and Configuration object were created for each unique HDFS namespace
+      // in the call to resolve above. The HDFS Client did us a favor and cached these objects
+      // so that the next time someone calls FileSystem.get(uri), they get the cached object.
+      // However, these objects were created not with the VFS classloader, but the
+      // classloader above it. We need to override the classloader on the Configuration objects.
+      // Ran into an issue were log recovery was being attempted and SequenceFile$Reader was
+      // trying to instantiate the key class via WritableName.getClass(String, Configuration)
+      printDebug("Setting ClassLoader on HDFS FileSystem objects");
+      for (FileObject fo : this.files) {
+        if (fo instanceof HdfsFileObject) {
+          String uri = fo.getName().getRootURI();
+          Configuration c = new Configuration(true);
+          c.set(FileSystem.FS_DEFAULT_NAME_KEY, uri);
+          try {
+            FileSystem fs = FileSystem.get(c);
+            fs.getConf().setClassLoader(cl);
+          } catch (IOException e) {
+            throw new RuntimeException("Error setting classloader on HDFS FileSystem object", e);
+          }
+        }
+      }
+
+      // Update the delegate reference to the new classloader
+      this.cl = cl;
+      printDebug("ReloadingVFSClassLoader set.");
+    } finally {
+      updateLock.writeLock().unlock();
+    }
+  }
+
+  /**
+   * Remove the file from the monitor
+   * 
+   * @param file
+   *          to remove
+   * @throws RuntimeException
+   *           if error
+   */
+  private void removeFile(FileObject file) throws RuntimeException {
+    try {
+      getFileMonitor().removeFile(file);
+    } catch (RuntimeException re) {
+      printError("Error removing file from VFS cache: " + file.toString());
+      re.printStackTrace();
+      throw re;
+    }
+  }
+
+  @Override
+  public void fileCreated(FileChangeEvent event) throws Exception {
+    printDebug(event.getFileObject().getURL().toString() + " created, recreating classloader");
+    scheduleRefresh();
+  }
+
+  @Override
+  public void fileDeleted(FileChangeEvent event) throws Exception {
+    printDebug(event.getFileObject().getURL().toString() + " deleted, recreating classloader");
+    scheduleRefresh();
+  }
+
+  @Override
+  public void fileChanged(FileChangeEvent event) throws Exception {
+    printDebug(event.getFileObject().getURL().toString() + " changed, recreating classloader");
+    scheduleRefresh();
+  }
+
+  private void scheduleRefresh() {
+    try {
+      executor.execute(refresher);
+    } catch (RejectedExecutionException e) {
+      printDebug("Ignoring refresh request (already refreshing)");
+    }
+  }
+
+  @Override
+  public void close() {
+
+    forEachCatchRTEs(Stream.of(this.files), f -> {
+      removeFile(f);
+      printDebug("Closing, removing file from monitoring: " + f.toString());
+    });
+
+    this.executor.shutdownNow();
+    this.monitor.stop();
+    if (null != this.vfs)
+      VFSManager.returnVfs(this.vfs);
+    vfs = null;
+  }
+
+  public static <T> void forEachCatchRTEs(Stream<T> stream, Consumer<T> consumer) {
+    stream.flatMap(o -> {
+      try {
+        consumer.accept(o);
+        return null;
+      } catch (RuntimeException e) {
+        return Stream.of(e);
+      }
+    }).reduce((e1, e2) -> {
+      e1.addSuppressed(e2);
+      return e1;
+    }).ifPresent(e -> {
+      throw e;
+    });
+  }
+
+  private boolean retryPermitted(long retries) {
+    return (this.maxRetries < 0 || retries < this.maxRetries);
+  }
+
+  public String stringify(FileObject[] files) {
+    StringBuilder sb = new StringBuilder();
+    sb.append('[');
+    String delim = "";
+    for (FileObject file : files) {
+      sb.append(delim);
+      delim = ", ";
+      sb.append(file.getName());
+    }
+    sb.append(']');
+    return sb.toString();
+  }
+
+  /**
+   * Return a reference to the delegate classloader, create a new one if necessary
+   * 
+   * @return reference to delegate classloader
+   */
+  synchronized ClassLoader getDelegateClassLoader() {
+    // We cannot create the VFS file system during VM initialization,
+    // we have to perform some lazy initialization here due to the fact
+    // that the logging libraries (and others) make use of the ServiceLoader
+    // and call ClassLoader.getSystemClassLoader() which you can't do until
+    // the VM is fully initialized.
+    if (!isVMInitialized() || vfsInitializing) {
+      return this.parent;
+    } else if (null == this.vfs) {
+      this.vfsInitializing = true;
+      printDebug("getDelegateClassLoader() initializing VFS.");
+      getFileSystem();
+      if (null == getFileSystem()) {
+        // Some error happened
+        throw new RuntimeException("Problem creating VFS file system");
+      }
+      printDebug("getDelegateClassLoader() VFS initialized.");
+    }
+    if (null == this.cl) {
+      try {
+        printDebug("Creating initial delegate class loader");
+        updateDelegateClassloader();
+      } catch (Exception e) {
+        e.printStackTrace();
+        throw new RuntimeException("Error creating initial delegate classloader", e);
+      }
+    }
+    if (this.vfsInitializing) {
+      this.vfsInitializing = false;
+      printDebug(ClassPathPrinter.getClassPath(this, true));
+    }
+    try {
+      updateLock.readLock().lock();
+      return this.cl;
+    } finally {
+      updateLock.readLock().unlock();
+    }
+  }
+
+  @Override
+  public Class<?> findClass(String name) throws ClassNotFoundException {
+    ClassLoader d = getDelegateClassLoader();
+    if (d instanceof VFSClassLoaderWrapper) {
+      return ((VFSClassLoaderWrapper) d).findClass(name);
+    } else {
+      return null;
+    }
+  }
+
+  @Override
+  public URL findResource(String name) {
+    ClassLoader d = getDelegateClassLoader();
+    if (d instanceof VFSClassLoaderWrapper) {
+      return ((VFSClassLoaderWrapper) d).findResource(name);
+    } else {
+      return null;
+    }
+  }
+
+  @Override
+  public Enumeration<URL> findResources(String name) throws IOException {
+    ClassLoader d = getDelegateClassLoader();
+    if (d instanceof VFSClassLoaderWrapper) {
+      return ((VFSClassLoaderWrapper) d).findResources(name);
+    } else {
+      return null;
+    }
+  }
+
+  @Override
+  public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+    ClassLoader d = getDelegateClassLoader();
+    if (d instanceof VFSClassLoaderWrapper) {
+      return ((VFSClassLoaderWrapper) d).loadClass(name, resolve);
+    } else {
+      return null;
+    }
+  }
+
+  @Override
+  public String getName() {
+    return name;
+  }
+
+  private boolean isVMInitialized() {
+    if (VM_INITIALIZED) {
+      return VM_INITIALIZED;
+    } else {
+      // We can't call VM.isBooted() directly, but we know from System.initPhase3() that
+      // when this classloader is set via 'java.system.class.loader' that it will be initialized,
+      // then set as the Thread context classloader, then the VM is fully initialized.
+      try {
+        printDebug(
+            "System ClassLoader: " + ClassLoader.getSystemClassLoader().getClass().getName());
+        VM_INITIALIZED = ClassLoader.getSystemClassLoader().equals(this);
+      } catch (IllegalStateException e) {
+        // VM is still initializing
+        VM_INITIALIZED = false;
+      }
+      printDebug("VM Initialized: " + VM_INITIALIZED);
+      return VM_INITIALIZED;
+    }
+  }
+
+  @Override
+  public Class<?> loadClass(String name) throws ClassNotFoundException {
+    return getDelegateClassLoader().loadClass(name);
+  }
+
+  @Override
+  public URL getResource(String name) {
+    return getDelegateClassLoader().getResource(name);
+  }
+
+  @Override
+  public Enumeration<URL> getResources(String name) throws IOException {
+    return getDelegateClassLoader().getResources(name);
+  }
+
+  @Override
+  public Stream<URL> resources(String name) {
+    return getDelegateClassLoader().resources(name);
+  }
+
+  @Override
+  public InputStream getResourceAsStream(String name) {
+    return getDelegateClassLoader().getResourceAsStream(name);
+  }
+
+  @Override
+  public void setDefaultAssertionStatus(boolean enabled) {
+    getDelegateClassLoader().setDefaultAssertionStatus(enabled);
+  }
+
+  @Override
+  public void setPackageAssertionStatus(String packageName, boolean enabled) {
+    getDelegateClassLoader().setPackageAssertionStatus(packageName, enabled);
+  }
+
+  @Override
+  public void setClassAssertionStatus(String className, boolean enabled) {
+    getDelegateClassLoader().setClassAssertionStatus(className, enabled);
+  }
+
+  @Override
+  public void clearAssertionStatus() {
+    getDelegateClassLoader().clearAssertionStatus();
+  }
+
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = 1;
+    result = prime * result + ((name == null) ? 0 : name.hashCode());
+    result = prime * result + ((parent.getName() == null) ? 0 : parent.getName().hashCode());
+    return result;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj)
+      return true;
+    if (obj == null)
+      return false;
+    if (getClass() != obj.getClass())
+      return false;
+    ReloadingVFSClassLoader other = (ReloadingVFSClassLoader) obj;
+    if (name == null) {
+      if (other.name != null)
+        return false;
+    } else if (!name.equals(other.name))
+      return false;
+    if (parent == null) {
+      if (other.parent != null)
+        return false;
+    } else if (!parent.getName().equals(other.parent.getName()))
+      return false;
+    return true;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+
+    if (null != this.files) {
+      for (FileObject f : files) {
+        try {
+          buf.append("\t").append(f.getURL()).append("\n");
+        } catch (FileSystemException e) {
+          printError("Error getting URL for file: " + f.toString());
+          e.printStackTrace();
+        }
+      }
+    }
+    return buf.toString();
+  }
+
+  // VisibleForTesting intentionally not using annotation from Guava
+  // because it adds unwanted dependency
+  void setMaxRetries(long maxRetries) {
+    this.maxRetries = maxRetries;
+  }
+
+  // VisibleForTesting intentionally not using annotation from Guava
+  // because it adds unwanted dependency
+  void setVMInitializedForTests() {
+    VM_INITIALIZED = true;
+  }
+
+  // VisibleForTesting intentionally not using annotation from Guava
+  // because it adds unwanted dependency
+  void setVFSForTests(DefaultFileSystemManager vfs) {
+    this.vfs = vfs;
+  }
+
+  void enableDebugForTests() {
+    DEBUG = true;
+  }
+}
diff --git a/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/UniqueFileReplicator.java b/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/UniqueFileReplicator.java
new file mode 100644
index 0000000..abef05a
--- /dev/null
+++ b/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/UniqueFileReplicator.java
@@ -0,0 +1,105 @@
+/*
+ * 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.accumulo.classloader.vfs;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSelector;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.provider.FileReplicator;
+import org.apache.commons.vfs2.provider.UriParser;
+import org.apache.commons.vfs2.provider.VfsComponent;
+import org.apache.commons.vfs2.provider.VfsComponentContext;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+public class UniqueFileReplicator implements VfsComponent, FileReplicator {
+
+  private static final char[] TMP_RESERVED_CHARS =
+      {'?', '/', '\\', ' ', '&', '"', '\'', '*', '#', ';', ':', '<', '>', '|'};
+
+  private File tempDir;
+  private VfsComponentContext context;
+  private List<File> tmpFiles = Collections.synchronizedList(new ArrayList<>());
+
+  public UniqueFileReplicator(File tempDir) {
+    this.tempDir = tempDir;
+    if (!tempDir.exists() && !tempDir.mkdirs())
+      System.out.println("Unexpected error creating directory: " + tempDir.getAbsolutePath());
+  }
+
+  @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN",
+      justification = "input files are specified by admin, not unchecked user input")
+  @Override
+  public File replicateFile(FileObject srcFile, FileSelector selector) throws FileSystemException {
+    String baseName = srcFile.getName().getBaseName();
+
+    try {
+      String safeBasename = UriParser.encode(baseName, TMP_RESERVED_CHARS).replace('%', '_');
+      File file = File.createTempFile("vfsr_", "_" + safeBasename, tempDir);
+      file.deleteOnExit();
+
+      final FileObject destFile = context.toFileObject(file);
+      destFile.copyFrom(srcFile, selector);
+
+      return file;
+    } catch (IOException e) {
+      throw new FileSystemException(e);
+    }
+  }
+
+  @Override
+  public void setLogger(Log logger) {
+    // TODO Auto-generated method stub
+
+  }
+
+  @Override
+  public void setContext(VfsComponentContext context) {
+    this.context = context;
+  }
+
+  @Override
+  public void init() throws FileSystemException {
+
+  }
+
+  @Override
+  public void close() {
+    synchronized (tmpFiles) {
+      for (File tmpFile : tmpFiles) {
+        if (!tmpFile.delete())
+          System.out.println("File does not exist: " + tmpFile.getAbsolutePath());
+      }
+    }
+
+    if (tempDir.exists()) {
+      String[] list = tempDir.list();
+      int numChildren = list == null ? 0 : list.length;
+      if (numChildren == 0 && !tempDir.delete())
+        System.out.println("Cannot delete empty directory: " + tempDir.getAbsolutePath());
+    }
+  }
+}
diff --git a/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/VFSClassLoaderWrapper.java b/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/VFSClassLoaderWrapper.java
new file mode 100644
index 0000000..7f17684
--- /dev/null
+++ b/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/VFSClassLoaderWrapper.java
@@ -0,0 +1,83 @@
+/*
+ * 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.accumulo.classloader.vfs;
+
+import java.io.IOException;
+import java.net.URL;
+import java.security.CodeSource;
+import java.security.PermissionCollection;
+import java.util.Enumeration;
+
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.impl.VFSClassLoader;
+
+/**
+ * This class exists to expose methods that are protected in the parent class so that we can use
+ * this in a delegate pattern
+ */
+public class VFSClassLoaderWrapper extends VFSClassLoader {
+
+  public VFSClassLoaderWrapper(FileObject file, FileSystemManager manager, ClassLoader parent)
+      throws FileSystemException {
+    super(file, manager, parent);
+  }
+
+  public VFSClassLoaderWrapper(FileObject file, FileSystemManager manager)
+      throws FileSystemException {
+    super(file, manager);
+  }
+
+  public VFSClassLoaderWrapper(FileObject[] files, FileSystemManager manager, ClassLoader parent)
+      throws FileSystemException {
+    super(files, manager, parent);
+  }
+
+  public VFSClassLoaderWrapper(FileObject[] files, FileSystemManager manager)
+      throws FileSystemException {
+    super(files, manager);
+  }
+
+  @Override
+  public Class<?> findClass(String name) throws ClassNotFoundException {
+    return super.findClass(name);
+  }
+
+  @Override
+  public PermissionCollection getPermissions(CodeSource cs) {
+    return super.getPermissions(cs);
+  }
+
+  @Override
+  public URL findResource(String name) {
+    return super.findResource(name);
+  }
+
+  @Override
+  public Enumeration<URL> findResources(String name) throws IOException {
+    return super.findResources(name);
+  }
+
+  @Override
+  public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+    return super.loadClass(name, resolve);
+  }
+
+}
diff --git a/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/VFSManager.java b/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/VFSManager.java
new file mode 100644
index 0000000..ba153d0
--- /dev/null
+++ b/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/VFSManager.java
@@ -0,0 +1,219 @@
+/*
+ * 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.accumulo.classloader.vfs;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.vfs2.CacheStrategy;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.FileType;
+import org.apache.commons.vfs2.cache.SoftRefFilesCache;
+import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
+import org.apache.commons.vfs2.impl.FileContentInfoFilenameFactory;
+import org.apache.commons.vfs2.provider.FileReplicator;
+import org.apache.commons.vfs2.provider.hdfs.HdfsFileProvider;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+public class VFSManager {
+
+  public static class AccumuloVFSManagerShutdownThread implements Runnable {
+
+    @Override
+    public void run() {
+      try {
+        VFSManager.close();
+      } catch (Exception e) {
+        // do nothing, we are shutting down anyway
+      }
+    }
+  }
+
+  private static List<WeakReference<DefaultFileSystemManager>> vfsInstances =
+      Collections.synchronizedList(new ArrayList<>());
+  private static volatile boolean DEBUG = false;
+
+  static void enableDebug() {
+    DEBUG = true;
+  }
+
+  static {
+    // Register the shutdown hook
+    Runtime.getRuntime().addShutdownHook(new Thread(new AccumuloVFSManagerShutdownThread()));
+  }
+
+  public static FileObject[] resolve(FileSystemManager vfs, String uris)
+      throws FileSystemException {
+    return resolve(vfs, uris, new ArrayList<>());
+  }
+
+  static FileObject[] resolve(FileSystemManager vfs, String uris,
+      ArrayList<FileObject> pathsToMonitor) throws FileSystemException {
+    if (uris == null) {
+      return new FileObject[0];
+    }
+
+    ArrayList<FileObject> classpath = new ArrayList<>();
+
+    pathsToMonitor.clear();
+
+    for (String path : uris.split(",")) {
+
+      path = path.trim();
+
+      if (path.equals("")) {
+        continue;
+      }
+
+      path = ReloadingVFSClassLoader.replaceEnvVars(path, System.getenv());
+
+      FileObject fo = vfs.resolveFile(path);
+
+      switch (fo.getType()) {
+        case FILE:
+        case FOLDER:
+          classpath.add(fo);
+          pathsToMonitor.add(fo);
+          break;
+        case IMAGINARY:
+          // assume its a pattern
+          String pattern = fo.getName().getBaseName();
+          if (fo.getParent() != null) {
+            // still monitor the parent
+            pathsToMonitor.add(fo.getParent());
+            if (fo.getParent().getType() == FileType.FOLDER) {
+              FileObject[] children = fo.getParent().getChildren();
+              for (FileObject child : children) {
+                if (child.getType() == FileType.FILE
+                    && child.getName().getBaseName().matches(pattern)) {
+                  classpath.add(child);
+                }
+              }
+            } else {
+              if (DEBUG)
+                System.out.println("classpath entry " + fo.getParent().toString() + " is "
+                    + fo.getParent().getType().toString());
+            }
+          } else {
+            if (DEBUG)
+              System.out.println("ignoring classpath entry: " + fo.toString());
+          }
+          break;
+        default:
+          System.out.println("ignoring classpath entry:  " + fo.toString());
+          break;
+      }
+
+    }
+
+    return classpath.toArray(new FileObject[classpath.size()]);
+  }
+
+  public static DefaultFileSystemManager generateVfs() throws FileSystemException {
+    DefaultFileSystemManager vfs = new DefaultFileSystemManager();
+    vfs.addProvider("res", new org.apache.commons.vfs2.provider.res.ResourceFileProvider());
+    vfs.addProvider("zip", new org.apache.commons.vfs2.provider.zip.ZipFileProvider());
+    vfs.addProvider("gz", new org.apache.commons.vfs2.provider.gzip.GzipFileProvider());
+    vfs.addProvider("ram", new org.apache.commons.vfs2.provider.ram.RamFileProvider());
+    vfs.addProvider("file", new org.apache.commons.vfs2.provider.local.DefaultLocalFileProvider());
+    vfs.addProvider("jar", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
+    vfs.addProvider("http", new org.apache.commons.vfs2.provider.http.HttpFileProvider());
+    vfs.addProvider("https", new org.apache.commons.vfs2.provider.https.HttpsFileProvider());
+    vfs.addProvider("ftp", new org.apache.commons.vfs2.provider.ftp.FtpFileProvider());
+    vfs.addProvider("ftps", new org.apache.commons.vfs2.provider.ftps.FtpsFileProvider());
+    vfs.addProvider("war", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
+    vfs.addProvider("par", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
+    vfs.addProvider("ear", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
+    vfs.addProvider("sar", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
+    vfs.addProvider("ejb3", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
+    vfs.addProvider("tmp", new org.apache.commons.vfs2.provider.temp.TemporaryFileProvider());
+    vfs.addProvider("tar", new org.apache.commons.vfs2.provider.tar.TarFileProvider());
+    vfs.addProvider("tbz2", new org.apache.commons.vfs2.provider.tar.TarFileProvider());
+    vfs.addProvider("tgz", new org.apache.commons.vfs2.provider.tar.TarFileProvider());
+    vfs.addProvider("bz2", new org.apache.commons.vfs2.provider.bzip2.Bzip2FileProvider());
+    vfs.addProvider("hdfs", new HdfsFileProvider());
+    vfs.addExtensionMap("jar", "jar");
+    vfs.addExtensionMap("zip", "zip");
+    vfs.addExtensionMap("gz", "gz");
+    vfs.addExtensionMap("tar", "tar");
+    vfs.addExtensionMap("tbz2", "tar");
+    vfs.addExtensionMap("tgz", "tar");
+    vfs.addExtensionMap("bz2", "bz2");
+    vfs.addMimeTypeMap("application/x-tar", "tar");
+    vfs.addMimeTypeMap("application/x-gzip", "gz");
+    vfs.addMimeTypeMap("application/zip", "zip");
+    vfs.setFileContentInfoFactory(new FileContentInfoFilenameFactory());
+    vfs.setFilesCache(new SoftRefFilesCache());
+    File cacheDir = computeTopCacheDir();
+    vfs.setReplicator(new UniqueFileReplicator(cacheDir));
+    vfs.setCacheStrategy(CacheStrategy.ON_RESOLVE);
+    vfs.init();
+    vfsInstances.add(new WeakReference<>(vfs));
+    return vfs;
+  }
+
+  @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN",
+      justification = "tmpdir is controlled by admin, not unchecked user input")
+  private static File computeTopCacheDir() {
+    String cacheDirPath = ReloadingVFSClassLoader.getVFSCacheDir();
+    String procName = ManagementFactory.getRuntimeMXBean().getName();
+    return new File(cacheDirPath,
+        "accumulo-vfs-manager-cache-" + procName + "-" + System.getProperty("user.name", "nouser"));
+  }
+
+  public static void returnVfs(DefaultFileSystemManager vfs) {
+    if (DEBUG) {
+      System.out.println("Closing VFS instance.");
+    }
+    FileReplicator replicator;
+    try {
+      replicator = vfs.getReplicator();
+      if (replicator instanceof UniqueFileReplicator) {
+        ((UniqueFileReplicator) replicator).close();
+      }
+    } catch (FileSystemException e) {
+      System.err.println("Error occurred closing VFS instance: " + e.getMessage());
+    }
+    vfs.close();
+  }
+
+  public static void close() {
+    for (WeakReference<DefaultFileSystemManager> vfsInstance : vfsInstances) {
+      DefaultFileSystemManager ref = vfsInstance.get();
+      if (ref != null) {
+        returnVfs(ref);
+      }
+    }
+    try {
+      FileUtils.deleteDirectory(computeTopCacheDir());
+    } catch (IOException e) {
+      System.err.println("IOException deleting cache directory");
+      e.printStackTrace();
+    }
+  }
+}
diff --git a/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/context/ReloadingVFSContextClassLoaderFactory.java b/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/context/ReloadingVFSContextClassLoaderFactory.java
new file mode 100644
index 0000000..a4f93f9
--- /dev/null
+++ b/modules/vfs-class-loader/src/main/java/org/apache/accumulo/classloader/vfs/context/ReloadingVFSContextClassLoaderFactory.java
@@ -0,0 +1,271 @@
+/*
+ * 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.accumulo.classloader.vfs.context;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.accumulo.classloader.vfs.ReloadingVFSClassLoader;
+import org.apache.accumulo.core.spi.common.ClassLoaderFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * A ClassLoaderFactory implementation that uses a ReloadingVFSClassLoader per defined context.
+ * Configuration of this class is done with a JSON file whose location is defined by the system
+ * property <b>vfs.context.class.loader.config</b>. To use this ClassLoaderFactory you need to set
+ * the Accumulo configuration property <b>general.context.factory</b> to the fully qualified name of
+ * this class, create a configuration file that defines the supported contexts and their
+ * configuration, and set <b>vfs.context.class.loader.config</b> to the location of the
+ * configuration file.
+ *
+ * <p>
+ * Example configuration file:
+ *
+ * <pre>
+ * {
+ *  "contexts": [
+ *    {
+ *      "name": "cx1",
+ *      "config": {
+ *        "classPath": "file:///tmp/foo",
+ *        "postDelegate": true,
+ *        "monitorIntervalMs": 30000
+ *      }
+ *    },
+ *    {
+ *      "name": "cx2",
+ *      "config": {
+ *        "classPath": "file:///tmp/bar",
+ *        "postDelegate": false,
+ *        "monitorIntervalMs": 30000
+ *      }
+ *    }
+ *  ]
+ * }
+ * </pre>
+ */
+public class ReloadingVFSContextClassLoaderFactory implements ClassLoaderFactory {
+
+  public static class Contexts {
+    List<Context> contexts;
+
+    public List<Context> getContexts() {
+      return contexts;
+    }
+
+    public void setContexts(List<Context> contexts) {
+      this.contexts = contexts;
+    }
+
+    @Override
+    public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + ((contexts == null) ? 0 : contexts.hashCode());
+      return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj)
+        return true;
+      if (obj == null)
+        return false;
+      if (getClass() != obj.getClass())
+        return false;
+      Contexts other = (Contexts) obj;
+      if (contexts == null) {
+        if (other.contexts != null)
+          return false;
+      } else if (!contexts.equals(other.contexts))
+        return false;
+      return true;
+    }
+  }
+
+  public static class Context {
+    private String name;
+    private ContextConfig config;
+
+    public String getName() {
+      return name;
+    }
+
+    public void setName(String name) {
+      this.name = name;
+    }
+
+    public ContextConfig getConfig() {
+      return config;
+    }
+
+    public void setConfig(ContextConfig config) {
+      this.config = config;
+    }
+
+    @Override
+    public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + ((config == null) ? 0 : config.hashCode());
+      result = prime * result + ((name == null) ? 0 : name.hashCode());
+      return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj)
+        return true;
+      if (obj == null)
+        return false;
+      if (getClass() != obj.getClass())
+        return false;
+      Context other = (Context) obj;
+      if (config == null) {
+        if (other.config != null)
+          return false;
+      } else if (!config.equals(other.config))
+        return false;
+      if (name == null) {
+        if (other.name != null)
+          return false;
+      } else if (!name.equals(other.name))
+        return false;
+      return true;
+    }
+  }
+
+  public static class ContextConfig {
+    private String classPath;
+    private boolean postDelegate;
+    private long monitorIntervalMs;
+
+    public String getClassPath() {
+      return classPath;
+    }
+
+    public void setClassPath(String classPath) {
+      this.classPath = ReloadingVFSClassLoader.replaceEnvVars(classPath, System.getenv());
+    }
+
+    public boolean getPostDelegate() {
+      return postDelegate;
+    }
+
+    public void setPostDelegate(boolean postDelegate) {
+      this.postDelegate = postDelegate;
+    }
+
+    public long getMonitorIntervalMs() {
+      return monitorIntervalMs;
+    }
+
+    public void setMonitorIntervalMs(long monitorIntervalMs) {
+      this.monitorIntervalMs = monitorIntervalMs;
+    }
+
+    @Override
+    public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + ((classPath == null) ? 0 : classPath.hashCode());
+      result = prime * result + (int) (monitorIntervalMs ^ (monitorIntervalMs >>> 32));
+      result = prime * result + (postDelegate ? 1231 : 1237);
+      return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj)
+        return true;
+      if (obj == null)
+        return false;
+      if (getClass() != obj.getClass())
+        return false;
+      ContextConfig other = (ContextConfig) obj;
+      if (classPath == null) {
+        if (other.classPath != null)
+          return false;
+      } else if (!classPath.equals(other.classPath))
+        return false;
+      if (monitorIntervalMs != other.monitorIntervalMs)
+        return false;
+      if (postDelegate != other.postDelegate)
+        return false;
+      return true;
+    }
+  }
+
+  public static final String CONFIG_LOCATION = "vfs.context.class.loader.config";
+  private static final Map<String,ReloadingVFSClassLoader> CONTEXTS = new HashMap<>();
+
+  protected String getConfigFileLocation() {
+    String loc = System.getProperty(CONFIG_LOCATION);
+    if (null == loc || loc.isBlank()) {
+      throw new RuntimeException(CONFIG_LOCATION
+          + " system property must be set to use ReloadingVFSContextClassLoaderFactory");
+    }
+    return loc;
+  }
+
+  @Override
+  public void initialize(ClassLoaderFactoryConfiguration conf) throws Exception {
+    // Properties
+    File f = new File(getConfigFileLocation());
+    if (!f.canRead()) {
+      throw new RuntimeException("Unable to read configuration file: " + f.getAbsolutePath());
+    }
+    Gson g = new Gson();
+    Contexts con = g.fromJson(Files.newBufferedReader(f.toPath()), Contexts.class);
+
+    con.getContexts().forEach(c -> {
+      CONTEXTS.put(c.getName(), new ReloadingVFSClassLoader(
+          ReloadingVFSContextClassLoaderFactory.class.getClassLoader()) {
+        @Override
+        protected String getClassPath() {
+          return c.getConfig().getClassPath();
+        }
+
+        @Override
+        protected boolean isPreDelegationModel() {
+          return !(c.getConfig().getPostDelegate());
+        }
+
+        @Override
+        protected long getMonitorInterval() {
+          return c.getConfig().getMonitorIntervalMs();
+        }
+      });
+    });
+  }
+
+  @Override
+  public ClassLoader getClassLoader(String contextName) throws IllegalArgumentException {
+    if (!CONTEXTS.containsKey(contextName)) {
+      throw new IllegalArgumentException(
+          "ReloadingVFSContextClassLoaderFactory not configured for context: " + contextName);
+    }
+    return CONTEXTS.get(contextName);
+  }
+
+}
diff --git a/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/AccumuloDFSBase.java b/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/AccumuloDFSBase.java
new file mode 100644
index 0000000..4c6c27d
--- /dev/null
+++ b/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/AccumuloDFSBase.java
@@ -0,0 +1,134 @@
+/*
+ * 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.accumulo.classloader.vfs;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+
+import org.apache.accumulo.start.classloader.vfs.MiniDFSUtil;
+import org.apache.commons.vfs2.CacheStrategy;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.cache.DefaultFilesCache;
+import org.apache.commons.vfs2.cache.SoftRefFilesCache;
+import org.apache.commons.vfs2.impl.DefaultFileReplicator;
+import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
+import org.apache.commons.vfs2.impl.FileContentInfoFilenameFactory;
+import org.apache.commons.vfs2.provider.hdfs.HdfsFileProvider;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hdfs.DFSConfigKeys;
+import org.apache.hadoop.hdfs.MiniDFSCluster;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "paths not set by user input")
+public class AccumuloDFSBase {
+
+  protected static Configuration conf = null;
+  protected static DefaultFileSystemManager vfs = null;
+  protected static MiniDFSCluster cluster = null;
+
+  private static URI HDFS_URI;
+
+  protected static URI getHdfsUri() {
+    return HDFS_URI;
+  }
+
+  @BeforeClass
+  public static void miniDfsClusterSetup() {
+    System.setProperty("java.io.tmpdir", System.getProperty("user.dir") + "/target");
+
+    // Put the MiniDFSCluster directory in the target directory
+    System.setProperty("test.build.data", "target/build/test/data");
+
+    // Setup HDFS
+    conf = new Configuration();
+    conf.set("hadoop.security.token.service.use_ip", "true");
+
+    conf.set("dfs.datanode.data.dir.perm", MiniDFSUtil.computeDatanodeDirectoryPermission());
+    conf.setLong(DFSConfigKeys.DFS_BLOCK_SIZE_KEY, 1024 * 1024); // 1M blocksize
+
+    try {
+      cluster = new MiniDFSCluster.Builder(conf).build();
+      cluster.waitClusterUp();
+      // We can't assume that the hostname of "localhost" will still be "localhost" after
+      // starting up the NameNode. We may get mapped into a FQDN via settings in /etc/hosts.
+      HDFS_URI = cluster.getFileSystem().getUri();
+    } catch (IOException e) {
+      throw new RuntimeException("Error setting up mini cluster", e);
+    }
+
+    // Set up the VFS
+    vfs = new DefaultFileSystemManager();
+    try {
+      vfs.setFilesCache(new DefaultFilesCache());
+      vfs.addProvider("res", new org.apache.commons.vfs2.provider.res.ResourceFileProvider());
+      vfs.addProvider("zip", new org.apache.commons.vfs2.provider.zip.ZipFileProvider());
+      vfs.addProvider("gz", new org.apache.commons.vfs2.provider.gzip.GzipFileProvider());
+      vfs.addProvider("ram", new org.apache.commons.vfs2.provider.ram.RamFileProvider());
+      vfs.addProvider("file",
+          new org.apache.commons.vfs2.provider.local.DefaultLocalFileProvider());
+      vfs.addProvider("jar", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
+      vfs.addProvider("http", new org.apache.commons.vfs2.provider.http.HttpFileProvider());
+      vfs.addProvider("https", new org.apache.commons.vfs2.provider.https.HttpsFileProvider());
+      vfs.addProvider("ftp", new org.apache.commons.vfs2.provider.ftp.FtpFileProvider());
+      vfs.addProvider("ftps", new org.apache.commons.vfs2.provider.ftps.FtpsFileProvider());
+      vfs.addProvider("war", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
+      vfs.addProvider("par", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
+      vfs.addProvider("ear", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
+      vfs.addProvider("sar", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
+      vfs.addProvider("ejb3", new org.apache.commons.vfs2.provider.jar.JarFileProvider());
+      vfs.addProvider("tmp", new org.apache.commons.vfs2.provider.temp.TemporaryFileProvider());
+      vfs.addProvider("tar", new org.apache.commons.vfs2.provider.tar.TarFileProvider());
+      vfs.addProvider("tbz2", new org.apache.commons.vfs2.provider.tar.TarFileProvider());
+      vfs.addProvider("tgz", new org.apache.commons.vfs2.provider.tar.TarFileProvider());
+      vfs.addProvider("bz2", new org.apache.commons.vfs2.provider.bzip2.Bzip2FileProvider());
+      vfs.addProvider("hdfs", new HdfsFileProvider());
+      vfs.addExtensionMap("jar", "jar");
+      vfs.addExtensionMap("zip", "zip");
+      vfs.addExtensionMap("gz", "gz");
+      vfs.addExtensionMap("tar", "tar");
+      vfs.addExtensionMap("tbz2", "tar");
+      vfs.addExtensionMap("tgz", "tar");
+      vfs.addExtensionMap("bz2", "bz2");
+      vfs.addMimeTypeMap("application/x-tar", "tar");
+      vfs.addMimeTypeMap("application/x-gzip", "gz");
+      vfs.addMimeTypeMap("application/zip", "zip");
+      vfs.setFileContentInfoFactory(new FileContentInfoFilenameFactory());
+      vfs.setFilesCache(new SoftRefFilesCache());
+      vfs.setReplicator(new DefaultFileReplicator(new File(System.getProperty("java.io.tmpdir"),
+          "accumulo-vfs-cache-" + System.getProperty("user.name", "nouser"))));
+      vfs.setCacheStrategy(CacheStrategy.ON_RESOLVE);
+      vfs.init();
+    } catch (FileSystemException e) {
+      throw new RuntimeException("Error setting up VFS", e);
+    }
+
+  }
+
+  @AfterClass
+  public static void tearDownMiniDfsCluster() {
+    if (null != cluster) {
+      cluster.shutdown();
+    }
+  }
+
+}
diff --git a/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/ClassPathPrinterTest.java b/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/ClassPathPrinterTest.java
new file mode 100644
index 0000000..6845b2a
--- /dev/null
+++ b/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/ClassPathPrinterTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.accumulo.classloader.vfs;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.net.MalformedURLException;
+
+import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "paths not set by user input")
+public class ClassPathPrinterTest {
+
+  @Rule
+  public TemporaryFolder folder1 =
+      new TemporaryFolder(new File(System.getProperty("user.dir") + "/target"));
+
+  private final ClassLoader parent = ClassPathPrinterTest.class.getClassLoader();
+
+  private static void assertPattern(String output, String pattern, boolean shouldMatch) {
+    if (shouldMatch) {
+      assertTrue("Pattern " + pattern + " did not match output: " + output,
+          output.matches(pattern));
+    } else {
+      assertFalse("Pattern " + pattern + " should not match output: " + output,
+          output.matches(pattern));
+    }
+  }
+
+  @Test
+  public void testPrintClassPath() throws Exception {
+    File conf = folder1.newFile("accumulo.properties");
+    DefaultFileSystemManager vfs = VFSManager.generateVfs();
+
+    ReloadingVFSClassLoader cl = new ReloadingVFSClassLoader(parent) {
+      @Override
+      protected String getClassPath() {
+        try {
+          return conf.toURI().toURL().toString();
+        } catch (MalformedURLException e) {
+          throw new RuntimeException("URL problem", e);
+        }
+      }
+
+      @Override
+      protected DefaultFileSystemManager getFileSystem() {
+        return vfs;
+      }
+    };
+    cl.setVMInitializedForTests();
+    cl.setVFSForTests(vfs);
+
+    assertPattern(ClassPathPrinter.getClassPath(cl, true), "(?s).*\\s+.*\\n$", true);
+    assertTrue(ClassPathPrinter.getClassPath(cl, true)
+        .contains("Level 3: ReloadingVFSClassLoader, classpath items are"));
+    assertTrue(ClassPathPrinter.getClassPath(cl, true).length()
+        > ClassPathPrinter.getClassPath(cl, false).length());
+    assertPattern(ClassPathPrinter.getClassPath(cl, false), "(?s).*\\s+.*\\n$", false);
+    assertFalse(ClassPathPrinter.getClassPath(cl, false)
+        .contains("Level 3: ReloadingVFSClassLoader, classpath items are"));
+  }
+}
diff --git a/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/ReloadingVFSClassLoaderTest.java b/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/ReloadingVFSClassLoaderTest.java
new file mode 100644
index 0000000..739b5ac
--- /dev/null
+++ b/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/ReloadingVFSClassLoaderTest.java
@@ -0,0 +1,225 @@
+/*
+ * 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.accumulo.classloader.vfs;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+@SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", justification = "paths not set by user input")
+public class ReloadingVFSClassLoaderTest {
+
+  @Rule
+  public TemporaryFolder folder1 =
+      new TemporaryFolder(new File(System.getProperty("user.dir") + "/target"));
+  String folderPath;
+  private DefaultFileSystemManager vfs;
+
+  @Before
+  public void setup() throws Exception {
+    System.setProperty(ReloadingVFSClassLoader.VFS_CLASSPATH_MONITOR_INTERVAL, "1");
+    vfs = VFSManager.generateVfs();
+
+    folderPath = folder1.getRoot().toURI() + ".*";
+
+    FileUtils.copyURLToFile(this.getClass().getResource("/HelloWorld.jar"),
+        folder1.newFile("HelloWorld.jar"));
+  }
+
+  FileObject[] createFileSystems(FileObject[] fos) throws FileSystemException {
+    FileObject[] rfos = new FileObject[fos.length];
+    for (int i = 0; i < fos.length; i++) {
+      if (vfs.canCreateFileSystem(fos[i])) {
+        rfos[i] = vfs.createFileSystem(fos[i]);
+      } else {
+        rfos[i] = fos[i];
+      }
+    }
+
+    return rfos;
+  }
+
+  @Test
+  public void testConstructor() throws Exception {
+    FileObject testDir = vfs.resolveFile(folder1.getRoot().toURI().toString());
+    FileObject[] dirContents = testDir.getChildren();
+
+    ReloadingVFSClassLoader arvcl =
+        new ReloadingVFSClassLoader(ClassLoader.getSystemClassLoader()) {
+          @Override
+          protected String getClassPath() {
+            return folderPath;
+          }
+
+          @Override
+          protected DefaultFileSystemManager getFileSystem() {
+            return vfs;
+          }
+        };
+    arvcl.setVMInitializedForTests();
+    arvcl.setVFSForTests(vfs);
+
+    FileObject[] files = ((VFSClassLoaderWrapper) arvcl.getDelegateClassLoader()).getFileObjects();
+    assertArrayEquals(createFileSystems(dirContents), files);
+
+    arvcl.close();
+  }
+
+  @Test
+  public void testReloading() throws Exception {
+    FileObject testDir = vfs.resolveFile(folder1.getRoot().toURI().toString());
+    FileObject[] dirContents = testDir.getChildren();
+
+    ReloadingVFSClassLoader arvcl =
+        new ReloadingVFSClassLoader(ClassLoader.getSystemClassLoader()) {
+          @Override
+          protected String getClassPath() {
+            return folderPath;
+          }
+
+          @Override
+          protected long getMonitorInterval() {
+            return 500l;
+          }
+
+          @Override
+          protected DefaultFileSystemManager getFileSystem() {
+            return vfs;
+          }
+        };
+    arvcl.setVMInitializedForTests();
+    arvcl.setVFSForTests(vfs);
+
+    FileObject[] files = ((VFSClassLoaderWrapper) arvcl.getDelegateClassLoader()).getFileObjects();
+    assertArrayEquals(createFileSystems(dirContents), files);
+
+    // set retry settings sufficiently low that not everything is reloaded in the first round
+    arvcl.setMaxRetries(1);
+
+    Class<?> clazz1 = arvcl.loadClass("test.HelloWorld");
+    Object o1 = clazz1.getDeclaredConstructor().newInstance();
+    assertEquals("Hello World!", o1.toString());
+
+    // Check that the class is the same before the update
+    Class<?> clazz1_5 = arvcl.loadClass("test.HelloWorld");
+    assertEquals(clazz1, clazz1_5);
+
+    assertTrue(new File(folder1.getRoot(), "HelloWorld.jar").delete());
+
+    Thread.sleep(1000);
+
+    // Update the class
+    FileUtils.copyURLToFile(this.getClass().getResource("/HelloWorld.jar"),
+        folder1.newFile("HelloWorld2.jar"));
+
+    // Wait for the monitor to notice
+    Thread.sleep(1000);
+
+    Class<?> clazz2 = arvcl.loadClass("test.HelloWorld");
+    Object o2 = clazz2.getDeclaredConstructor().newInstance();
+    assertEquals("Hello World!", o2.toString());
+
+    // This is false because they are loaded by a different classloader
+    assertNotEquals(clazz1, clazz2);
+    assertNotEquals(o1, o2);
+
+    arvcl.close();
+  }
+
+  @Test
+  public void testReloadingWithLongerTimeout() throws Exception {
+    FileObject testDir = vfs.resolveFile(folder1.getRoot().toURI().toString());
+    FileObject[] dirContents = testDir.getChildren();
+
+    ReloadingVFSClassLoader arvcl =
+        new ReloadingVFSClassLoader(ClassLoader.getSystemClassLoader()) {
+          @Override
+          protected String getClassPath() {
+            return folderPath;
+          }
+
+          @Override
+          protected long getMonitorInterval() {
+            return 1000l;
+          }
+
+          @Override
+          protected DefaultFileSystemManager getFileSystem() {
+            return vfs;
+          }
+        };
+    arvcl.setVMInitializedForTests();
+    arvcl.setVFSForTests(vfs);
+
+    FileObject[] files = ((VFSClassLoaderWrapper) arvcl.getDelegateClassLoader()).getFileObjects();
+    assertArrayEquals(createFileSystems(dirContents), files);
+
+    // set retry settings sufficiently high such that reloading happens in the first rounds
+    arvcl.setMaxRetries(3);
+
+    Class<?> clazz1 = arvcl.loadClass("test.HelloWorld");
+    Object o1 = clazz1.getDeclaredConstructor().newInstance();
+    assertEquals("Hello World!", o1.toString());
+
+    // Check that the class is the same before the update
+    Class<?> clazz1_5 = arvcl.loadClass("test.HelloWorld");
+    assertEquals(clazz1, clazz1_5);
+
+    assertTrue(new File(folder1.getRoot(), "HelloWorld.jar").delete());
+
+    Thread.sleep(3000);
+
+    // Update the class
+    FileUtils.copyURLToFile(this.getClass().getResource("/HelloWorld.jar"),
+        folder1.newFile("HelloWorld2.jar"));
+
+    // Wait for the monitor to notice
+    Thread.sleep(3000);
+
+    Class<?> clazz2 = arvcl.loadClass("test.HelloWorld");
+    Object o2 = clazz2.getDeclaredConstructor().newInstance();
+    assertEquals("Hello World!", o2.toString());
+
+    // This is false because even though it's the same class, it's loaded from a different jar
+    // this is a change in behavior from previous versions of vfs2 where it would load the same
+    // class from different jars as though it was from the first jar
+    assertNotEquals(clazz1, clazz2);
+    assertNotSame(o1, o2);
+    assertEquals(clazz1.getName(), clazz2.getName());
+    assertEquals(o1.toString(), o2.toString());
+
+    arvcl.close();
+  }
+
+}
diff --git a/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/VfsClassLoaderTest.java b/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/VfsClassLoaderTest.java
new file mode 100644
index 0000000..4778872
--- /dev/null
+++ b/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/VfsClassLoaderTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.accumulo.classloader.vfs;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.net.URL;
+
+import org.apache.commons.vfs2.FileChangeEvent;
+import org.apache.commons.vfs2.FileListener;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.impl.DefaultFileMonitor;
+import org.apache.commons.vfs2.impl.VFSClassLoader;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class VfsClassLoaderTest extends AccumuloDFSBase {
+
+  private static final Path TEST_DIR = new Path(getHdfsUri() + "/test-dir");
+
+  private FileSystem hdfs = null;
+  private VFSClassLoader cl = null;
+
+  @Before
+  public void setup() throws Exception {
+
+    this.hdfs = cluster.getFileSystem();
+    this.hdfs.mkdirs(TEST_DIR);
+
+    // Copy jar file to TEST_DIR
+    URL jarPath = this.getClass().getResource("/HelloWorld.jar");
+    Path src = new Path(jarPath.toURI().toString());
+    Path dst = new Path(TEST_DIR, src.getName());
+    this.hdfs.copyFromLocalFile(src, dst);
+
+    FileObject testDir = vfs.resolveFile(TEST_DIR.toUri().toString());
+    FileObject[] dirContents = testDir.getChildren();
+
+    // Point the VFSClassLoader to all of the objects in TEST_DIR
+    this.cl = new VFSClassLoader(dirContents, vfs);
+  }
+
+  @Test
+  public void testGetClass() throws Exception {
+    Class<?> helloWorldClass = this.cl.loadClass("test.HelloWorld");
+    Object o = helloWorldClass.getDeclaredConstructor().newInstance();
+    assertEquals("Hello World!", o.toString());
+  }
+
+  @Test
+  public void testFileMonitor() throws Exception {
+    MyFileMonitor listener = new MyFileMonitor();
+    DefaultFileMonitor monitor = new DefaultFileMonitor(listener);
+    monitor.setRecursive(true);
+    FileObject testDir = vfs.resolveFile(TEST_DIR.toUri().toString());
+    monitor.addFile(testDir);
+    monitor.start();
+
+    // Copy jar file to a new file name
+    URL jarPath = this.getClass().getResource("/HelloWorld.jar");
+    Path src = new Path(jarPath.toURI().toString());
+    Path dst = new Path(TEST_DIR, "HelloWorld2.jar");
+    this.hdfs.copyFromLocalFile(src, dst);
+
+    // VFS-487 significantly wait to avoid failure
+    Thread.sleep(7000);
+    assertTrue(listener.isFileCreated());
+
+    // Update the jar
+    jarPath = this.getClass().getResource("/HelloWorld.jar");
+    src = new Path(jarPath.toURI().toString());
+    dst = new Path(TEST_DIR, "HelloWorld2.jar");
+    this.hdfs.copyFromLocalFile(src, dst);
+
+    // VFS-487 significantly wait to avoid failure
+    Thread.sleep(7000);
+    assertTrue(listener.isFileChanged());
+
+    this.hdfs.delete(dst, false);
+    // VFS-487 significantly wait to avoid failure
+    Thread.sleep(7000);
+    assertTrue(listener.isFileDeleted());
+
+    monitor.stop();
+
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    this.hdfs.delete(TEST_DIR, true);
+  }
+
+  public static class MyFileMonitor implements FileListener {
+
+    private boolean fileChanged = false;
+    private boolean fileDeleted = false;
+    private boolean fileCreated = false;
+
+    @Override
+    public void fileCreated(FileChangeEvent event) throws Exception {
+      // System.out.println(event.getFile() + " created");
+      this.fileCreated = true;
+    }
+
+    @Override
+    public void fileDeleted(FileChangeEvent event) throws Exception {
+      // System.out.println(event.getFile() + " deleted");
+      this.fileDeleted = true;
+    }
+
+    @Override
+    public void fileChanged(FileChangeEvent event) throws Exception {
+      // System.out.println(event.getFile() + " changed");
+      this.fileChanged = true;
+    }
+
+    public boolean isFileChanged() {
+      return fileChanged;
+    }
+
+    public boolean isFileDeleted() {
+      return fileDeleted;
+    }
+
+    public boolean isFileCreated() {
+      return fileCreated;
+    }
+
+  }
+}
diff --git a/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/context/ReloadingVFSContextClassLoaderFactoryTest.java b/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/context/ReloadingVFSContextClassLoaderFactoryTest.java
new file mode 100644
index 0000000..4be5c46
--- /dev/null
+++ b/modules/vfs-class-loader/src/test/java/org/apache/accumulo/classloader/vfs/context/ReloadingVFSContextClassLoaderFactoryTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.accumulo.classloader.vfs.context;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.nio.file.StandardOpenOption.WRITE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.accumulo.classloader.vfs.context.ReloadingVFSContextClassLoaderFactory.Context;
+import org.apache.accumulo.classloader.vfs.context.ReloadingVFSContextClassLoaderFactory.ContextConfig;
+import org.apache.accumulo.classloader.vfs.context.ReloadingVFSContextClassLoaderFactory.Contexts;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import com.google.gson.Gson;
+
+public class ReloadingVFSContextClassLoaderFactoryTest {
+
+  @Rule
+  public TemporaryFolder temp = new TemporaryFolder();
+
+  private static final Contexts c = new Contexts();
+
+  @BeforeClass
+  public static void setup() throws Exception {
+    ContextConfig cc1 = new ContextConfig();
+    cc1.setClassPath("file:///tmp/foo");
+    cc1.setPostDelegate(true);
+    cc1.setMonitorIntervalMs(30000);
+    Context c1 = new Context();
+    c1.setName("cx1");
+    c1.setConfig(cc1);
+
+    ContextConfig cc2 = new ContextConfig();
+    cc2.setClassPath("file:///tmp/bar");
+    cc2.setPostDelegate(false);
+    cc2.setMonitorIntervalMs(30000);
+    Context c2 = new Context();
+    c2.setName("cx2");
+    c2.setConfig(cc2);
+
+    List<Context> list = new ArrayList<>();
+    list.add(c1);
+    list.add(c2);
+    c.setContexts(list);
+  }
+
+  @Test
+  public void testDeSer() throws Exception {
+    Gson g = new Gson().newBuilder().setPrettyPrinting().create();
+    String contexts = g.toJson(c);
+    System.out.println(contexts);
+
+    Gson g2 = new Gson();
+    Contexts actual = g2.fromJson(contexts, Contexts.class);
+
+    assertEquals(c, actual);
+
+  }
+
+  @Test
+  public void testCreation() throws Exception {
+    File f = temp.newFile();
+    f.deleteOnExit();
+    Gson g = new Gson();
+    String contexts = g.toJson(c);
+    try (BufferedWriter writer = Files.newBufferedWriter(f.toPath(), UTF_8, WRITE)) {
+      writer.write(contexts);
+    }
+    ReloadingVFSContextClassLoaderFactory cl = new ReloadingVFSContextClassLoaderFactory() {
+      @Override
+      protected String getConfigFileLocation() {
+        return f.getAbsolutePath();
+      }
+    };
+    cl.initialize(null);
+    try {
+      cl.getClassLoader("c1");
+      fail("Expected illegal argument exception");
+    } catch (IllegalArgumentException e) {
+      // works
+    }
+    cl.getClassLoader("cx1");
+    cl.getClassLoader("cx2");
+
+  }
+
+}
diff --git a/modules/vfs-class-loader/src/test/java/test/HelloWorldTemplate b/modules/vfs-class-loader/src/test/java/test/HelloWorldTemplate
new file mode 100644
index 0000000..b1a6a73
--- /dev/null
+++ b/modules/vfs-class-loader/src/test/java/test/HelloWorldTemplate
@@ -0,0 +1,27 @@
+/*
+ * 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 test;
+
+public class HelloWorld {
+
+  @Override
+  public String toString() {
+    return "%%";
+  }
+}
diff --git a/modules/vfs-class-loader/src/test/java/test/Test.java b/modules/vfs-class-loader/src/test/java/test/Test.java
new file mode 100644
index 0000000..a93aded
--- /dev/null
+++ b/modules/vfs-class-loader/src/test/java/test/Test.java
@@ -0,0 +1,27 @@
+/*
+ * 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 test;
+
+public interface Test {
+
+  String hello();
+
+  int add();
+
+}
diff --git a/modules/vfs-class-loader/src/test/java/test/TestTemplate b/modules/vfs-class-loader/src/test/java/test/TestTemplate
new file mode 100644
index 0000000..ab5e217
--- /dev/null
+++ b/modules/vfs-class-loader/src/test/java/test/TestTemplate
@@ -0,0 +1,36 @@
+/*
+ * 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 test;
+
+public class TestObject implements Test {
+    
+    int i = 0;
+
+    @Override
+    public String hello() {
+        return "Hello from testX";
+    }
+
+    @Override
+    public int add() {
+        i += 1;
+        return i;
+    }
+
+}
diff --git a/modules/vfs-class-loader/src/test/resources/log4j2-test.properties b/modules/vfs-class-loader/src/test/resources/log4j2-test.properties
new file mode 100644
index 0000000..6dcf0c5
--- /dev/null
+++ b/modules/vfs-class-loader/src/test/resources/log4j2-test.properties
@@ -0,0 +1,35 @@
+#
+# 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.
+#
+
+status = info
+dest = err
+name = AccumuloVFSClassLoaderTestLoggingProperties
+
+appender.console.type = Console
+appender.console.name = STDOUT
+appender.console.target = SYSTEM_OUT
+appender.console.layout.type = PatternLayout
+appender.console.layout.pattern = %d{ISO8601} [%-8c{2}] %-5p: %m%n
+
+logger.01.name = org.apache.accumulo.server.util.TabletIterator
+logger.01.level = error
+
+rootLogger.level = info
+rootLogger.appenderRef.console.ref = STDOUT
+
diff --git a/modules/vfs-class-loader/src/test/shell/makeHelloWorldJars.sh b/modules/vfs-class-loader/src/test/shell/makeHelloWorldJars.sh
new file mode 100755
index 0000000..04289ec
--- /dev/null
+++ b/modules/vfs-class-loader/src/test/shell/makeHelloWorldJars.sh
@@ -0,0 +1,33 @@
+#! /usr/bin/env bash
+#
+# 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.
+#
+
+if [ -z "$JAVA_HOME" ]; then
+   echo "JAVA_HOME is not set. Java is required to proceed"
+   exit 1
+fi
+mkdir -p target/generated-sources/HelloWorld/test
+sed "s/%%/Hello World\!/" < src/test/java/test/HelloWorldTemplate > target/generated-sources/HelloWorld/test/HelloWorld.java
+$JAVA_HOME/bin/javac target/generated-sources/HelloWorld/test/HelloWorld.java -d target/generated-sources/HelloWorld
+$JAVA_HOME/bin/jar -cf target/test-classes/HelloWorld.jar -C target/generated-sources/HelloWorld test/HelloWorld.class
+
+mkdir -p target/generated-sources/HalloWelt/test
+sed "s/%%/Hallo Welt/" < src/test/java/test/HelloWorldTemplate > target/generated-sources/HalloWelt/test/HelloWorld.java
+$JAVA_HOME/bin/javac target/generated-sources/HalloWelt/test/HelloWorld.java -d target/generated-sources/HalloWelt
+$JAVA_HOME/bin/jar -cf target/test-classes/HelloWorld2.jar -C target/generated-sources/HalloWelt test/HelloWorld.class
diff --git a/modules/vfs-class-loader/src/test/shell/makeTestJars.sh b/modules/vfs-class-loader/src/test/shell/makeTestJars.sh
new file mode 100755
index 0000000..fc86fb7
--- /dev/null
+++ b/modules/vfs-class-loader/src/test/shell/makeTestJars.sh
@@ -0,0 +1,32 @@
+#! /usr/bin/env bash
+#
+# 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.
+#
+
+if [ -z "$JAVA_HOME" ]; then
+   echo "JAVA_HOME is not set. Java is required to proceed"
+   exit 1
+fi
+
+for x in A B C
+do
+    mkdir -p target/generated-sources/$x/test target/test-classes/ClassLoaderTest$x
+    sed "s/testX/test$x/" < src/test/java/test/TestTemplate > target/generated-sources/$x/test/TestObject.java
+    $JAVA_HOME/bin/javac -cp target/test-classes target/generated-sources/$x/test/TestObject.java -d target/generated-sources/$x
+    $JAVA_HOME/bin/jar -cf target/test-classes/ClassLoaderTest$x/Test.jar -C target/generated-sources/$x test/TestObject.class
+done
diff --git a/pom.xml b/pom.xml
index 61e4e2a..21ca3e3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,6 +25,7 @@
   <groupId>org.apache.accumulo</groupId>
   <artifactId>classloader-extras</artifactId>
   <version>1.0.0-SNAPSHOT</version>
+  <packaging>pom</packaging>
   <name>Classloader Extras</name>
   <description>Classloader Extras provided by the Apache Accumulo project</description>
   <inceptionYear>2020</inceptionYear>
@@ -66,6 +67,9 @@
       <archive>https://lists.apache.org/list.html?notifications@accumulo.apache.org</archive>
     </mailingList>
   </mailingLists>
+  <modules>
+    <module>modules/vfs-class-loader</module>
+  </modules>
   <scm>
     <connection>scm:git:https://gitbox.apache.org/repos/asf/accumulo-classloaders.git</connection>
     <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/accumulo-classloaders.git</developerConnection>
@@ -94,14 +98,11 @@
   </properties>
   <dependencies>
     <!-- spotbugs-annotations provides SuppressFBWarnings annotation -->
-    <!--
     <dependency>
       <groupId>com.github.spotbugs</groupId>
       <artifactId>spotbugs-annotations</artifactId>
       <version>4.1.2</version>
-      <optional>true</optional>
     </dependency>
-    -->
   </dependencies>
   <build>
     <pluginManagement>