注意:因为 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 设计得相当灵活,但其最基本的用途如下:
- 类加载器有父加载器。
- 要加载任何资源,CL 将首先要求其父级加载它。他们调用
defineClass,使其加载的类设置为您的父类是加载器,而不是您的自定义加载器。
-
只有如果父母无法完成,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();
}
}
祝项目的其余部分好运!