【问题标题】:Java ClassLoader: load same class twiceJava ClassLoader:两次加载相同的类
【发布时间】:2012-03-21 12:27:47
【问题描述】:

我有一个ClassLoader,它从源文件加载由JavaCompiler 编译的类。 但是当我更改源文件,保存并重新编译时,ClassLoader 仍然会加载该类的第一个版本。

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

我错过了什么?比如 newInstance 什么的?

【问题讨论】:

    标签: java class compiler-construction


    【解决方案1】:

    类加载器不能替换已经加载的类。 loadClass 将返回现有 Class 实例的引用。

    您必须实例化一个新的类加载器并使用它来加载新类。然后,如果你想“替换”这个类,你就必须扔掉这个类加载器并创建另一个新的。


    回应您的评论:做类似的事情

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

    该类加载器将使用“默认委托父类加载器”,您必须注意,该类(由其完全限定的类名标识)尚未加载并且无法由该父类加载器加载。所以“pathToClassAsUrl”不应该在类路径上!

    【讨论】:

    • 如何替换或删除我的方法中的 ClassLoader?
    • 谢谢,我在这里找到了对您评论的很好解释:exampledepot.com/egs/java.lang/reloadclass.html
    • @Andreas_D 如果我创建两个新的类加载器并尝试为每个类加载相同的类(仍然没有加载)?
    • 然后你会在你的 JVM 中获得同一个类的两个“实例”。这就是应用服务器在 Java 中工作的原因。例如,许多应用程序可能会加载相同的日志库,而取消部署一个应用程序不会对其他应用程序或框架产生任何影响,因为它们都使用不同的类加载器实例。
    【解决方案2】:

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

    例如

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

    【讨论】:

    • 如何在同一个方法中每次都加载一个新的ClassLoader?
    • 你可以创建一个new ClassLoader(),你可以通过调用defineClass来触发它加载类。
    • new Classloader() 不能这样启动。
    • 不错,需要创建一个子类,可以是匿名子类。
    【解决方案3】:

    如前所述,

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

    Java 8 及之前的版本

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

    Java 9+

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

    从上面我们可以得出结论,每个 Class 对象都是由其完全限定的类名和定义它的加载器(也称为定义的加载器)标识的

    来自Javadocs

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

    defineClass 方法将字节数组转换为 类 类。可以创建这个新定义的类的实例 使用 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();
    }
    
    1. 驱动程序 - 执行测试

    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
        }
    }
    

    【讨论】:

      【解决方案4】:

      我认为这个问题可能比其他答案所暗示的更为基本。类加载器很可能加载的文件与您认为的不同。要测试这个理论,请删除 .class 文件(不要重新编译您的 .java 源代码)并运行您的代码。你应该得到一个例外。

      如果您没有收到异常,那么显然类加载器正在加载一个与您认为的不同的 .class 文件。所以搜索另一个同名的 .class 文件的位置。删除该 .class 文件,然后重试。继续尝试,直到找到实际正在加载的 .class 文件。完成此操作后,您可以重新编译代码并手动将类文件放在正确的目录中。

      【讨论】:

      • 但是上面的答案表明,一旦加载了一个类,类加载器就不会从文件中再次加载它。这似乎是适当的行为。我也想通了。
      猜你喜欢
      • 1970-01-01
      • 2014-08-23
      • 1970-01-01
      • 1970-01-01
      • 2018-07-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-11-24
      相关资源
      最近更新 更多