java类加载器-将类强制转换为接口

ahy6op9u  于 2021-07-07  发布在  Java
关注(0)|答案(1)|浏览(475)

我正在尝试开发一个java应用程序,用户可以在其中添加功能(通过插件),这些功能必须实现一个公共接口:pluginfunction。然后,custom.class文件必须位于指定的目录中(在本例中是./plugins),并由我的custom classloader加载。
从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)

我已经做了一些研究,我发现问题的出现是因为接口是由不同于我的自定义类加载器的类加载器加载的,但是我不知道如何修复这个问题。
谢谢

p3rjfoxz

p3rjfoxz1#

注意:因为java代表所有类型(类、接口、枚举等) java.lang.Class 我将在这个答案的其余部分使用“类”这个术语,但它也包括接口和枚举等。
通常,我们java开发人员会说一个类型(类、接口、枚举等)只被系统加载一次(例如,只有一个 java.lang.Class ).
这是真的。
然而,“类”的定义需要一些捏造:类不仅仅是通过它的完全限定名来定义的( operaciones.Suma ). 它实际上是由其名称和加载程序的组合来定义的。
现在,在正常情况下,给定的vm中只存在一个相关的加载程序:它加载了拥有main方法的类,并将加载执行此方法最终需要的所有内容,它查看类路径来执行其任务。

要点:示例的兼容性

如果你最终得到了两个独立的类,它们都被命名为, java.lang.Integer ,通过使用两个类装入器分别装入该类,则这些类型是完全不兼容的。你会得到一些疯狂的错误,比如“java.lang.integer类型的示例不能分配给java.lang.integer类型的变量”。

重要:边界类型的概念

在你的模块系统中,有三个世界。在你的主应用程序中,你可以在内部做一些事情。插件甚至不需要知道这些。是标记为私人之类的东西。
在插件系统中,也有私有组件。
但是,还有一个第三世界:介于两者之间的是什么。大概,你最终会在你的主应用程序代码中生成一个字符串,然后把它交给插件。这意味着 java.lang.String 是边界类型。这个插件有 operaciones.PluginOperacion 作为它需要的东西(毕竟它实现了它!),但是你的主代码也是。这也是一种边界类型。
关键点:所有边界类型都必须由一个类加载器加载(对于插件代码和主应用程序都是如此),否则就不能将它们用作边界类型。
因此,对您观察到的情况的解释很简单:插件最终在自己的加载程序中加载了边界类型,而不是在主类的加载程序中,这就是您需要修复的。

重要提示:“装载机是什么?”

“加载这个类的加载程序”是什么意思?很简单:类装入器最终调用本机 defineClass 方法,传递字节码的字节数组。这使得你调用 defineClass 方法。每当该类最终需要另一个类来完成它的工作时,它将立即要求它的类装入器这样做。即使是像这样简单的事情也是如此 java.lang.String .

重要提示:类加载器有亲子关系

classloader api的设计相当灵活,但其最基本的预期用途如下:
类加载器有父加载器。
要加载任何资源,cl将首先要求其父级加载它。他们打电话来 defineClass ,使其加载的类设置为您的父级是加载程序,而不是您的自定义加载程序。
只有当父母做不到的时候,cl才会自己做。你最后打电话来 defineClass ,你现在是加载器了。您加载的内容所需的任何类型都从#1开始,并将再次导致“先询问父级,如果不能,则我们加载它”。
你不必这样做,你可以选择不去问你的父母,并且总是自己加载它。有时这样做是有原因的。

正确的设计

所以,诀窍是,使用parentage系统来确保只加载一次边界类型。可能是由您为主应用程序设置的一个类加载器加载的,但这可能过于工程化了:您的主应用程序和所有边界类都应该由java标准加载器加载(该加载器使用您的主方法加载该类),插件本身及其定义的任何类型都应该由插件的加载器加载。
由于父级的关系,这就解决了:插件的加载程序有一个父级(主加载程序)。您要求您的自定义加载程序加载插件。它首先询问它的父级(主加载程序),但是找不到它,因为这个插件不在你的类路径中。因此,pluginload加载它。就在pluginloader完成这项工作的那一刻,pluginloader立即被要求加载 operaciones.PluginOperacion 因为您正在加载的插件类扩展/实现了该类。
遵循api的标准意图,您的插件加载程序将要求其父级加载它,并且。。。这应该会成功,因此 j.l.Class 你的插件加载程序返回的示例仍然是由mainloader加载的。例如,打电话 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 您继承的将首先询问父级,只有父级找不到时,它才会调用 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();
    }
}

祝项目的其他部分好运!

相关问题