You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@geode.apache.org by ds...@apache.org on 2017/04/21 23:42:14 UTC

[18/51] [abbrv] geode git commit: GEODE-2686: Remove JarClassLoader

http://git-wip-us.apache.org/repos/asf/geode/blob/6fd2d123/geode-core/src/main/java/org/apache/geode/internal/JarDeployer.java
----------------------------------------------------------------------
diff --git a/geode-core/src/main/java/org/apache/geode/internal/JarDeployer.java b/geode-core/src/main/java/org/apache/geode/internal/JarDeployer.java
index 18d4b42..ad5c435 100644
--- a/geode-core/src/main/java/org/apache/geode/internal/JarDeployer.java
+++ b/geode-core/src/main/java/org/apache/geode/internal/JarDeployer.java
@@ -14,44 +14,60 @@
  */
 package org.apache.geode.internal;
 
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.geode.GemFireException;
+import org.apache.geode.GemFireIOException;
 import org.apache.geode.SystemFailure;
 import org.apache.geode.internal.logging.LogService;
 import org.apache.logging.log4j.Logger;
 
 import java.io.BufferedInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.FilenameFilter;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.RandomAccessFile;
 import java.io.Serializable;
-import java.nio.channels.FileLock;
+import java.net.URL;
+import java.net.URLClassLoader;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Stream;
 
 public class JarDeployer implements Serializable {
   private static final long serialVersionUID = 1L;
   private static final Logger logger = LogService.getLogger();
-  public static final String JAR_PREFIX = "vf.gf#";
-  public static final String JAR_PREFIX_FOR_REGEX = "^vf\\.gf#";
+  public static final String JAR_PREFIX = "";
+  public static final String JAR_PREFIX_FOR_REGEX = "";
   private static final Lock lock = new ReentrantLock();
 
+  private final Map<String, DeployedJar> deployedJars = new ConcurrentHashMap<>();
+
+
   // Split a versioned filename into its name and version
   public static final Pattern versionedPattern =
-      Pattern.compile(JAR_PREFIX_FOR_REGEX + "(.*)#(\\d++)$");
+      Pattern.compile(JAR_PREFIX_FOR_REGEX + "(.*)\\.v(\\d++).jar$");
 
   private final File deployDirectory;
 
@@ -63,262 +79,36 @@ public class JarDeployer implements Serializable {
     this.deployDirectory = deployDirectory;
   }
 
-  /**
-   * Re-deploy all previously deployed JAR files.
-   */
-  public void loadPreviouslyDeployedJars() {
-    List<JarClassLoader> jarClassLoaders = new ArrayList<JarClassLoader>();
-
-    lock.lock();
-    try {
-      try {
-        verifyWritableDeployDirectory();
-        final Set<String> jarNames = findDistinctDeployedJars();
-        if (!jarNames.isEmpty()) {
-          for (String jarName : jarNames) {
-            final File[] jarFiles = findSortedOldVersionsOfJar(jarName);
-
-            // It's possible the JARs were deleted by another process
-            if (jarFiles.length != 0) {
-              JarClassLoader jarClassLoader = findJarClassLoader(jarName);
-
-              try {
-                final byte[] jarBytes = getJarContent(jarFiles[0]);
-                if (!JarClassLoader.isValidJarContent(jarBytes)) {
-                  logger.warn("Invalid JAR file found and deleted: {}",
-                      jarFiles[0].getAbsolutePath());
-                  jarFiles[0].delete();
-                } else {
-                  // Test to see if the exact same file is already in use
-                  if (jarClassLoader == null
-                      || !jarClassLoader.getFileName().equals(jarFiles[0].getName())) {
-                    jarClassLoader = new JarClassLoader(jarFiles[0], jarName, jarBytes);
-                    ClassPathLoader.getLatest().addOrReplaceAndSetLatest(jarClassLoader);
-                    jarClassLoaders.add(jarClassLoader);
-                  }
-                }
-              } catch (IOException ioex) {
-                // Another process deleted the file so don't bother doing anything else with it
-                if (logger.isDebugEnabled()) {
-                  logger.debug("Failed attempt to use JAR to create JarClassLoader for: {}",
-                      jarName);
-                }
-              }
-
-              // Remove any old left-behind versions of this JAR file
-              for (File jarFile : jarFiles) {
-                if (jarFile.exists() && (jarClassLoader == null
-                    || !jarClassLoader.getFileName().equals(jarFile.getName()))) {
-                  attemptFileLockAndDelete(jarFile);
-                }
-              }
-            }
-          }
-        }
-
-        for (JarClassLoader jarClassLoader : jarClassLoaders) {
-          jarClassLoader.loadClassesAndRegisterFunctions();
-        }
-      } catch (VirtualMachineError e) {
-        SystemFailure.initiateFailure(e);
-        throw e;
-      } catch (Throwable th) {
-        SystemFailure.checkFailure();
-        logger.error("Error when attempting to deploy JAR files on load.", th);
-      }
-    } finally {
-      lock.unlock();
-    }
+  public File getDeployDirectory() {
+    return this.deployDirectory;
   }
 
-  /**
-   * Deploy the given JAR files.
-   * 
-   * @param jarNames Array of names of the JAR files to deploy.
-   * @param jarBytes Array of contents of the JAR files to deploy.
-   * @return An array of newly created JAR class loaders. Entries will be null for an JARs that were
-   *         already deployed.
-   * @throws IOException When there's an error saving the JAR file to disk
-   */
-  public JarClassLoader[] deploy(final String jarNames[], final byte[][] jarBytes)
-      throws IOException, ClassNotFoundException {
-    JarClassLoader[] jarClassLoaders = new JarClassLoader[jarNames.length];
-    verifyWritableDeployDirectory();
-
+  public DeployedJar deployWithoutRegistering(final String jarName, final byte[] jarBytes)
+      throws IOException {
     lock.lock();
+
     try {
-      for (int i = 0; i < jarNames.length; i++) {
-        if (!JarClassLoader.isValidJarContent(jarBytes[i])) {
-          throw new IllegalArgumentException(
-              "File does not contain valid JAR content: " + jarNames[i]);
-        }
-      }
+      verifyWritableDeployDirectory();
 
-      for (int i = 0; i < jarNames.length; i++) {
-        jarClassLoaders[i] = deployWithoutRegistering(jarNames[i], jarBytes[i]);
-      }
+      File newVersionedJarFile = getNextVersionedJarFile(jarName);
+      writeJarBytesToFile(newVersionedJarFile, jarBytes);
 
-      for (JarClassLoader jarClassLoader : jarClassLoaders) {
-        if (jarClassLoader != null) {
-          jarClassLoader.loadClassesAndRegisterFunctions();
-        }
-      }
+      return new DeployedJar(newVersionedJarFile, jarName, jarBytes);
     } finally {
       lock.unlock();
     }
-    return jarClassLoaders;
   }
 
-  /**
-   * Deploy the given JAR file without registering functions.
-   * 
-   * @param jarName Name of the JAR file to deploy.
-   * @param jarBytes Contents of the JAR file to deploy.
-   * @return The newly created JarClassLoader or null if the JAR was already deployed
-   * @throws IOException When there's an error saving the JAR file to disk
-   */
-  private JarClassLoader deployWithoutRegistering(final String jarName, final byte[] jarBytes)
-      throws IOException {
-    JarClassLoader oldJarClassLoader = findJarClassLoader(jarName);
-
-    final boolean isDebugEnabled = logger.isDebugEnabled();
-    if (isDebugEnabled) {
-      logger.debug("Deploying {}: {}", jarName, (oldJarClassLoader == null ? ": not yet deployed"
-          : ": already deployed as " + oldJarClassLoader.getFileCanonicalPath()));
-    }
-
-    // Test to see if the exact same file is being deployed
-    if (oldJarClassLoader != null && oldJarClassLoader.hasSameContent(jarBytes)) {
-      return null;
-    }
-
-    JarClassLoader newJarClassLoader = null;
-
-    do {
-      File[] oldJarFiles = findSortedOldVersionsOfJar(jarName);
-
-      try {
-        // If this is the first version of this JAR file we've seen ...
-        if (oldJarFiles.length == 0) {
-          if (isDebugEnabled) {
-            logger.debug("There were no pre-existing versions for JAR: {}", jarName);
-          }
-          File nextVersionJarFile = getNextVersionJarFile(jarName);
-          if (writeJarBytesToFile(nextVersionJarFile, jarBytes)) {
-            newJarClassLoader = new JarClassLoader(nextVersionJarFile, jarName, jarBytes);
-            if (isDebugEnabled) {
-              logger.debug("Successfully created initial JarClassLoader at file: {}",
-                  nextVersionJarFile.getAbsolutePath());
-            }
-          } else {
-            if (isDebugEnabled) {
-              logger.debug("Unable to write contents for first version of JAR to file: {}",
-                  nextVersionJarFile.getAbsolutePath());
-            }
-          }
-
-        } else {
-          // Most recent is at the beginning of the list, see if this JAR matches what's
-          // already on disk.
-          if (doesFileMatchBytes(oldJarFiles[0], jarBytes)) {
-            if (isDebugEnabled) {
-              logger.debug("A version on disk was an exact match for the JAR being deployed: {}",
-                  oldJarFiles[0].getAbsolutePath());
-            }
-            newJarClassLoader = new JarClassLoader(oldJarFiles[0], jarName, jarBytes);
-            if (isDebugEnabled) {
-              logger.debug("Successfully reused JAR to create JarClassLoader from file: {}",
-                  oldJarFiles[0].getAbsolutePath());
-            }
-          } else {
-            // This JAR isn't on disk
-            if (isDebugEnabled) {
-              logger.debug("Need to create a new version for JAR: {}", jarName);
-            }
-            File nextVersionJarFile = getNextVersionJarFile(oldJarFiles[0].getName());
-            if (writeJarBytesToFile(nextVersionJarFile, jarBytes)) {
-              newJarClassLoader = new JarClassLoader(nextVersionJarFile, jarName, jarBytes);
-              if (isDebugEnabled) {
-                logger.debug("Successfully created next JarClassLoader at file: {}",
-                    nextVersionJarFile.getAbsolutePath());
-              }
-            } else {
-              if (isDebugEnabled) {
-                logger.debug("Unable to write contents for next version of JAR to file: {}",
-                    nextVersionJarFile.getAbsolutePath());
-              }
-            }
-          }
-        }
-      } catch (IOException ioex) {
-        // Another process deleted the file before we could get to it, just start again
-        logger.info("Failed attempt to use JAR to create JarClassLoader for: {} : {}", jarName,
-            ioex.getMessage());
-      }
-
-      if (isDebugEnabled) {
-        if (newJarClassLoader == null) {
-          logger.debug("Unable to determine a JAR file location, will loop and try again: {}",
-              jarName);
-        } else {
-          logger.debug("Exiting loop for JarClassLoader creation using file: {}",
-              newJarClassLoader.getFileName());
-        }
-      }
-    } while (newJarClassLoader == null);
-
-    ClassPathLoader.getLatest().addOrReplaceAndSetLatest(newJarClassLoader);
-
-    // Remove the JAR file that was undeployed as part of this redeploy
-    if (oldJarClassLoader != null) {
-      attemptFileLockAndDelete(new File(this.deployDirectory, oldJarClassLoader.getFileName()));
-    }
-
-    return newJarClassLoader;
-  }
 
   /**
-   * Undeploy the given JAR file.
+   * Get a list of all currently deployed jars.
    * 
-   * @param jarName The name of the JAR file to undeploy
-   * @return The path to the location on disk where the JAR file had been deployed
-   * @throws IOException If there's a problem deleting the file
+   * @return The list of DeployedJars
    */
-  public String undeploy(final String jarName) throws IOException {
-    JarClassLoader jarClassLoader = null;
-    verifyWritableDeployDirectory();
-
-    lock.lock();
-    try {
-      jarClassLoader = findJarClassLoader(jarName);
-      if (jarClassLoader == null) {
-        throw new IllegalArgumentException("JAR not deployed");
-      }
-
-      ClassPathLoader.getLatest().removeAndSetLatest(jarClassLoader);
-      attemptFileLockAndDelete(new File(this.deployDirectory, jarClassLoader.getFileName()));
-      return jarClassLoader.getFileCanonicalPath();
-    } finally {
-      lock.unlock();
-    }
+  public List<DeployedJar> findDeployedJars() {
+    return getDeployedJars().values().stream().collect(toList());
   }
 
-  /**
-   * Get a list of all currently deployed JarClassLoaders.
-   * 
-   * @return The list of JarClassLoaders
-   */
-  public List<JarClassLoader> findJarClassLoaders() {
-    List<JarClassLoader> returnList = new ArrayList<JarClassLoader>();
-    Collection<ClassLoader> classLoaders = ClassPathLoader.getLatest().getClassLoaders();
-    for (ClassLoader classLoader : classLoaders) {
-      if (classLoader instanceof JarClassLoader) {
-        returnList.add((JarClassLoader) classLoader);
-      }
-    }
-
-    return returnList;
-  }
 
   /**
    * Suspend all deploy and undeploy operations. This is done by acquiring and holding the lock
@@ -339,26 +129,19 @@ public class JarDeployer implements Serializable {
     lock.unlock();
   }
 
-  /**
-   * Figure out the next version of a JAR file
-   * 
-   * @param latestVersionedJarName The previous most recent version of the JAR file or original name
-   *        if there wasn't one
-   * @return The file that represents the next version
-   */
-  protected File getNextVersionJarFile(final String latestVersionedJarName) {
-    String newFileName;
-    final Matcher matcher = versionedPattern.matcher(latestVersionedJarName);
-    if (matcher.find()) {
-      newFileName = JAR_PREFIX + matcher.group(1) + "#" + (Integer.parseInt(matcher.group(2)) + 1);
+  protected File getNextVersionedJarFile(String unversionedJarName) {
+    File[] oldVersions = findSortedOldVersionsOfJar(unversionedJarName);
+
+    String nextVersionedJarName;
+    if (oldVersions == null || oldVersions.length == 0) {
+      nextVersionedJarName = removeJarExtension(unversionedJarName) + ".v1.jar";
     } else {
-      newFileName = JAR_PREFIX + latestVersionedJarName + "#1";
+      String latestVersionedJarName = oldVersions[0].getName();
+      int nextVersion = extractVersionFromFilename(latestVersionedJarName) + 1;
+      nextVersionedJarName = removeJarExtension(unversionedJarName) + ".v" + nextVersion + ".jar";
     }
 
-    if (logger.isDebugEnabled()) {
-      logger.debug("Next version file name will be: {}", newFileName);
-    }
-    return new File(this.deployDirectory, newFileName);
+    return new File(deployDirectory, nextVersionedJarName);
   }
 
   /**
@@ -370,27 +153,18 @@ public class JarDeployer implements Serializable {
    * @param jarBytes Contents of the JAR file to deploy.
    * @return True if the file was successfully written, false otherwise
    */
-  private boolean writeJarBytesToFile(final File file, final byte[] jarBytes) {
+  private boolean writeJarBytesToFile(final File file, final byte[] jarBytes) throws IOException {
     final boolean isDebugEnabled = logger.isDebugEnabled();
-    try {
-      if (file.createNewFile()) {
-        if (isDebugEnabled) {
-          logger.debug("Successfully created new JAR file: {}", file.getAbsolutePath());
-        }
-        final OutputStream outStream = new FileOutputStream(file);
-        outStream.write(jarBytes);
-        outStream.close();
-        return true;
-      }
-      return doesFileMatchBytes(file, jarBytes);
-
-    } catch (IOException ioex) {
-      // Another VM clobbered what was happening here, try again
+    if (file.createNewFile()) {
       if (isDebugEnabled) {
-        logger.debug("IOException while trying to write JAR content to file: {}", ioex);
+        logger.debug("Successfully created new JAR file: {}", file.getAbsolutePath());
       }
-      return false;
+      final OutputStream outStream = new FileOutputStream(file);
+      outStream.write(jarBytes);
+      outStream.close();
+      return true;
     }
+    return doesFileMatchBytes(file, jarBytes);
   }
 
   /**
@@ -457,86 +231,22 @@ public class JarDeployer implements Serializable {
     return true;
   }
 
-  private void attemptFileLockAndDelete(final File file) throws IOException {
-    final String absolutePath = file.getAbsolutePath();
-    FileOutputStream fileOutputStream = new FileOutputStream(file, true);
-    final boolean isDebugEnabled = logger.isDebugEnabled();
-    try {
-      FileLock fileLock = null;
-      try {
-        fileLock = fileOutputStream.getChannel().tryLock();
-
-        if (fileLock != null) {
-          if (isDebugEnabled) {
-            logger.debug("Tried and acquired exclusive lock for file: {}, w/ channel {}",
-                absolutePath, fileLock.channel());
-          }
-
-          if (file.delete()) {
-            if (isDebugEnabled) {
-              logger.debug("Deleted file with name: {}", absolutePath);
-            }
-          } else {
-            if (isDebugEnabled) {
-              logger.debug("Could not delete file, will truncate instead and delete on exit: {}",
-                  absolutePath);
-            }
-            file.deleteOnExit();
-
-            RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
-            try {
-              randomAccessFile.setLength(0);
-            } finally {
-              try {
-                randomAccessFile.close();
-              } catch (IOException ioex) {
-                logger.error("Could not close file when attempting to set zero length", ioex);
-              }
-            }
-          }
-        } else {
-          if (isDebugEnabled) {
-            logger.debug("Will not delete file since exclusive lock unavailable: {}", absolutePath);
-          }
-        }
-
-      } finally {
-        if (fileLock != null) {
-          try {
-            fileLock.release();
-            fileLock.channel().close();
-            if (isDebugEnabled) {
-              logger.debug("Released file lock for file: {}, w/ channel: {}", absolutePath,
-                  fileLock.channel());
-            }
-          } catch (IOException ioex) {
-            logger.error("Could not close channel on JAR lock file", ioex);
-          }
-        }
-      }
-    } finally {
-      try {
-        fileOutputStream.close();
-      } catch (IOException ioex) {
-        logger.error("Could not close output stream on JAR file", ioex);
-      }
-    }
-  }
-
   /**
    * Find the version number that's embedded in the name of this file
    * 
-   * @param file File to get the version number from
+   * @param filename Filename to get the version number from
    * @return The version number embedded in the filename
    */
-  int extractVersionFromFilename(final File file) {
-    final Matcher matcher = versionedPattern.matcher(file.getName());
-    matcher.find();
-    return Integer.parseInt(matcher.group(2));
+  public static int extractVersionFromFilename(final String filename) {
+    final Matcher matcher = versionedPattern.matcher(filename);
+    if (matcher.find()) {
+      return Integer.parseInt(matcher.group(2));
+    } else {
+      return 0;
+    }
   }
 
   protected Set<String> findDistinctDeployedJars() {
-
     // Find all deployed JAR files
     final File[] oldFiles = this.deployDirectory.listFiles(new FilenameFilter() {
       @Override
@@ -559,41 +269,32 @@ public class JarDeployer implements Serializable {
    * Find all versions of the JAR file that are currently on disk and return them sorted from newest
    * (highest version) to oldest
    * 
-   * @param jarFilename Name of the JAR file that we want old versions of
+   * @param unversionedJarName Name of the JAR file that we want old versions of
    * @return Sorted array of files that are older versions of the given JAR
    */
-  protected File[] findSortedOldVersionsOfJar(final String jarFilename) {
+  protected File[] findSortedOldVersionsOfJar(final String unversionedJarName) {
     // Find all matching files
-    final Pattern pattern = Pattern.compile(JAR_PREFIX_FOR_REGEX + jarFilename + "#\\d++$");
-    final File[] oldJarFiles = this.deployDirectory.listFiles(new FilenameFilter() {
-      @Override
-      public boolean accept(final File file, final String name) {
-        return (pattern.matcher(name).matches());
-      }
-    });
+    final Pattern pattern = Pattern.compile(
+        JAR_PREFIX_FOR_REGEX + removeJarExtension(unversionedJarName) + "\\.v\\d++\\.jar$");
+    final File[] oldJarFiles =
+        this.deployDirectory.listFiles((file, name) -> (pattern.matcher(name).matches()));
 
     // Sort them in order from newest (highest version) to oldest
-    Arrays.sort(oldJarFiles, new Comparator<File>() {
-      @Override
-      public int compare(final File file1, final File file2) {
-        int file1Version = extractVersionFromFilename(file1);
-        int file2Version = extractVersionFromFilename(file2);
-        return file2Version - file1Version;
-      }
+    Arrays.sort(oldJarFiles, (file1, file2) -> {
+      int file1Version = extractVersionFromFilename(file1.getName());
+      int file2Version = extractVersionFromFilename(file2.getName());
+      return file2Version - file1Version;
     });
 
     return oldJarFiles;
   }
 
-  private JarClassLoader findJarClassLoader(final String jarName) {
-    Collection<ClassLoader> classLoaders = ClassPathLoader.getLatest().getClassLoaders();
-    for (ClassLoader classLoader : classLoaders) {
-      if (classLoader instanceof JarClassLoader
-          && ((JarClassLoader) classLoader).getJarName().equals(jarName)) {
-        return (JarClassLoader) classLoader;
-      }
+  protected String removeJarExtension(String jarName) {
+    if (jarName != null && jarName.endsWith(".jar")) {
+      return jarName.replaceAll("\\.jar$", "");
+    } else {
+      return jarName;
     }
-    return null;
   }
 
   /**
@@ -601,7 +302,7 @@ public class JarDeployer implements Serializable {
    * 
    * @throws IOException If the directory isn't writable
    */
-  private void verifyWritableDeployDirectory() throws IOException {
+  public void verifyWritableDeployDirectory() throws IOException {
     Exception exception = null;
     int tryCount = 0;
     do {
@@ -627,20 +328,246 @@ public class JarDeployer implements Serializable {
         "Unable to write to deploy directory: " + this.deployDirectory.getCanonicalPath());
   }
 
-  private byte[] getJarContent(File jarFile) throws IOException {
-    InputStream inputStream = new FileInputStream(jarFile);
+  final Pattern oldNamingPattern = Pattern.compile("^vf\\.gf#(.*)\\.jar#(\\d+)$");
+
+  /*
+   * In Geode 1.1.0, the deployed version of 'myjar.jar' would be named 'vf.gf#myjar.jar#1'. Now it
+   * is be named 'myjar.v1.jar'. We need to rename all existing deployed jars to the new convention
+   * if this is the first time starting up with the new naming format.
+   */
+  protected void renameJarsWithOldNamingConvention() throws IOException {
+    Set<File> jarsWithOldNamingConvention = findJarsWithOldNamingConvention();
+
+    if (jarsWithOldNamingConvention.isEmpty()) {
+      return;
+    }
+
+    for (File jar : jarsWithOldNamingConvention) {
+      renameJarWithOldNamingConvention(jar);
+    }
+  }
+
+  protected Set<File> findJarsWithOldNamingConvention() {
+    return Stream.of(this.deployDirectory.listFiles())
+        .filter((File file) -> isOldNamingConvention(file.getName())).collect(toSet());
+  }
+
+  protected boolean isOldNamingConvention(String fileName) {
+    return oldNamingPattern.matcher(fileName).matches();
+  }
+
+  private void renameJarWithOldNamingConvention(File oldJar) throws IOException {
+    Matcher matcher = oldNamingPattern.matcher(oldJar.getName());
+    if (!matcher.matches()) {
+      throw new IllegalArgumentException("The given jar " + oldJar.getCanonicalPath()
+          + " does not match the old naming convention");
+    }
+
+    String unversionedJarNameWithoutExtension = matcher.group(1);
+    String jarVersion = matcher.group(2);
+    String newJarName = unversionedJarNameWithoutExtension + ".v" + jarVersion + ".jar";
+
+    File newJar = new File(this.deployDirectory, newJarName);
+    logger.debug("Renaming deployed jar from " + oldJar.getCanonicalPath() + " to "
+        + newJar.getCanonicalPath());
+
+    FileUtils.moveFile(oldJar, newJar);
+    FileUtils.deleteQuietly(oldJar);
+  }
+
+  /**
+   * Re-deploy all previously deployed JAR files.
+   */
+  public void loadPreviouslyDeployedJars() {
+    lock.lock();
+    try {
+      verifyWritableDeployDirectory();
+      renameJarsWithOldNamingConvention();
+
+      final Set<String> jarNames = findDistinctDeployedJars();
+      if (jarNames.isEmpty()) {
+        return;
+      }
+
+      Map<String, DeployedJar> latestVersionOfEachJar = new LinkedHashMap<>();
+
+      for (String jarName : jarNames) {
+        final File[] jarFiles = findSortedOldVersionsOfJar(jarName);
+
+        Optional<File> latestValidDeployedJarOptional =
+            Arrays.stream(jarFiles).filter(Objects::nonNull).filter(jarFile -> {
+              try {
+                return DeployedJar.isValidJarContent(FileUtils.readFileToByteArray(jarFile));
+              } catch (IOException e) {
+                return false;
+              }
+            }).findFirst();
+
+        if (!latestValidDeployedJarOptional.isPresent()) {
+          // No valid version of this jar
+          continue;
+        }
+
+        File latestValidDeployedJar = latestValidDeployedJarOptional.get();
+        latestVersionOfEachJar.put(jarName, new DeployedJar(latestValidDeployedJar, jarName));
+
+        // Remove any old left-behind versions of this JAR file
+        for (File jarFile : jarFiles) {
+          if (!latestValidDeployedJar.equals(jarFile)) {
+            FileUtils.deleteQuietly(jarFile);
+          }
+        }
+      }
+
+      registerNewVersions(latestVersionOfEachJar.values().stream().collect(toList()));
+      // ClassPathLoader.getLatest().deploy(latestVersionOfEachJar.keySet().toArray(),
+      // latestVersionOfEachJar.values().toArray())
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    } finally {
+      lock.unlock();
+    }
+  }
+
+
+  public URL[] getDeployedJarURLs() {
+    return this.deployedJars.values().stream().map(DeployedJar::getFileURL).toArray(URL[]::new);
+
+  }
+
+  public List<DeployedJar> registerNewVersions(List<DeployedJar> deployedJars)
+      throws ClassNotFoundException {
+    lock.lock();
+    try {
+      for (DeployedJar deployedJar : deployedJars) {
+        if (deployedJar != null) {
+          DeployedJar oldJar = this.deployedJars.put(deployedJar.getJarName(), deployedJar);
+          if (oldJar != null) {
+            oldJar.cleanUp();
+          }
+        }
+      }
+
+      ClassPathLoader.getLatest().rebuildClassLoaderForDeployedJars();
+
+      for (DeployedJar deployedJar : deployedJars) {
+        if (deployedJar != null) {
+          deployedJar.loadClassesAndRegisterFunctions();
+        }
+      }
+    } finally {
+      lock.unlock();
+    }
+
+    return deployedJars;
+  }
+
+  /**
+   * Deploy the given JAR files.
+   * 
+   * @param jarNames Array of names of the JAR files to deploy.
+   * @param jarBytes Array of contents of the JAR files to deploy.
+   * @return An array of newly created JAR class loaders. Entries will be null for an JARs that were
+   *         already deployed.
+   * @throws IOException When there's an error saving the JAR file to disk
+   */
+  public List<DeployedJar> deploy(final String jarNames[], final byte[][] jarBytes)
+      throws IOException, ClassNotFoundException {
+    DeployedJar[] deployedJars = new DeployedJar[jarNames.length];
+
+    for (int i = 0; i < jarNames.length; i++) {
+      if (!DeployedJar.isValidJarContent(jarBytes[i])) {
+        throw new IllegalArgumentException(
+            "File does not contain valid JAR content: " + jarNames[i]);
+      }
+    }
+
+    lock.lock();
+    try {
+      for (int i = 0; i < jarNames.length; i++) {
+        String jarName = jarNames[i];
+        byte[] newJarBytes = jarBytes[i];
+
+        boolean shouldDeployNewVersion = shouldDeployNewVersion(jarName, newJarBytes);
+
+        if (shouldDeployNewVersion) {
+          deployedJars[i] = deployWithoutRegistering(jarName, newJarBytes);
+        } else {
+          deployedJars[i] = null;
+        }
+      }
+
+      return registerNewVersions(Arrays.asList(deployedJars));
+    } finally {
+      lock.unlock();
+    }
+  }
+
+  private boolean shouldDeployNewVersion(String jarName, byte[] newJarBytes) throws IOException {
+    DeployedJar oldDeployedJar = this.deployedJars.get(jarName);
+
+    if (oldDeployedJar == null) {
+      return true;
+    }
+
+    if (oldDeployedJar.hasSameContentAs(newJarBytes)) {
+      logger.warn("Jar is identical to the latest deployed version: ",
+          oldDeployedJar.getFileCanonicalPath());
+
+      return false;
+    }
+
+    return true;
+  }
+
+  public DeployedJar findDeployedJar(String jarName) {
+    return this.deployedJars.get(jarName);
+  }
+
+  public DeployedJar deploy(final String jarName, final byte[] jarBytes)
+      throws IOException, ClassNotFoundException {
+    lock.lock();
+
     try {
-      final ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream();
-      final byte[] bytes = new byte[4096];
+      List<DeployedJar> deployedJars = deploy(new String[] {jarName}, new byte[][] {jarBytes});
+      if (deployedJars == null || deployedJars.size() == 0) {
+        return null;
+      }
+
+      return deployedJars.get(0);
+    } finally {
+      lock.unlock();
+    }
+
+  }
 
-      int bytesRead;
-      while (((bytesRead = inputStream.read(bytes)) != -1)) {
-        byteOutStream.write(bytes, 0, bytesRead);
+  public Map<String, DeployedJar> getDeployedJars() {
+    return Collections.unmodifiableMap(this.deployedJars);
+  }
+
+  /**
+   * Undeploy the given JAR file.
+   * 
+   * @param jarName The name of the JAR file to undeploy
+   * @return The path to the location on disk where the JAR file had been deployed
+   * @throws IOException If there's a problem deleting the file
+   */
+  public String undeploy(final String jarName) throws IOException {
+    lock.lock();
+
+    try {
+      DeployedJar deployedJar = deployedJars.remove(jarName);
+      if (deployedJar == null) {
+        throw new IllegalArgumentException("JAR not deployed");
       }
 
-      return byteOutStream.toByteArray();
+      ClassPathLoader.getLatest().rebuildClassLoaderForDeployedJars();
+
+      deployedJar.cleanUp();
+
+      return deployedJar.getFileCanonicalPath();
     } finally {
-      inputStream.close();
+      lock.unlock();
     }
   }
 }

http://git-wip-us.apache.org/repos/asf/geode/blob/6fd2d123/geode-core/src/main/java/org/apache/geode/internal/cache/ClusterConfigurationLoader.java
----------------------------------------------------------------------
diff --git a/geode-core/src/main/java/org/apache/geode/internal/cache/ClusterConfigurationLoader.java b/geode-core/src/main/java/org/apache/geode/internal/cache/ClusterConfigurationLoader.java
index f904af1..2b627b2 100644
--- a/geode-core/src/main/java/org/apache/geode/internal/cache/ClusterConfigurationLoader.java
+++ b/geode-core/src/main/java/org/apache/geode/internal/cache/ClusterConfigurationLoader.java
@@ -24,9 +24,11 @@ import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Properties;
 import java.util.Set;
 
+import org.apache.geode.internal.ClassPathLoader;
 import org.apache.logging.log4j.Logger;
 
 import org.apache.geode.UnmodifiableException;
@@ -35,7 +37,7 @@ import org.apache.geode.distributed.internal.DistributionConfig;
 import org.apache.geode.distributed.internal.ClusterConfigurationService;
 import org.apache.geode.distributed.internal.tcpserver.TcpClient;
 import org.apache.geode.internal.ConfigSource;
-import org.apache.geode.internal.JarClassLoader;
+import org.apache.geode.internal.DeployedJar;
 import org.apache.geode.internal.JarDeployer;
 import org.apache.geode.internal.admin.remote.DistributionLocatorId;
 import org.apache.geode.internal.i18n.LocalizedStrings;
@@ -56,45 +58,38 @@ public class ClusterConfigurationLoader {
    * 
    * @param cache Cache of this member
    * @param response {@link ConfigurationResponse} received from the locators
-   * @throws IOException
-   * @throws ClassNotFoundException
    */
   public static void deployJarsReceivedFromClusterConfiguration(Cache cache,
       ConfigurationResponse response) throws IOException, ClassNotFoundException {
-    if (response == null)
+    if (response == null) {
       return;
+    }
 
     String[] jarFileNames = response.getJarNames();
     byte[][] jarBytes = response.getJars();
 
-    final JarDeployer jarDeployer = new JarDeployer(
-        ((GemFireCacheImpl) cache).getDistributedSystem().getConfig().getDeployWorkingDir());
-
-    /******
-     * Un-deploy the existing jars, deployed during cache creation, do not delete anything
-     */
-
     if (jarFileNames != null && jarBytes != null) {
-      JarClassLoader[] jarClassLoaders = jarDeployer.deploy(jarFileNames, jarBytes);
-      for (int i = 0; i < jarFileNames.length; i++) {
-        if (jarClassLoaders[i] != null) {
-          logger.info("Deployed " + (jarClassLoaders[i].getFileCanonicalPath()));
-        }
-      }
+      List<DeployedJar> deployedJars =
+          ClassPathLoader.getLatest().getJarDeployer().deploy(jarFileNames, jarBytes);
+
+      deployedJars.stream().filter(Objects::nonNull)
+          .forEach((jar) -> logger.info("Deployed " + (jar.getFile().getAbsolutePath())));
     }
+    // TODO: Jared - Does this need to actually undeploy extra jars like the javadoc says?
   }
 
   /***
    * Apply the cache-xml cluster configuration on this member
-   * 
+   *
    * @param cache Cache created for this member
    * @param response {@link ConfigurationResponse} containing the requested {@link Configuration}
    * @param config this member's config.
    */
   public static void applyClusterXmlConfiguration(Cache cache, ConfigurationResponse response,
       DistributionConfig config) {
-    if (response == null || response.getRequestedConfiguration().isEmpty())
+    if (response == null || response.getRequestedConfiguration().isEmpty()) {
       return;
+    }
 
     List<String> groups = getGroups(config);
     Map<String, Configuration> requestedConfiguration = response.getRequestedConfiguration();
@@ -138,15 +133,16 @@ public class ClusterConfigurationLoader {
 
   /***
    * Apply the gemfire properties cluster configuration on this member
-   * 
+   *
    * @param cache Cache created for this member
    * @param response {@link ConfigurationResponse} containing the requested {@link Configuration}
    * @param config this member's config
    */
   public static void applyClusterPropertiesConfiguration(Cache cache,
       ConfigurationResponse response, DistributionConfig config) {
-    if (response == null || response.getRequestedConfiguration().isEmpty())
+    if (response == null || response.getRequestedConfiguration().isEmpty()) {
       return;
+    }
 
     List<String> groups = getGroups(config);
     Map<String, Configuration> requestedConfiguration = response.getRequestedConfiguration();
@@ -189,8 +185,6 @@ public class ClusterConfigurationLoader {
    * 
    * @param config this member's configuration.
    * @return {@link ConfigurationResponse}
-   * @throws ClusterConfigurationNotAvailableException
-   * @throws UnknownHostException
    */
   public static ConfigurationResponse requestConfigurationFromLocators(DistributionConfig config,
       List<String> locatorList)

http://git-wip-us.apache.org/repos/asf/geode/blob/6fd2d123/geode-core/src/main/java/org/apache/geode/internal/cache/GemFireCacheImpl.java
----------------------------------------------------------------------
diff --git a/geode-core/src/main/java/org/apache/geode/internal/cache/GemFireCacheImpl.java b/geode-core/src/main/java/org/apache/geode/internal/cache/GemFireCacheImpl.java
index 08f916b..fb311e7 100755
--- a/geode-core/src/main/java/org/apache/geode/internal/cache/GemFireCacheImpl.java
+++ b/geode-core/src/main/java/org/apache/geode/internal/cache/GemFireCacheImpl.java
@@ -1192,7 +1192,7 @@ public class GemFireCacheImpl
       listener.cacheCreated(this);
     }
 
-    ClassPathLoader.setLatestToDefault();
+    ClassPathLoader.setLatestToDefault(this.system.getConfig().getDeployWorkingDir());
 
     // request and check cluster configuration
     ConfigurationResponse configurationResponse = requestSharedConfiguration();
@@ -1239,7 +1239,7 @@ public class GemFireCacheImpl
 
     try {
       // Deploy all the jars from the deploy working dir.
-      new JarDeployer(this.system.getConfig().getDeployWorkingDir()).loadPreviouslyDeployedJars();
+      ClassPathLoader.getLatest().getJarDeployer().loadPreviouslyDeployedJars();
       ClusterConfigurationLoader.applyClusterXmlConfiguration(this, configurationResponse,
           system.getConfig());
       initializeDeclarativeCache();

http://git-wip-us.apache.org/repos/asf/geode/blob/6fd2d123/geode-core/src/main/java/org/apache/geode/internal/cache/persistence/BackupManager.java
----------------------------------------------------------------------
diff --git a/geode-core/src/main/java/org/apache/geode/internal/cache/persistence/BackupManager.java b/geode-core/src/main/java/org/apache/geode/internal/cache/persistence/BackupManager.java
index d052551..deb53cb 100644
--- a/geode-core/src/main/java/org/apache/geode/internal/cache/persistence/BackupManager.java
+++ b/geode-core/src/main/java/org/apache/geode/internal/cache/persistence/BackupManager.java
@@ -22,7 +22,8 @@ import org.apache.geode.distributed.internal.DM;
 import org.apache.geode.distributed.internal.DistributionConfig;
 import org.apache.geode.distributed.internal.MembershipListener;
 import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
-import org.apache.geode.internal.JarClassLoader;
+import org.apache.geode.internal.ClassPathLoader;
+import org.apache.geode.internal.DeployedJar;
 import org.apache.geode.internal.JarDeployer;
 import org.apache.geode.internal.cache.DiskStoreImpl;
 import org.apache.geode.internal.cache.GemFireCacheImpl;
@@ -290,21 +291,20 @@ public class BackupManager implements MembershipListener {
     JarDeployer deployer = null;
 
     try {
-      deployer = new JarDeployer();
-
       /*
        * Suspend any user deployed jar file updates during this backup.
        */
+      deployer = ClassPathLoader.getLatest().getJarDeployer();
       deployer.suspendAll();
 
-      List<JarClassLoader> jarList = deployer.findJarClassLoaders();
+      List<DeployedJar> jarList = deployer.findDeployedJars();
       if (!jarList.isEmpty()) {
         File userBackupDir = new File(backupDir, USER_FILES);
         if (!userBackupDir.exists()) {
           userBackupDir.mkdir();
         }
 
-        for (JarClassLoader loader : jarList) {
+        for (DeployedJar loader : jarList) {
           File source = new File(loader.getFileCanonicalPath());
           File dest = new File(userBackupDir, source.getName());
           if (source.isDirectory()) {

http://git-wip-us.apache.org/repos/asf/geode/blob/6fd2d123/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/DeployFunction.java
----------------------------------------------------------------------
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/DeployFunction.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/DeployFunction.java
index 5f1f161..148aa5f 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/DeployFunction.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/DeployFunction.java
@@ -15,8 +15,10 @@
 package org.apache.geode.management.internal.cli.functions;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
+import org.apache.geode.internal.ClassPathLoader;
 import org.apache.logging.log4j.Logger;
 
 import org.apache.geode.SystemFailure;
@@ -27,7 +29,7 @@ import org.apache.geode.cache.execute.Function;
 import org.apache.geode.cache.execute.FunctionContext;
 import org.apache.geode.distributed.DistributedMember;
 import org.apache.geode.internal.InternalEntity;
-import org.apache.geode.internal.JarClassLoader;
+import org.apache.geode.internal.DeployedJar;
 import org.apache.geode.internal.JarDeployer;
 import org.apache.geode.internal.cache.GemFireCacheImpl;
 import org.apache.geode.internal.logging.LogService;
@@ -62,11 +64,12 @@ public class DeployFunction implements Function, InternalEntity {
       }
 
       List<String> deployedList = new ArrayList<String>();
-      JarClassLoader[] jarClassLoaders = jarDeployer.deploy(jarFilenames, jarBytes);
+      List<DeployedJar> jarClassLoaders =
+          ClassPathLoader.getLatest().getJarDeployer().deploy(jarFilenames, jarBytes);
       for (int i = 0; i < jarFilenames.length; i++) {
         deployedList.add(jarFilenames[i]);
-        if (jarClassLoaders[i] != null) {
-          deployedList.add(jarClassLoaders[i].getFileCanonicalPath());
+        if (jarClassLoaders.get(i) != null) {
+          deployedList.add(jarClassLoaders.get(i).getFileCanonicalPath());
         } else {
           deployedList.add("Already deployed");
         }

http://git-wip-us.apache.org/repos/asf/geode/blob/6fd2d123/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/ListDeployedFunction.java
----------------------------------------------------------------------
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/ListDeployedFunction.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/ListDeployedFunction.java
index 8df24db..3d6a321 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/ListDeployedFunction.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/ListDeployedFunction.java
@@ -16,6 +16,8 @@ package org.apache.geode.management.internal.cli.functions;
 
 import java.util.List;
 
+import org.apache.geode.internal.ClassPathLoader;
+import org.apache.geode.internal.DeployedJar;
 import org.apache.logging.log4j.Logger;
 
 import org.apache.geode.SystemFailure;
@@ -26,7 +28,6 @@ import org.apache.geode.cache.execute.Function;
 import org.apache.geode.cache.execute.FunctionContext;
 import org.apache.geode.distributed.DistributedMember;
 import org.apache.geode.internal.InternalEntity;
-import org.apache.geode.internal.JarClassLoader;
 import org.apache.geode.internal.JarDeployer;
 import org.apache.geode.internal.cache.GemFireCacheImpl;
 import org.apache.geode.internal.logging.LogService;
@@ -45,8 +46,7 @@ public class ListDeployedFunction implements Function, InternalEntity {
 
     try {
       Cache cache = CacheFactory.getAnyInstance();
-      final JarDeployer jarDeployer = new JarDeployer(
-          ((GemFireCacheImpl) cache).getDistributedSystem().getConfig().getDeployWorkingDir());
+      final JarDeployer jarDeployer = ClassPathLoader.getLatest().getJarDeployer();
 
       DistributedMember member = cache.getDistributedSystem().getDistributedMember();
 
@@ -56,10 +56,10 @@ public class ListDeployedFunction implements Function, InternalEntity {
         memberId = member.getName();
       }
 
-      final List<JarClassLoader> jarClassLoaders = jarDeployer.findJarClassLoaders();
+      final List<DeployedJar> jarClassLoaders = jarDeployer.findDeployedJars();
       final String[] jars = new String[jarClassLoaders.size() * 2];
       int index = 0;
-      for (JarClassLoader jarClassLoader : jarClassLoaders) {
+      for (DeployedJar jarClassLoader : jarClassLoaders) {
         jars[index++] = jarClassLoader.getJarName();
         jars[index++] = jarClassLoader.getFileCanonicalPath();
       }

http://git-wip-us.apache.org/repos/asf/geode/blob/6fd2d123/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/UndeployFunction.java
----------------------------------------------------------------------
diff --git a/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/UndeployFunction.java b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/UndeployFunction.java
index 3f05082..14d875e 100644
--- a/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/UndeployFunction.java
+++ b/geode-core/src/main/java/org/apache/geode/management/internal/cli/functions/UndeployFunction.java
@@ -18,6 +18,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.StringTokenizer;
 
+import org.apache.geode.internal.ClassPathLoader;
 import org.apache.logging.log4j.Logger;
 
 import org.apache.geode.SystemFailure;
@@ -28,7 +29,7 @@ import org.apache.geode.cache.execute.Function;
 import org.apache.geode.cache.execute.FunctionContext;
 import org.apache.geode.distributed.DistributedMember;
 import org.apache.geode.internal.InternalEntity;
-import org.apache.geode.internal.JarClassLoader;
+import org.apache.geode.internal.DeployedJar;
 import org.apache.geode.internal.JarDeployer;
 import org.apache.geode.internal.cache.GemFireCacheImpl;
 import org.apache.geode.internal.logging.LogService;
@@ -50,8 +51,7 @@ public class UndeployFunction implements Function, InternalEntity {
       final String jarFilenameList = (String) args[0]; // Comma separated
       Cache cache = CacheFactory.getAnyInstance();
 
-      final JarDeployer jarDeployer = new JarDeployer(
-          ((GemFireCacheImpl) cache).getDistributedSystem().getConfig().getDeployWorkingDir());
+      final JarDeployer jarDeployer = ClassPathLoader.getLatest().getJarDeployer();
 
       DistributedMember member = cache.getDistributedSystem().getDistributedMember();
 
@@ -63,13 +63,14 @@ public class UndeployFunction implements Function, InternalEntity {
 
       String[] undeployedJars = new String[0];
       if (jarFilenameList == null || jarFilenameList.equals("")) {
-        final List<JarClassLoader> jarClassLoaders = jarDeployer.findJarClassLoaders();
+        final List<DeployedJar> jarClassLoaders = jarDeployer.findDeployedJars();
         undeployedJars = new String[jarClassLoaders.size() * 2];
         int index = 0;
-        for (JarClassLoader jarClassLoader : jarClassLoaders) {
+        for (DeployedJar jarClassLoader : jarClassLoaders) {
           undeployedJars[index++] = jarClassLoader.getJarName();
           try {
-            undeployedJars[index++] = jarDeployer.undeploy(jarClassLoader.getJarName());
+            undeployedJars[index++] =
+                ClassPathLoader.getLatest().getJarDeployer().undeploy(jarClassLoader.getJarName());
           } catch (IllegalArgumentException iaex) {
             // It's okay for it to have have been uneployed from this server
             undeployedJars[index++] = iaex.getMessage();
@@ -82,7 +83,7 @@ public class UndeployFunction implements Function, InternalEntity {
           String jarFilename = jarTokenizer.nextToken().trim();
           try {
             undeployedList.add(jarFilename);
-            undeployedList.add(jarDeployer.undeploy(jarFilename));
+            undeployedList.add(ClassPathLoader.getLatest().getJarDeployer().undeploy(jarFilename));
           } catch (IllegalArgumentException iaex) {
             // It's okay for it to not have been deployed to this server
             undeployedList.add(iaex.getMessage());

http://git-wip-us.apache.org/repos/asf/geode/blob/6fd2d123/geode-core/src/test/java/org/apache/geode/internal/ClassPathLoaderIntegrationTest.java
----------------------------------------------------------------------
diff --git a/geode-core/src/test/java/org/apache/geode/internal/ClassPathLoaderIntegrationTest.java b/geode-core/src/test/java/org/apache/geode/internal/ClassPathLoaderIntegrationTest.java
index c52d575..d30feb6 100644
--- a/geode-core/src/test/java/org/apache/geode/internal/ClassPathLoaderIntegrationTest.java
+++ b/geode-core/src/test/java/org/apache/geode/internal/ClassPathLoaderIntegrationTest.java
@@ -14,6 +14,8 @@
  */
 package org.apache.geode.internal;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.Assert.*;
 
 import java.io.BufferedInputStream;
@@ -24,11 +26,20 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URL;
 import java.util.Enumeration;
+import java.util.List;
+import java.util.Properties;
 import java.util.Vector;
 
 import org.apache.bcel.Constants;
 import org.apache.bcel.classfile.JavaClass;
 import org.apache.bcel.generic.ClassGen;
+import org.apache.commons.io.FileUtils;
+import org.apache.geode.cache.execute.Execution;
+import org.apache.geode.cache.execute.FunctionService;
+import org.apache.geode.cache.execute.ResultCollector;
+import org.apache.geode.distributed.DistributedSystem;
+import org.apache.geode.internal.cache.GemFireCacheImpl;
+import org.apache.geode.test.dunit.rules.ServerStarterRule;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -53,8 +64,9 @@ public class ClassPathLoaderIntegrationTest {
 
   private static final int TEMP_FILE_BYTES_COUNT = 256;
 
-  private volatile File tempFile;
-  private volatile File tempFile2;
+  private File tempFile;
+  private File tempFile2;
+  private File extLibsDir;
 
   @Rule
   public RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();
@@ -65,8 +77,9 @@ public class ClassPathLoaderIntegrationTest {
   @Before
   public void setUp() throws Exception {
     System.setProperty(ClassPathLoader.EXCLUDE_TCCL_PROPERTY, "false");
-    System.setProperty(ClassPathLoader.EXT_LIB_DIR_PARENT_PROPERTY,
-        this.temporaryFolder.getRoot().getAbsolutePath());
+
+    extLibsDir = new File(this.temporaryFolder.getRoot(), "ext");
+    extLibsDir.mkdirs();
 
     this.tempFile = this.temporaryFolder.newFile("tempFile1.tmp");
     FileOutputStream fos = new FileOutputStream(this.tempFile);
@@ -77,98 +90,248 @@ public class ClassPathLoaderIntegrationTest {
     fos = new FileOutputStream(this.tempFile2);
     fos.write(new byte[TEMP_FILE_BYTES_COUNT]);
     fos.close();
+
+    System.setProperty("user.dir", temporaryFolder.getRoot().getAbsolutePath());
+    ClassPathLoader.setLatestToDefault(temporaryFolder.getRoot());
   }
 
-  /**
-   * Verifies that <tt>getResource</tt> works with custom loader from {@link ClassPathLoader}.
-   */
+
   @Test
-  public void testGetResourceWithCustomLoader() throws Exception {
-    System.out.println("\nStarting ClassPathLoaderTest#testGetResourceWithCustomLoader");
+  public void testDeployFileAndChange() throws IOException, ClassNotFoundException {
+    String jarName = "JarDeployerIntegrationTest.jar";
 
-    ClassPathLoader dcl = ClassPathLoader.createWithDefaults(false);
-    dcl = dcl.addOrReplace(new GeneratingClassLoader());
+    String classAResource = "integration/parent/ClassA.class";
+    String classBResource = "integration/parent/ClassB.class";
 
-    String resourceToGet = "com/nowhere/testGetResourceWithCustomLoader.rsc";
-    URL url = dcl.getResource(resourceToGet);
-    assertNotNull(url);
+    String classAName = "integration.parent.ClassA";
+    String classBName = "integration.parent.ClassB";
 
-    InputStream is = url != null ? url.openStream() : null;
-    assertNotNull(is);
+    byte[] firstJarBytes = createJarWithClass("ClassA");
 
-    int totalBytesRead = 0;
-    byte[] input = new byte[128];
+    // First deploy of the JAR file
+    File firstDeployedJarFile =
+        ClassPathLoader.getLatest().getJarDeployer().deploy(jarName, firstJarBytes).getFile();
 
-    BufferedInputStream bis = new BufferedInputStream(is);
-    for (int bytesRead = bis.read(input); bytesRead > -1;) {
-      totalBytesRead += bytesRead;
-      bytesRead = bis.read(input);
-    }
-    bis.close();
+    assertThat(firstDeployedJarFile).exists().hasBinaryContent(firstJarBytes);
+    assertThat(firstDeployedJarFile.getName()).contains(".v1.").doesNotContain(".v2.");
+
+    assertThatClassCanBeLoaded(classAName);
+    assertThatClassCannotBeLoaded(classBName);
+
+    assertThatResourceCanBeLoaded(classAResource);
+    assertThatResourceCannotBeLoaded(classBResource);
 
-    assertEquals(TEMP_FILE_BYTES_COUNT, totalBytesRead);
+    // Now deploy an updated JAR file and make sure that the next version of the JAR file
+    // was created and the first one is no longer used
+    byte[] secondJarBytes = createJarWithClass("ClassB");
+
+    File secondDeployedJarFile =
+        ClassPathLoader.getLatest().getJarDeployer().deploy(jarName, secondJarBytes).getFile();
+
+    assertThat(secondDeployedJarFile).exists().hasBinaryContent(secondJarBytes);
+    assertThat(secondDeployedJarFile.getName()).contains(".v2.").doesNotContain(".v1.");
+
+    assertThatClassCanBeLoaded(classBName);
+    assertThatClassCannotBeLoaded(classAName);
+
+    assertThatResourceCanBeLoaded(classBResource);
+    assertThatResourceCannotBeLoaded(classAResource);
+
+    // Now undeploy JAR and make sure it gets cleaned up
+    ClassPathLoader.getLatest().getJarDeployer().undeploy(jarName);
+    assertThatClassCannotBeLoaded(classBName);
+    assertThatClassCannotBeLoaded(classAName);
+
+    assertThatResourceCannotBeLoaded(classBResource);
+    assertThatResourceCannotBeLoaded(classAResource);
   }
 
-  /**
-   * Verifies that <tt>getResources</tt> works with custom loader from {@link ClassPathLoader}.
-   */
   @Test
-  public void testGetResourcesWithCustomLoader() throws Exception {
-    System.out.println("\nStarting ClassPathLoaderTest#testGetResourceWithCustomLoader");
+  public void testDeployNoUpdateWhenNoChange() throws IOException, ClassNotFoundException {
+    String jarName = "JarDeployerIntegrationTest.jar";
 
-    ClassPathLoader dcl = ClassPathLoader.createWithDefaults(false);
-    dcl = dcl.addOrReplace(new GeneratingClassLoader());
+    // First deploy of the JAR file
+    byte[] jarBytes = new ClassBuilder().createJarFromName("JarDeployerDUnitDNUWNC");
+    DeployedJar jarClassLoader =
+        ClassPathLoader.getLatest().getJarDeployer().deploy(jarName, jarBytes);
+    File deployedJar = new File(jarClassLoader.getFileCanonicalPath());
 
-    String resourceToGet = "com/nowhere/testGetResourceWithCustomLoader.rsc";
-    Enumeration<URL> urls = dcl.getResources(resourceToGet);
-    assertNotNull(urls);
-    assertTrue(urls.hasMoreElements());
+    assertThat(deployedJar).exists();
+    assertThat(deployedJar.getName()).contains(".v1.");
 
-    URL url = urls.nextElement();
-    InputStream is = url != null ? url.openStream() : null;
-    assertNotNull(is);
+    // Re-deploy of the same JAR should do nothing
+    DeployedJar newJarClassLoader =
+        ClassPathLoader.getLatest().getJarDeployer().deploy(jarName, jarBytes);
+    assertThat(newJarClassLoader).isNull();
+    assertThat(deployedJar).exists();
 
-    int totalBytesRead = 0;
-    byte[] input = new byte[128];
+  }
 
-    BufferedInputStream bis = new BufferedInputStream(is);
-    for (int bytesRead = bis.read(input); bytesRead > -1;) {
-      totalBytesRead += bytesRead;
-      bytesRead = bis.read(input);
-    }
-    bis.close();
+  @Test
+  public void testDeployWithExistingDependentJars() throws Exception {
+    ClassBuilder classBuilder = new ClassBuilder();
+    final File parentJarFile =
+        new File(temporaryFolder.getRoot(), "JarDeployerDUnitAParent.v1.jar");
+    final File usesJarFile = new File(temporaryFolder.getRoot(), "JarDeployerDUnitUses.v1.jar");
+    final File functionJarFile =
+        new File(temporaryFolder.getRoot(), "JarDeployerDUnitFunction.v1.jar");
+
+    // Write out a JAR files.
+    StringBuffer stringBuffer = new StringBuffer();
+    stringBuffer.append("package jddunit.parent;");
+    stringBuffer.append("public class JarDeployerDUnitParent {");
+    stringBuffer.append("public String getValueParent() {");
+    stringBuffer.append("return \"PARENT\";}}");
+
+    byte[] jarBytes = classBuilder.createJarFromClassContent(
+        "jddunit/parent/JarDeployerDUnitParent", stringBuffer.toString());
+    FileOutputStream outStream = new FileOutputStream(parentJarFile);
+    outStream.write(jarBytes);
+    outStream.close();
+
+    stringBuffer = new StringBuffer();
+    stringBuffer.append("package jddunit.uses;");
+    stringBuffer.append("public class JarDeployerDUnitUses {");
+    stringBuffer.append("public String getValueUses() {");
+    stringBuffer.append("return \"USES\";}}");
 
-    assertEquals(TEMP_FILE_BYTES_COUNT, totalBytesRead);
+    jarBytes = classBuilder.createJarFromClassContent("jddunit/uses/JarDeployerDUnitUses",
+        stringBuffer.toString());
+    outStream = new FileOutputStream(usesJarFile);
+    outStream.write(jarBytes);
+    outStream.close();
+
+    stringBuffer = new StringBuffer();
+    stringBuffer.append("package jddunit.function;");
+    stringBuffer.append("import jddunit.parent.JarDeployerDUnitParent;");
+    stringBuffer.append("import jddunit.uses.JarDeployerDUnitUses;");
+    stringBuffer.append("import org.apache.geode.cache.execute.Function;");
+    stringBuffer.append("import org.apache.geode.cache.execute.FunctionContext;");
+    stringBuffer.append(
+        "public class JarDeployerDUnitFunction  extends JarDeployerDUnitParent implements Function {");
+    stringBuffer.append("private JarDeployerDUnitUses uses = new JarDeployerDUnitUses();");
+    stringBuffer.append("public boolean hasResult() {return true;}");
+    stringBuffer.append(
+        "public void execute(FunctionContext context) {context.getResultSender().lastResult(getValueParent() + \":\" + uses.getValueUses());}");
+    stringBuffer.append("public String getId() {return \"JarDeployerDUnitFunction\";}");
+    stringBuffer.append("public boolean optimizeForWrite() {return false;}");
+    stringBuffer.append("public boolean isHA() {return false;}}");
+
+    ClassBuilder functionClassBuilder = new ClassBuilder();
+    functionClassBuilder.addToClassPath(parentJarFile.getAbsolutePath());
+    functionClassBuilder.addToClassPath(usesJarFile.getAbsolutePath());
+    jarBytes = functionClassBuilder.createJarFromClassContent(
+        "jddunit/function/JarDeployerDUnitFunction", stringBuffer.toString());
+    outStream = new FileOutputStream(functionJarFile);
+    outStream.write(jarBytes);
+    outStream.close();
+
+    Properties properties = new Properties();
+    properties.setProperty("user.dir", temporaryFolder.getRoot().getAbsolutePath());
+    ServerStarterRule serverStarterRule = new ServerStarterRule();
+    serverStarterRule.startServer();
+
+    GemFireCacheImpl gemFireCache = GemFireCacheImpl.getInstance();
+    DistributedSystem distributedSystem = gemFireCache.getDistributedSystem();
+    Execution execution =
+        FunctionService.onMember(distributedSystem, distributedSystem.getDistributedMember());
+    ResultCollector resultCollector = execution.execute("JarDeployerDUnitFunction");
+    @SuppressWarnings("unchecked")
+    List<String> result = (List<String>) resultCollector.getResult();
+    assertEquals("PARENT:USES", result.get(0));
+
+    serverStarterRule.after();
   }
 
-  /**
-   * Verifies that <tt>getResourceAsStream</tt> works with custom loader from
-   * {@link ClassPathLoader}.
-   */
   @Test
-  public void testGetResourceAsStreamWithCustomLoader() throws Exception {
-    System.out.println("\nStarting ClassPathLoaderTest#testGetResourceAsStreamWithCustomLoader");
+  public void deployNewVersionOfFunctionOverOldVersion() throws Exception {
+    File jarVersion1 = createVersionOfJar("Version1", "MyFunction", "MyJar.jar");
+    File jarVersion2 = createVersionOfJar("Version2", "MyFunction", "MyJar.jar");
 
-    ClassPathLoader dcl = ClassPathLoader.createWithDefaults(false);
-    dcl = dcl.addOrReplace(new GeneratingClassLoader());
+    Properties properties = new Properties();
+    properties.setProperty("user.dir", temporaryFolder.getRoot().getAbsolutePath());
+    ServerStarterRule serverStarterRule = new ServerStarterRule();
+    serverStarterRule.startServer();
 
-    String resourceToGet = "com/nowhere/testGetResourceAsStreamWithCustomLoader.rsc";
-    InputStream is = dcl.getResourceAsStream(resourceToGet);
-    assertNotNull(is);
+    GemFireCacheImpl gemFireCache = GemFireCacheImpl.getInstance();
+    DistributedSystem distributedSystem = gemFireCache.getDistributedSystem();
 
-    int totalBytesRead = 0;
-    byte[] input = new byte[128];
+    ClassPathLoader.getLatest().getJarDeployer().deploy("MyJar.jar",
+        FileUtils.readFileToByteArray(jarVersion1));
 
-    BufferedInputStream bis = new BufferedInputStream(is);
-    for (int bytesRead = bis.read(input); bytesRead > -1;) {
-      totalBytesRead += bytesRead;
-      bytesRead = bis.read(input);
-    }
-    bis.close();
+    assertThatClassCanBeLoaded("jddunit.function.MyFunction");
+    Execution execution =
+        FunctionService.onMember(distributedSystem, distributedSystem.getDistributedMember());
+
+    List<String> result = (List<String>) execution.execute("MyFunction").getResult();
+    assertThat(result.get(0)).isEqualTo("Version1");
+
+
+    ClassPathLoader.getLatest().getJarDeployer().deploy("MyJar.jar",
+        FileUtils.readFileToByteArray(jarVersion2));
+    result = (List<String>) execution.execute("MyFunction").getResult();
+    assertThat(result.get(0)).isEqualTo("Version2");
 
-    assertEquals(TEMP_FILE_BYTES_COUNT, totalBytesRead);
+
+    serverStarterRule.after();
   }
 
+
+  private File createVersionOfJar(String version, String functionName, String jarName)
+      throws IOException {
+    String classContents =
+        "package jddunit.function;" + "import org.apache.geode.cache.execute.Function;"
+            + "import org.apache.geode.cache.execute.FunctionContext;" + "public class "
+            + functionName + " implements Function {" + "public boolean hasResult() {return true;}"
+            + "public String getId() {return \"" + functionName + "\";}"
+            + "public void execute(FunctionContext context) {context.getResultSender().lastResult(\""
+            + version + "\");}}";
+
+    File jar = new File(this.temporaryFolder.newFolder(version), jarName);
+    ClassBuilder functionClassBuilder = new ClassBuilder();
+    functionClassBuilder.writeJarFromContent("jddunit/function/" + functionName, classContents,
+        jar);
+
+    return jar;
+  }
+
+  private void assertThatClassCanBeLoaded(String className) throws ClassNotFoundException {
+    assertThat(ClassPathLoader.getLatest().forName(className)).isNotNull();
+  }
+
+  private void assertThatClassCannotBeLoaded(String className) throws ClassNotFoundException {
+    assertThatThrownBy(() -> ClassPathLoader.getLatest().forName(className))
+        .isExactlyInstanceOf(ClassNotFoundException.class);
+  }
+
+  private void assertThatResourceCanBeLoaded(String resourceName) throws IOException {
+    // ClassPathLoader.getResource
+    assertThat(ClassPathLoader.getLatest().getResource(resourceName)).isNotNull();
+
+    // ClassPathLoader.getResources
+    Enumeration<URL> urls = ClassPathLoader.getLatest().getResources(resourceName);
+    assertThat(urls).isNotNull();
+    assertThat(urls.hasMoreElements()).isTrue();
+
+    // ClassPathLoader.getResourceAsStream
+    InputStream is = ClassPathLoader.getLatest().getResourceAsStream(resourceName);
+    assertThat(is).isNotNull();
+  }
+
+  private void assertThatResourceCannotBeLoaded(String resourceName) throws IOException {
+    // ClassPathLoader.getResource
+    assertThat(ClassPathLoader.getLatest().getResource(resourceName)).isNull();
+
+    // ClassPathLoader.getResources
+    Enumeration<URL> urls = ClassPathLoader.getLatest().getResources(resourceName);
+    assertThat(urls.hasMoreElements()).isFalse();
+
+    // ClassPathLoader.getResourceAsStream
+    InputStream is = ClassPathLoader.getLatest().getResourceAsStream(resourceName);
+    assertThat(is).isNull();
+  }
+
+
   /**
    * Verifies that <tt>getResource</tt> works with TCCL from {@link ClassPathLoader}.
    */
@@ -281,154 +444,6 @@ public class ClassPathLoaderIntegrationTest {
     }
   }
 
-  /**
-   * Verifies that JAR files found in the extlib directory will be correctly added to the
-   * {@link ClassPathLoader}.
-   */
-  @Test
-  public void testJarsInExtLib() throws Exception {
-    System.out.println("\nStarting ClassPathLoaderTest#testJarsInExtLib");
-
-    File EXT_LIB_DIR = ClassPathLoader.defineEXT_LIB_DIR();
-    EXT_LIB_DIR.mkdir();
-
-    File subdir = new File(EXT_LIB_DIR, "cplju");
-    subdir.mkdir();
-
-    final ClassBuilder classBuilder = new ClassBuilder();
-
-    writeJarBytesToFile(new File(EXT_LIB_DIR, "ClassPathLoaderJUnit1.jar"),
-        classBuilder.createJarFromClassContent("com/cpljunit1/ClassPathLoaderJUnit1",
-            "package com.cpljunit1; public class ClassPathLoaderJUnit1 {}"));
-    writeJarBytesToFile(new File(subdir, "ClassPathLoaderJUnit2.jar"),
-        classBuilder.createJarFromClassContent("com/cpljunit2/ClassPathLoaderJUnit2",
-            "package com.cpljunit2; public class ClassPathLoaderJUnit2 {}"));
-
-    ClassPathLoader classPathLoader = ClassPathLoader.createWithDefaults(false);
-    try {
-      classPathLoader.forName("com.cpljunit1.ClassPathLoaderJUnit1");
-    } catch (ClassNotFoundException cnfex) {
-      fail("JAR file not correctly added to Classpath");
-    }
-
-    try {
-      classPathLoader.forName("com.cpljunit2.ClassPathLoaderJUnit2");
-    } catch (ClassNotFoundException cnfex) {
-      fail("JAR file not correctly added to Classpath");
-    }
-
-    assertNotNull(classPathLoader.getResource("com/cpljunit2/ClassPathLoaderJUnit2.class"));
-
-    Enumeration<URL> urls = classPathLoader.getResources("com/cpljunit1");
-    if (!urls.hasMoreElements()) {
-      fail("Resources should return one element");
-    }
-  }
-
-  /**
-   * Verifies that the 3rd custom loader will get the resource. Parent cannot find it and TCCL is
-   * broken. This verifies that all custom loaders are checked and that the custom loaders are all
-   * checked before TCCL.
-   */
-  @Test
-  public void testGetResourceAsStreamWithMultipleCustomLoaders() throws Exception {
-    System.out
-        .println("\nStarting ClassPathLoaderTest#testGetResourceAsStreamWithMultipleCustomLoaders");
-
-    // create DCL such that the 3rd loader should find the resource
-    // first custom loader becomes parent which won't find anything
-    ClassPathLoader dcl = ClassPathLoader.createWithDefaults(false);
-    dcl = dcl.addOrReplace(new GeneratingClassLoader());
-    dcl = dcl.addOrReplace(new SimpleClassLoader(getClass().getClassLoader()));
-    dcl = dcl.addOrReplace(new NullClassLoader());
-
-    String resourceToGet = "com/nowhere/testGetResourceAsStreamWithMultipleCustomLoaders.rsc";
-
-    ClassLoader cl = Thread.currentThread().getContextClassLoader();
-    try {
-      // set TCCL to throw errors which makes sure we find before checking TCCL
-      Thread.currentThread().setContextClassLoader(new BrokenClassLoader());
-
-      InputStream is = dcl.getResourceAsStream(resourceToGet);
-      assertNotNull(is);
-      is.close();
-    } finally {
-      Thread.currentThread().setContextClassLoader(cl);
-    }
-  }
-
-  /**
-   * Verifies that the 3rd custom loader will get the resource. Parent cannot find it and TCCL is
-   * broken. This verifies that all custom loaders are checked and that the custom loaders are all
-   * checked before TCCL.
-   */
-  @Test
-  public void testGetResourceWithMultipleCustomLoaders() throws Exception {
-    System.out.println("\nStarting ClassPathLoaderTest#testGetResourceWithMultipleCustomLoaders");
-
-    // create DCL such that the 3rd loader should find the resource
-    // first custom loader becomes parent which won't find anything
-    ClassPathLoader dcl = ClassPathLoader.createWithDefaults(false);
-    dcl = dcl.addOrReplace(new GeneratingClassLoader());
-    dcl = dcl.addOrReplace(new SimpleClassLoader(getClass().getClassLoader()));
-    dcl = dcl.addOrReplace(new NullClassLoader());
-
-    String resourceToGet = "com/nowhere/testGetResourceWithMultipleCustomLoaders.rsc";
-
-    ClassLoader cl = Thread.currentThread().getContextClassLoader();
-    try {
-      // set TCCL to throw errors which makes sure we find before checking TCCL
-      Thread.currentThread().setContextClassLoader(new BrokenClassLoader());
-
-      URL url = dcl.getResource(resourceToGet);
-      assertNotNull(url);
-    } finally {
-      Thread.currentThread().setContextClassLoader(cl);
-    }
-  }
-
-  /**
-   * Verifies that the 3rd custom loader will get the resources. Parent cannot find it and TCCL is
-   * broken. This verifies that all custom loaders are checked and that the custom loaders are all
-   * checked before TCCL.
-   */
-  @Test
-  public void testGetResourcesWithMultipleCustomLoaders() throws Exception {
-    System.out.println("\nStarting ClassPathLoaderTest#testGetResourceWithMultipleCustomLoaders");
-
-    // create DCL such that the 3rd loader should find the resource
-    // first custom loader becomes parent which won't find anything
-    ClassPathLoader dcl = ClassPathLoader.createWithDefaults(false);
-    dcl = dcl.addOrReplace(new GeneratingClassLoader());
-    dcl = dcl.addOrReplace(new GeneratingClassLoader2());
-    dcl = dcl.addOrReplace(new SimpleClassLoader(getClass().getClassLoader()));
-    dcl = dcl.addOrReplace(new NullClassLoader());
-
-    String resourceToGet = "com/nowhere/testGetResourceWithMultipleCustomLoaders.rsc";
-
-    ClassLoader cl = Thread.currentThread().getContextClassLoader();
-    try {
-      // set TCCL to throw errors which makes sure we find before checking TCCL
-      Thread.currentThread().setContextClassLoader(new BrokenClassLoader());
-
-      Enumeration<URL> urls = dcl.getResources(resourceToGet);
-      assertNotNull(urls);
-      assertTrue(urls.hasMoreElements());
-
-      URL url = urls.nextElement();
-      assertNotNull(url);
-
-      // Should find two with unique URLs
-      assertTrue("Did not find all resources.", urls.hasMoreElements());
-      URL url2 = urls.nextElement();
-      assertNotNull(url2);
-      assertTrue("Resource URLs should be unique.", !url.equals(url2));
-
-    } finally {
-      Thread.currentThread().setContextClassLoader(cl);
-    }
-  }
-
   private void writeJarBytesToFile(File jarFile, byte[] jarBytes) throws IOException {
     final OutputStream outStream = new FileOutputStream(jarFile);
     outStream.write(jarBytes);
@@ -508,4 +523,11 @@ public class ClassPathLoaderIntegrationTest {
       return tempFile2;
     }
   }
+
+  private byte[] createJarWithClass(String className) throws IOException {
+    String stringBuilder = "package integration.parent;" + "public class " + className + " {}";
+
+    return new ClassBuilder().createJarFromClassContent("integration/parent/" + className,
+        stringBuilder);
+  }
 }