简体   繁体   中英

Java ClassLoader: load same class twice

I have a ClassLoader which loads a class compiled by JavaCompiler from a source file. But when I change the source file, save it and recompile it, the ClassLoader still loads the first version of the class.

   ClassLoader cl = Thread.currentThread().getContextClassLoader();
   Class<?> compiledClass = cl.loadClass(stringClass);

What am I missing? like a newInstance or something?

A classloader can't replace a class that already has been loaded. loadClass will return the reference of the existing Class instance.

You'll have to instantiate a new classloader and use it to load the new class. And then, if you want to "replace" the class, you'll have to throw this classloader away and create another new one.


In response to your comment(s): do something like

ClassLoader cl = new UrlClassLoader(new URL[]{pathToClassAsUrl});
Class<?> compiledClass = cl.loadClass(stringClass);

This classloader will use the "default delegation parent ClassLoader" and you have to take care, the class (identified by it fully qualified classname) has not been loaded and can't be loaded by that parent classloader. So the "pathToClassAsUrl" shouldn't be on the classpath!

As it was stated before,

  1. Each class loader remembers (caches) the classes that is has loaded before and won't reload it again - essentially each class loader defines a namespace.
  2. Child class loader delegates class loading to the parent class loader, ie

Java 8 and before

Custom Class Loader(s) -> App Class Loader -> Extension Class Loader -> Bootstrap Class Loader

Java 9+

Custom Class Loader(s) -> App Class Loader -> Platform Class Loader -> Bootstrap Class Loader.

From the above we can conclude that each Class object is identified by its fully qualified class name and the loader than defined it (also known as defined loader)

From Javadocs :

Every Class object contains a reference to the ClassLoader that defined it.

The method defineClass converts an array of bytes into an instance of class Class. Instances of this newly defined class can be created using Class.newInstance.

The simple solution to reload class is to either define new (for example UrlClassLoader ) or your own custom class loader. For more complex scenario where you need to substitute class dynamic proxy mechanism can be utilized.

Please see below simple solution I used for a similar problem to reload same class by defining custom class loader. The essence - override findClass method of the parent class loader and then load the class from bytes read from the filesystem.

  1. MyClassLoader - overrides findClass and executed defineClass

 package com.example.classloader;
    
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    
    public class MyClassLoader extends ClassLoader {
    
        private String classFileLocation;
    
        public MyClassLoader(String classFileLocation) {
            this.classFileLocation = classFileLocation;
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            byte[] classBytes = loadClassBytesFromDisk(classFileLocation);
            return defineClass(name, classBytes, 0, classBytes.length);
        }
    
        private byte []  loadClassBytesFromDisk(String classFileLocation) {
            try {
                return Files.readAllBytes(Paths.get(classFileLocation));
            }
            catch (IOException e) {
                throw new RuntimeException("Unable to read file from disk");
            }
        }
    }
  1. SimpleClass - experiment subject - ** IMPORTANT : Compile with javac and then remove SimpleClass.java from class path (or just rename it) Otherwise it will be loaded by System Class Loader due to class loading delegation mechanism.** from src/main/java

javac com/example/classloader/SimpleClass.java


package com.example.classloader;

public class SimpleClassRenamed implements SimpleInterface {

    private static long count;

    public SimpleClassRenamed() {
        count++;
    }

    @Override
    public long getCount() {
        return count;
    }
}
  1. SimpleInterface - subject interface : separating interface from implementation to compile and execute output from the subject.

package com.example.classloader;

public interface SimpleInterface {

    long getCount();
}
  1. Driver - execute to test

package com.example.classloader;

import java.lang.reflect.InvocationTargetException;

public class MyClassLoaderTest {

    private static final String path = "src/main/java/com/example/classloader/SimpleClass.class";
    private static final String className = "com.example.classloader.SimpleClass";

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException,
            InvocationTargetException, InstantiationException {  // Exception hell due to reflection, sorry :)
        MyClassLoader classLoaderOne = new MyClassLoader(path);
        Class<?> classOne = classLoaderOne.loadClass(className);

        // we need to instantiate object using reflection,
        // otherwise if we use `new` the Class will be loaded by the System Class Loader
        SimpleInterface objectOne =
                (SimpleInterface) classOne.getDeclaredConstructor().newInstance();


        // trying to re-load the same class using same class loader
        classOne = classLoaderOne.loadClass(className); 
        SimpleInterface objectOneReloaded = (SimpleInterface) classOne.getDeclaredConstructor().newInstance();

        // new class loader
        MyClassLoader classLoaderTwo = new MyClassLoader(path);
        Class<?> classTwo = classLoaderTwo.loadClass(className);
        SimpleInterface ObjectTwo = (SimpleInterface) classTwo.getDeclaredConstructor().newInstance();

        System.out.println(objectOne.getCount()); // Outputs 2 - as it is the same instance
        System.out.println(objectOneReloaded.getCount()); // Outputs 2 - as it is the same instance

        System.out.println(ObjectTwo.getCount()); // Outputs 1 - as it is a distinct new instance
    }
}

You have to load a new ClassLoader each time, or you have to give the class a different name each time and access it via an interface.

eg

interface MyWorker {
  public void work();
}

class Worker1 implement MyWorker {
  public void work() { /* code */ }
}

class Worker2 implement MyWorker {
  public void work() { /* different code */ }
}

I think the problem might be more basic than what the other answers suggest. It is very possible that the class loader is loading a different file than what you think it is. To test out this theory, delete the .class file (DO NOT recompile your .java source) and run your code. You should get an exception.

If you do not get the exception, then obviously the class loader is loading a different .class file than the one you think it is. So search for the location of another .class file with the same name. Delete that .class file and try again. Keep trying until you find the .class file that is actually being loaded. Once you do that, you can recompile your code and manually put the class file in the correct directory.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM