0

I need to test differences between executions of the same library, but different versions - and in runtime. Hence I need to load lots classes which have the same package names. The whole execution starts from just one class which has all the rest as its dependencies.

I tried to load library #1 as project files (i.e. by ClassPath class loader) and library #2 as jar and loading its classes by UrlClassLoader.

Problem is when I load a class from a UrlClassLoader - all the dependent classes are taken from library #1, which are already loaded by ClassPath Class loader.

I know the the class loader form a hierarchy starting from Bootstrap class loader and then ending your custom class loaders - but in which way you can make Java load not only one explicitly mentioned class, but all its dependencies tree from the custom class loader?

Paul Ilves
  • 21
  • 1
  • 7

1 Answers1

3

You can load one version from classpath and the rest of versions using URLClassLoader by passing parent: null and using reflection to instantiate the members.

An alternative is to use a custom class loader, based on an approach described here that may look like:

import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandlerFactory;

public class ParentLastClassLoader extends ClassLoader {

    private ClassLoader parentClassLoader;
    private URLClassLoader noParentClassLoader;

    public ParentLastClassLoader(URL[] urls, ClassLoader parent) {
        super(parent);
        this.noParentClassLoader = new URLClassLoader(urls, null);
    }

    public ParentLastClassLoader(URL[] urls) {
        super(Thread.currentThread().getContextClassLoader());
        this.noParentClassLoader = new URLClassLoader(urls, null);
    }

    public ParentLastClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(parent);
        this.noParentClassLoader = new URLClassLoader(urls, null, factory);
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        try {
            // resolve using child class loader
            return noParentClassLoader.loadClass(name);
        } catch (ClassNotFoundException ex) {
            // resolve using parent class loader
            return super.loadClass(name, resolve);
        }
    }
}

I created a POC for this in this Git repo. The details are in the README.md.

POC contents

Considering that you have a library with multiple versions (project v1, v2, v3) consisting on following pieces:

We want our library to have this interface:

public interface Core {

    String getVersion();
    String getDependencyVersion();
}

Which is implemented by:

package sample.multiversion;

import sample.multiversion.deps.CoreDependency;

public class ImportantCore implements Core {

    private Utility utility;
    private CoreDependency coreDependency;

    public ImportantCore() {
        utility = new Utility();
        coreDependency = new CoreDependency();
    }

    public String getVersion() {
        return utility.getVersion();
    }

    public String getDependencyVersion() {
        return coreDependency.getVersion();
    }
}

that is using another class of the same library:

package sample.multiversion;

public class Utility {

    public String getVersion() {
        return "core-v1";
    }
}

Finally the library (projects v1, v2, v3) have a dependency (projects v1dep, v2dep, v3dep) containing:

package sample.multiversion.deps;

public class CoreDependency {

    public String getVersion() {
        return "core-dep-v1";
    }
}

Then we can load all three versions:

  1. V1 - from file
  2. V2 - from file
  3. V3 - from classpath

