关于 JavaPlugin 子类 Instantiation 过程访问主类实例报错的随笔
- 2022-04-06 04:46:40
- William_Shi
- 4618
温馨提示: 这篇文章于942天前编写,现在可能不再适用或落后.
这个问题源自大意如下的代码:
public class Main extends JavaPlugin {
Plugin plugin = Main.getProvidingPlugin(Main.class);
}
在这样做时,会得到一个 IllegalStateException。它是由 JavaPlugin Line 428 抛出的。 https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/browse/src/main/java/org/bukkit/plugin/java/JavaPlugin.java#428
首先关注 JavaPlugin 的单例问题。众所周知,同一个插件只能有一个主类,一个主类只能有一个实例。如果任何人妄想直接 new JavaPlugin 或 new Main,那么他一定会失败。当然强行通过奇技淫巧去创建主类实例是有可能的,不过这样做一般并不是用来修复实际开发中的 bug 或解决实际问题的,只是作为茶余饭后的谈资罢了。(所谓魔法)
实现这个单例所用的手段很简单。首先,Bukkit 自己实现了一个 PluginClassLoader 来加载类。它承担了插件里所有的类的加载工作。(有一些第三方库没有使用它,而是通过主线程的 Context ClassLoader 访问资源或加载类,于是导致种种问题。一般而言,解决方法就是临时将 Context ClassLoader 换成 PluginClassLoader,此处不再赘述。)加载插件时,每一个插件都有自己的类加载器,用来加载这个插件的类。插件和类加载器是一一对应的关系,因此如果需要让主类只有一个实例,只需要在类加载器里保存一个变量,类似这样。
final class PluginClassLoader extends URLClassLoader {
final JavaPlugin plugin;
PluginClassLoader( … ) throws … {
Class<? extends JavaPlugin> pluginClass; // Main
// …
this.plugin = pluginClass.newInstance();
}
void initialize( JavaPlugin javaPlugin ) {
if (this.plugin != null … )
throw new IllegalArgumentException("Plugin already initialized!", this.pluginState);
// …
}
}
然后在 JavaPlugin 的默认构造器内加上这样的限制:
public JavaPlugin() {
// …
((PluginClassLoader)classLoader).initialize(this);
}
即类加载器内已经保存了一个 JavaPlugin 的实例,当尝试新建 JavaPlugin 的实例时,检查这个变量是不是空,如果不是则抛出异常。很显然,插件的主类必须提供无参构造器,否则 Bukkit 不能加载插件。而这个无参构造器,无论是否声明,声明后是否在第一行显式调用 super() ,编译器都一定会加上对父类无参构造器的调用,因此对同一个主类创建第二个实例的过程将一定会被打断。
在已知类加载器保存了 JavaPlugin 单例的情况下,获取主类实例的途径呼之欲出。JavaPlugin#getPlugin 和 JavaPlugin#getProvidingPlugin 两个方法的实现其实是类似的,都是返回 PluginClassLoader 中 plugin 这个实例变量的值。
public static JavaPlugin getProvidingPlugin(@NotNull Class<?> clazz) {
ClassLoader cl = clazz.getClassLoader();
// …
JavaPlugin plugin = ((PluginClassLoader)cl).plugin;
// …
return plugin;
}
在类加载器的控制下,一个 JavaPlugin 只有一个实例。所以在提供 Class 对象作为参数的情况下(指这两个获取主类实例的方法),最简单的方式就是直接获取参数中 Class 的 ClassLoader,对插件而言就是 PluginClassLoader,然后直接得到对应的主类实例。
类的实例化过程,运行顺序是先递归调用父类构造器,再进行实例变量的初始化,最后调用构造器的非 this() 或 super() 的部分。可以进行如下测试:
class A {
final B bObject;
protected A() {
bObject = new B(this);
}
}
class B {
protected B(A aObject) {
System.out.println(aObject.bObject);
}
}
假如通过调用 new A() 来运行,这段代码不会报错,输出是 null。原因是所谓的变量初始化是这样运行的:先声明 bObject 并且初始值为 null (因为它属于 Reference,默认值是 null ),然后开始执行 A 的构造器。构造器中先运行 new B(),在 B 类中打印变量的值。此时变量 bObject 的值还是 null,打印出的就是 null。构造器执行后再把 B 的实例赋值给 bObject 变量,变量才非空。
那么最上方尝试在实例变量初始化阶段访问主类实例报错的问题不难解决了。实质上,变量初始化是处于 PluginClassLoader Line 79 https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/browse/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java#79 正是在这个 newInstance() 的调用中,插件的实例产生了。
推及 JavaPlugin 的单例,在 PluginClassLoader 类内, plugin 这个实例变量初始值是 null,在 PluginClassLoader 的构造器中把 newInstance() 方法的返回值赋给 plugin 变量,它才非空。在 JavaPlugin 的实例变量初始化阶段,newInstance() 方法还没有执行完毕,没有返回值,plugin 变量是 null。此时调用 getProvidingPlugin 方法,相当于访问 plugin 变量的值,得到的就是 null。可以在主类内写如下代码进行测试:
boolean result = someMethod();
boolean someMethod() {
try {
var clazz = Class.forName("org.bukkit.plugin.java.PluginClassLoader");
var field = clazz.getDeclaredField("plugin");
field.setAccessible(true);
System.out.println(field.get(this.getClassLoader()));
} catch (Exception ex) {
ex.printStackTrace();
}
return true;
}
输出的结果显而易见是 null。所以 Bukkit 给 getProvidingPlugin 加上一层判断,以免获取到 null。
阁下需要登录后才可以查看评论哦~