【问题标题】:Java ClassLoaders - Cast class to an interfaceJava ClassLoaders - 将类转换为接口
【发布时间】:2020-12-02 01:20:55
【问题描述】:

我正在尝试开发一个 java 应用程序,用户可以在其中添加功能(通过插件),这些功能必须实现一个通用接口:PluginFunction。 然后,自定义 .class 文件必须位于指定目录(在本例中为 ./plugins)并由我的自定义类加载器加载。

虽然从 IDE 测试一切正常,但是当我将应用程序导出到 jar 文件时,会引发以下异常:

java.lang.ClassCastException: class operaciones.Suma cannot be cast to class operaciones.PluginOperacion (operaciones.Suma is in unnamed module of loader logica.PluginClassLoader @daf4db4; operaciones.PluginOperacion is in unnamed module of loader java.net.URLClassLoader @6576fe71)
        at logica.OperacionesManager.loadOperaciones(OperacionesManager.java:75)
        at gui.commands.RefreshCommand.execute(RefreshCommand.java:28)
        at gui.GUI_CalcSimple.<init>(GUI_CalcSimple.java:124)
        at gui.GUI_CalcSimple$1.run(GUI_CalcSimple.java:60)

我一直在做一些研究,发现问题是因为接口是由与我的自定义类加载器不同的类加载器加载的,但我不知道如何解决这个问题。

谢谢

【问题讨论】:

  • 能不能把接口的代码加进去,用的地方也可以

标签: java exception plugins interface classloader


【解决方案1】:

注意:因为 java 代表所有类型(类、接口、枚举等)java.lang.Class 我将在此答案的其余部分使用“类”一词,但这也包括接口和枚举等。

通常,我们的 Java 开发人员说一个类型(类、接口、枚举等)只会被系统加载一次(例如,只有一个 java.lang.Class 的实例)。

这是真的。

然而,“一个类”,这个定义需要一些捏造:一个类不是只是由它的完全限定名称(operaciones.Suma)定义的。它实际上是由它的名称和它的加载器组合定义的。

现在,在正常情况下,给定 VM 中只有一个相关的加载器:它加载了具有您的 main 方法的类,并将加载执行该方法最终需要的所有内容,它查看类路径以做好自己的工作。

重要:实例的兼容性

如果您通过使用 2 个分别加载该类的类加载器以某种方式最终得到两个名为 java.lang.Integer 的单独加载的类,那么这些类型是完全不兼容的。您会遇到疯狂的错误,例如“java.lang.Integer 类型的实例不能分配给 java.lang.Integer 类型的变量”。

重要提示:边界类型的概念

在您的模块系统中,有 3 个世界。在您的主应用程序中,您可以在内部执行这些操作。该插件甚至不知道这一切。它是标记为私有等的东西。

在插件系统中,也有私有组件。

但是,还有第三个世界:介于两者之间的是什么。据推测,您最终会在主应用程序代码中生成一个字符串,然后将其交给插件。这意味着java.lang.String 是一种边界类型。该插件将operaciones.PluginOperacion 作为它需要的东西(毕竟它实现了它!),但你的主代码也是如此。那也是一种边界类型。

关键点:所有边界类型都必须由一个类加载器(插件代码和主应用程序)加载,否则您不能将它们用作边界类型。

因此,您所观察到的解释很简单:插件最终在其自己的加载器中加载了边界类型,而不是在您的主类的加载器中,这就是您需要解决的问题。

重要:“什么是加载器?”

“加载这个类的加载器”是什么意思?很简单:ClassLoaders 最终调用自己的原生 defineClass 方法,传递字节码的字节数组。 THAT 使您在“加载器”上调用defineClass 方法的实例。每当该类最终需要另一个类来完成其工作时,它会立即要求其类加载器这样做。即使对于像java.lang.String 这样简单的事情也是如此。

重要提示:类加载器有父级

ClassLoader API 设计得相当灵活,但其最基本的用途如下:

  1. 类加载器有父加载器。
  2. 要加载任何资源,CL 将首先要求其父级加载它。他们调用defineClass,使其加载的类设置为您的父类是加载器,而不是您的自定义加载器。
  3. 只有如果父母无法完成,CL 会自己完成。你最终打电话给defineClass,你现在是装载机。您加载的内容所需的任何类型都来自 #1,并且将再次导致“先询问父母,只有当它不能时,我们才加载它”。

您不必这样做,您可以选择不询问您的父母并始终自行加载。有时这样做是有理由的。

正确的设计

因此,诀窍是使用父系系统来确保边界类型只加载一次。可能是由您为主应用程序设置的一个类加载器,但这可能是过度设计它:您的主应用程序和 ALL 边界类应该由 java 标准加载器加载(用您的主应用程序加载类方法),并且插件本身和它定义的任何类型都由插件的加载器加载。

由于父级关系,这可以解决:插件的加载器有一个父级(主加载器)。您要求自定义加载程序加载插件。它首先询问其父级(主加载器),但由于此插件不在您的类路径中,所以找不到它。因此,pluginloader 加载它。在 pluginloader 完成此操作的那一刻,pluginloader 立即被要求加载operaciones.PluginOperacion,因为您正在加载的插件类扩展/实现了它。

按照 API 的标准意图,您的 pluginloader 将要求其父级加载它,并且...应该会成功,因此您的 pluginloader 返回的 j.l.Class 实例仍然由主加载器。如,调用 getClassLoader() 将返回相同的东西 YourMainApp.class.getClassLoader() 返回。

太棒了!怎么样?

class PluginLoader extends ClassLoader {
    public PluginLoader(ClassLoader parent) {
        super(parent);
    }

    public Class<?> findClass(String name) {
        String tgt = name.replace(".", "/") + ".class";
        byte[] bytecode = readFullyFromPluginjar(tgt);
        return defineClass(name, bytecode, 0, bytecode.length);
    }
}

这就是你所要做的。很简单,只要您了解它的工作原理。

但是,请注意,java 本身将调用 loadClass 而不是 findClass。幸运的是,您继承的 loadClass 的 impl 将首先询问 parent,并且只有当 parent 找不到时,它才会调用 findClass(最终将运行上面的覆盖代码)。因此,如果你想编写一个不符合首先询问父母的标准意图的类加载器,你可以重写 loadClass 。但是,标准意图通常是您想要的,因此通常覆盖 findClass,而不是 loadClass。

使用方法:

class Main {
    public PluginOperaciones loadPlugin(Path jarLocation, String className) {
        PluginLoader loader = new PluginLoader(Main.class.getClassLoader());
        loader.setJarSearchSpace(jarLocation);
        Class<?> pl = loader.loadClass(className); // load, not find!!
        return (PluginOperaciones) pl.getConstructor().newInstance();
    }
}

祝项目的其余部分好运!

【讨论】:

  • 或者只是使用 URLClassLoader 而不是重新发明它......
猜你喜欢
  • 2011-12-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-31
  • 2020-12-06
相关资源
最近更新 更多