Java类加载器:重复加载同一个类

3

我有一个ClassLoader,它可以从源文件中通过JavaCompiler编译加载一个类。 但是当我修改源文件并重新编译后,ClassLoader仍然会加载第一个版本的类。

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

我缺少什么?比如newInstance还是其他什么?
4个回答

11
一个类加载器无法替换已经被加载的类。loadClass将返回现有Class实例的引用。
您需要实例化一个新的类加载器并使用它来加载新的类。然后,如果您想要“替换”该类,您将不得不丢弃这个类加载器并创建另一个新的类加载器。
对于您的评论,可以尝试执行以下操作:
ClassLoader cl = new UrlClassLoader(new URL[]{pathToClassAsUrl});
Class<?> compiledClass = cl.loadClass(stringClass);

这个类加载器将使用"默认委托父类加载器",你需要注意的是,该类(通过完全限定的类名识别)尚未被加载,也不能通过该父类加载器加载。因此,“pathToClassAsUrl”不应该在类路径上!


我该如何在我的方法中替换或移除ClassLoader? - Arian
谢谢,我在这里找到了你评论的好解释:http://www.exampledepot.com/egs/java.lang/reloadclass.html - Arian
@Andreas_D 如果我创建两个新的类加载器,并尝试使用它们中的每一个加载相同的类(该类仍未加载),会发生什么? - bsiamionau
然后你在JVM中得到了两个相同类的“实例”。这就是为什么应用服务器在Java中工作的原因。许多应用程序可能会加载相同的日志记录库,例如,卸载一个应用程序对其他应用程序或框架没有任何影响,因为它们都使用不同的类加载器实例。 - Andreas Dolk

1

每次都必须加载一个新的ClassLoader,或者每次都必须给类取一个不同的名称,并通过接口访问它。

例如:

interface MyWorker {
  public void work();
}

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

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

1
如何在同一方法中每次加载一个新的ClassLoader? - Arian
你可以创建一个 new ClassLoader(),并通过调用 defineClass 来触发它加载类。 - Peter Lawrey
新建 Classloader() 不能像这样被初始化。 - Arian
1
是的,你需要创建一个子类,它可以是匿名子类。 - Peter Lawrey

1

正如之前所述,

  1. 每个类加载器都记住(缓存)它以前加载过的类,并且不会再次重新加载它 - 实际上,每个类加载器定义了一个命名空间。
  2. 子类加载器将类加载委托给父类加载器,即

Java 8及之前版本

自定义类加载器 -> 应用程序类加载器 -> 扩展类加载器 -> 引导类加载器

Java 9+

自定义类加载器 -> 应用程序类加载器 -> 平台类加载器 -> 引导类加载器。

由此可见,每个Class对象都通过其完全限定的类名和定义它的加载器(也称为已定义的加载器)进行标识。

Javadocs中可以得出:

每个Class对象都包含对定义它的ClassLoader的引用。

defineClass方法将字节数组转换为Class类的实例。可以使用Class.newInstance创建这个新定义的类的实例。

简单的解决方案是定义一个新的类加载器(例如UrlClassLoader),或者自己创建一个自定义的类加载器来重新加载类。 对于更复杂的情况,需要替换类时,可以利用动态代理机制。
请参见下面的简单解决方案,我用它来重新加载同一类,方法是定义自定义类加载器并覆盖父类加载器的findClass方法,然后从文件系统中读取字节加载类。
  1. MyClassLoader - 重写findClass并执行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 - 实验主题 - ** 重要提示:使用 javac 编译后,将 SimpleClass.java 从类路径中移除(或重命名) 否则由于类加载委托机制,它将被系统类加载器加载。** 来自 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 - 主题接口:将接口与实现分开,以编译和执行主题的输出。

package com.example.classloader;

public interface SimpleInterface {

    long getCount();
}

司机 - 执行以进行测试
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
    }
}

0

我认为问题可能比其他答案提供的更基础。很有可能类加载器正在加载一个与您想象不同的文件。为了测试这个理论,请删除.class文件(不要重新编译.java源代码),然后运行您的代码。您应该会收到一个异常。

如果您没有收到异常,那么显然类加载器正在加载与您想象的不同的.class文件。因此,请搜索具有相同名称的另一个.class文件的位置。删除该.class文件,然后再尝试一次。不断尝试,直到找到实际被加载的.class文件。一旦找到,您就可以重新编译代码并手动将类文件放在正确的目录中。


但是上面的答案表明,一旦类被加载,类加载器就不会再从文件中加载它。这似乎是适当的行为。我也发现了这一点。 - Arian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接