我正在尝试开发一个java应用程序,用户可以在其中添加功能(通过插件),该功能必须实现一个公共接口: PluginFunction。然后,定制插件文件必须位于指定的目录中(在本例中为./ .class ),并由我的定制类加载器加载。
虽然从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)我已经做了一些研究,我发现这个问题是因为接口是由不同于我的自定义类加载器的类加载器加载的,但我不知道如何解决这个问题。
谢谢
发布于 2020-12-02 09:43:24
注意:因为java代表所有类型类型(类、接口、枚举等)类,所以我将使用术语“java.lang.Class”来表示这个答案的其余部分,但这也包括接口和枚举等。
通常,我们java开发人员会说一个类型(类、接口、枚举等)只会被系统加载一次(就像在中一样,java.lang.Class只有一个实例)。
这是真的。
然而,对于“类”,这个定义需要一些模糊处理:一个类不仅仅是通过它的完全限定名(operaciones.Suma)来定义的。它实际上是由它的名称和加载器的组合来定义的。
现在,在正常情况下,在给定的VM中只有一个相关的加载器:它加载了具有您的main方法的类,并将加载执行此方法最终需要的所有内容,并查看类路径来完成其工作。
重要提示:实例兼容性
如果通过使用两个类加载器,每个类都单独加载了两个单独的加载类,那么这些类型是完全不兼容的。你会得到疯狂的错误,比如“不能将java.lang.Integer类型的实例赋值给java.lang.Integer类型的变量”。
重要:边界类型的概念
在你的模块系统中,有3个世界。这是你在内部,在你的主应用中做的事情。这个插件甚至不会知道这一切。它是标记为私人的东西之类的。
在插件系统中,这也有私有组件。
但是,还有第三世界:介于两者之间的是什么。大概,你最终会在你的主应用程序代码中生成一个字符串,然后把它交给插件。这意味着java.lang.String是一种边界类型。插件需要operaciones.PluginOperacion (毕竟是它实现了它!),但是你的主代码也是如此。这也是一种边界类型。
关键点:所有边界类型必须由一个类加载器加载(包括插件代码和主应用程序),否则您不能将它们用作边界类型。
因此,你观察到的东西的解释很简单:插件最终在它自己的加载器中加载了一个边界类型,而不是在你的主类的加载器中,这就是你需要解决的问题。
重要:“什么是装载器?”
“加载这个类的加载器”是什么意思?这很简单:在最后,ClassLoaders通过传递字节码的字节数组来调用本机defineClass方法。使你在“”上调用defineClass方法的实例。每当那个类最终需要另一个类来完成它的工作时,它就会立即要求它的类加载器这样做。即使对于像java.lang.String这样简单的东西,也是如此。
重要提示: ClassLoaders有亲子关系
ClassLoader应用编程接口被设计为相当灵活,但其最基本的预期用途如下:
defineClass,使它加载的类设置为您的父类是加载器,而不是您的自定义loader.defineClass,你现在就是加载器了。您加载的内容所需的任何类型都将从#1开始,并将再次导致“先询问父级,只有当它无法加载时,我们才会加载它”。你不需要这样做,你可以选择不问你的父母,而总是自己加载它。有时候这样做是有原因的。
正确的设计
所以,诀窍是,使用亲子关系系统来确保边界类型只加载一次。可能是您为主应用程序设置的一个类加载器,但这可能是过度工程:您的主应用程序和所有边界类都应该由java标准加载器(用main方法加载类)加载,插件本身和它定义的任何类型都由插件的加载器加载。
由于父子关系,这是可行的:插件的加载器有一个父加载器(主加载器)。你请求你的自定义加载器来加载插件。它首先请求它的父级(主加载器),但无法找到它,因为这个插件不在您的类路径中。因此,pluginloader加载它。在插件加载器完成此操作的那一刻,插件加载器立即被要求加载operaciones.PluginOperacion,因为您正在加载的插件类扩展/实现了这一点。
遵循API的标准意图,您的pluginloader将请求其父级加载它,然后...应该在之后的,因此插件加载器返回的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的impl将首先询问parent,只有当parent找不到时,它才会调用findClass (这将最终运行上面被覆盖的代码)。因此,如果您想编写一个不符合先询问parent的标准意图的类加载器,您可以重写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();
}
}祝你项目的其余部分好运!
https://stackoverflow.com/questions/65100898
复制相似问题