The code would be:

    // multiple versions of the same library to be used at the same time
    URL v1 = Paths.get("./../v1/build/libs/v1.jar").toUri().toURL();
    URL v2 = Paths.get("./../v2/build/libs/v2.jar").toUri().toURL();

    // library dependencies
    URL v1Dep = Paths.get("./../v1dep/build/libs/v1dep.jar").toUri().toURL();
    URL v2Dep = Paths.get("./../v2dep/build/libs/v2dep.jar").toUri().toURL();

    /**
     * version 1 and 2 loaders
     * - these loaders do not use the root loader - Thread.currentThread().getContextClassLoader()
     * - using the root loader new URLClassLoader(new URL[]{v1, v1Dep}, Thread.currentThread().getContextClassLoader());
     * will solve any class with the root loader and if no class is found then the child loader will be used
     * - because version 3 is loaded from classpath, the root loader should not be used to load version 1 and 2
     * - null needs to be passed to parent argument, else will not work
     */
    URLClassLoader loaderV1 = new URLClassLoader(new URL[]{v1, v1Dep}, null);
    URLClassLoader loaderV2 = new URLClassLoader(new URL[]{v2, v2Dep}, null);

    /**
     * Use custom class loader for loading classes first from children and last from parent
     */
    ParentLastClassLoader loaderV1Alt = new ParentLastClassLoader(new URL[]{v1, v1Dep});
    ParentLastClassLoader loaderV2Alt = new ParentLastClassLoader(new URL[]{v2, v2Dep});
    ParentLastClassLoader loaderV3Alt = new ParentLastClassLoader(new URL[]{});

    // get class from loader
    Class<?> coreV1Class = loaderV1.loadClass("sample.multiversion.ImportantCore");
    Class<?> coreV2Class = loaderV2.loadClass("sample.multiversion.ImportantCore");

    // get class from loader - custom version
    Class<?> coreV1AltClass = loaderV1Alt.loadClass("sample.multiversion.ImportantCore");
    Class<?> coreV2AltClass = loaderV2Alt.loadClass("sample.multiversion.ImportantCore");
    Class<?> coreV3AltClass = loaderV3Alt.loadClass("sample.multiversion.ImportantCore");

    // create class instance
    Object coreV1Instance = coreV1Class.newInstance();
    Object coreV2Instance = coreV2Class.newInstance();

    // create class instance - obtained from custom class loader
    Object coreV1AltInstance = coreV1AltClass.newInstance();
    Object coreV2AltInstance = coreV2AltClass.newInstance();

    // note that this is loaded from classpath
    Core coreV3Instance = new ImportantCore();
    Core coreV3AltInstance = (Core)coreV3AltClass.newInstance();

    // get version
    String v1Str = (String) coreV1Class.getMethod("getVersion").invoke(coreV1Instance);
    String v2Str = (String) coreV2Class.getMethod("getVersion").invoke(coreV2Instance);
    String v1AltStr = (String) coreV1AltClass.getMethod("getVersion").invoke(coreV1AltInstance);
    String v2AltStr = (String) coreV2AltClass.getMethod("getVersion").invoke(coreV2AltInstance);

    String v3Str = coreV3Instance.getVersion();
    String v3AltStr = coreV3AltInstance.getVersion();

    // get version of dependency
    String v1DepStr = (String) coreV1Class.getMethod("getDependencyVersion").invoke(coreV1Instance);
    String v2DepStr = (String) coreV2Class.getMethod("getDependencyVersion").invoke(coreV2Instance);
    String v1AltDepStr = (String) coreV1AltClass.getMethod("getDependencyVersion").invoke(coreV1AltInstance);
    String v2AltDepStr = (String) coreV2AltClass.getMethod("getDependencyVersion").invoke(coreV2AltInstance);

    String v3DepStr = coreV3Instance.getDependencyVersion();
    String v3AltDepStr = coreV3AltInstance.getDependencyVersion();

    System.out.println(String.format("V1 loader :: version = '%s' :: dependency_version = '%s'", v1Str, v1DepStr));
    System.out.println(String.format("V2 loader :: version = '%s' :: dependency_version = '%s'", v2Str, v2DepStr));
    System.out.println(String.format("V3 loader :: version = '%s' :: dependency_version = '%s'", v3Str, v3DepStr));

    System.out.println(String.format("V1 custom loader :: version = '%s' :: dependency_version = '%s'", v1AltStr, v1AltDepStr));
    System.out.println(String.format("V2 custom loader :: version = '%s' :: dependency_version = '%s'", v2AltStr, v2AltDepStr));
    System.out.println(String.format("V3 custom loader :: version = '%s' :: dependency_version = '%s'", v3AltStr, v3AltDepStr));

NOTE: Core interface could be part of another project which is used by projects v1, v2, v3, which could allow us to cast the newly created instance and work in a typesafe manner. But this might not be doable every time.

Community
  • 1
  • 1
andreim
  • 3,365
  • 23
  • 21
  • This sounds good indeed. But sometimes (as in my case) we need to have one lib loaded by Classpath classloader (i.e. part of the project sources) and the other one - as a jar. Is there a way to prevent the UrlClassLoader picking up the dependencies from the loaded lib and to look into the JAR first? – Paul Ilves Apr 25 '17 at 07:51
  • @PaulJ. done! I updated the POC to include a V3 which is loaded from classpath, leaving V1 and V2 to be loaded from file. – andreim Apr 25 '17 at 09:26
  • @PaulJ. re-edited the answer to include also a custom class loader that will load first classes from URL and if not found from the parent class loader. Does this solve your scenario? – andreim Apr 25 '17 at 10:18