0

I have to use a 3rd party platform, but the platform has an older version of the jar libjar-1.0.0.jar that cannot be replaced. The platform let me run my own (flat file) packages on top of it. I put the new version of libjar-2.0.0.jar under my package /packages/package-name/external-jar. When I used URLClassLoader to load libjar-2.0.0.jar and then printing out all declaredmethods, I was able to see the method that is in 2.0.0 jar. However, when I invoke, I always get NoSuchMethodException. When I print out newobj.class.getProtectionDomain().getCodeSource().getLocation().toString() , it always shows libjar-1.0.0.jar . Could anyone help explaining what I did wrong and what I need to do to force using the classes in a particular jar during runtime?

Here is a snapshot of my code

File f = new File(path);

URL[] urls = new URL[1];
urls[0] = f.toURI().toURL();
ClassLoader cl = new URLClassLoader(urls);

Class<?> utilsClass = cl.loadClass("com.myclass");
Constructor<?> cons = utilsClass.getConstructor(First.class, Second.class);
Object utils = cons.newInstance(firstObj, new Second());
if (utilsClass.getProtectionDomain() != null) {
           LOGGER.info(utilsClass.getProtectionDomain().getCodeSource().getLocation().toString());
}
// this print out --- 1.0.0.jar instead of 2.0.0.jar

for (Method m : utilsClass.getDeclaredMethods()) {
     LOGGER.info("methods: " + m.getName());
}
// method shows the "methodILookFor"

Method m = utilsClass.getDeclaredMethod("methodILookFor", Target.class, String[].class, Object.class);
// always throws NoSuchMethodException

m.invoke(utils, target, string, obj);
asun
  • 151
  • 5
  • 15

2 Answers2

1

How Class Loading Works

  • URLClassLoader is used for loading classes that are not already specified in the application classpath.
    • Class loading works on delegation principle. If a class is not loaded, the task of loading the class is delegated by the class loader to its parent class loader. If the class is not found by the parent class loader, it is passed to the child class loader for loading the class.
    • In your case, the URLClassLoader delegates the class loading to its parent, i.e., the Application Class Loader.
    • The Application Class Loader finds the class in libjar-1.0.0.jar. So, the URLClassLoader ends up not loading the class from libjar-2.0.0.jar.

Custom Class Loader

Here is a simple example of a custom class loader which extends URLClassLoader. This class loader tries to load classes from its URLs before it delegates to its parent class loader. It should be able to load different versions of JARs that you need in your example. You will find a complete example here with a unit test.

P.S. Class loading has changed in Java 9. It is not tested with Java 9 and may not work.

public class MyClassLoader extends URLClassLoader {

    public MyClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    @Override
    protected synchronized Class<?> loadClass(String name,
            boolean resolve) throws ClassNotFoundException {

        // 1. Check if the class has already been loaded
        Class<?> clazz = findLoadedClass(name);

        ClassLoader parentCL = getParent();

        // 2. If the class is not loaded and the class name starts
        // with 'java.' or 'javax.', delegate loading to parent
        if (clazz == null && parentCL != null && (name.startsWith(
                "java.") || name.startsWith(
                "javax."))) {
            clazz = parentCL.loadClass(name);

        }

        // 3. If the class is still null, try to load the class from the URL
        // (since we have already taken care of 'java.' and 'javax.'
        if (clazz == null) {
            try {
                clazz = super.findClass(name);
            } catch (ClassNotFoundException e) {
                //don't do anything
            }
        }

        // 4. If the class is still null, let the parent class loader load it.
        // Previously, we allowed 'java.' and 'javax.' classes to be loaded
        // from parent
        if (clazz == null && parentCL != null) {
            clazz = parentCL.loadClass(name);
        }

        // 5. If the class is still null, throw a class not found exception
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }

        if (resolve) {
            resolveClass(clazz);
        }

        return clazz;
    }
}
Indra Basak
  • 7,124
  • 1
  • 26
  • 45
  • Thanks for the explanation. Is there a way to force loading a particular jar over another? – asun Oct 03 '17 at 05:11
  • 1
    It's not trivial in most cases. OSGi framework had a nice way of solving the classpath hell issue. Java 9 modularity is a move in that direction. Is it possible for you to put your version 2 JAR before your 3rd party JAR in the application classpath. If that is not possible, unjar the 3rd party JAR and replace version 1.0 JAR with 2.0 JAR an then JAR up the 3rd party JAR. – Indra Basak Oct 03 '17 at 05:31
  • Thanks! It seems like the 3rd party platform is handling the classpath etc. I need to figure out a way to find where it's loading the jars. It's not an intuitive platform, but I have no choice. The moment I replaced the jar to the newer version jar, the platform crashed. Talked to the vendor and they have no plan to upgrade to the newer jar. However, our development needs the new method/feature in the newer jar. I am not sure unjar -> jar with the 2.0 jar would work in this case either. – asun Oct 03 '17 at 06:11
  • What 3rd party platform are you using? – Indra Basak Oct 03 '17 at 06:35
  • ncs is the platform. – asun Oct 03 '17 at 07:31
  • @asun Updated my posting with an example of a custom class loader. – Indra Basak Oct 03 '17 at 23:00
  • I found the platform is using mbean and it has it's own classloader to load all the jars. I print out all the urls under Thread.currentThread().getContextClassLoader(). I do see both libjar jars in the path. Would custom classloader works in this case since the platform already has its own classloader? another question is do i have to call the custom class loader each time I want to create an object in the newer version jar? – asun Oct 04 '17 at 03:41
  • 1
    **1.** If you use the custom class loader, you should keep the latest jar in some other location so it doesn't get picked by the application class loader. **2.** Specify the location of the `version 2` jar with the custom class loader. **3.** Every time you want to create a new instance of a class in the `version 2` jar, you have to use the custom class loader. **4.** There is a [way](https://stackoverflow.com/questions/5380275/replacement-system-classloader-for-classes-in-jars-containing-jars) of replacing the system class loader with the custom class loader but you have to be careful. – Indra Basak Oct 04 '17 at 03:58
1

Instead of using URL class loader you can try using custom class loader to load class using its fully qualified name. Using this approach you should be able to bypass the class loading delegation to the parent class loader which is causing problem in your case. So your class loader should be able to load the class from libjar-2.0.0.jar.

Vinit89
  • 583
  • 1
  • 7
  • 31