有时候在编写 Bukkit 的插件的时候,需要追踪特定方法调用者,以做出不同的行为和响应。但由于 Bukkit 的插件生态系统不同的插件由不同的作者维护,请求其他开发者适配你的接口通常比较困难。
好在,有一种方式可以间接的获取调用者的插件名,进而可以通过 PluginManager 取得插件实例。
获取谁调用的方法通常通过 StackTrace 进行。低版本的 Java 可以通过 new 一个 Exception 获得,而高版本中,Java 提供了一个 StackWalker 更高性能的方式获取。本文中使用 StackWalker 进行演示,演示环境为(JDK 17 + Spigot 1.18)。
分析 StackTrace
我在 com.ghostchu.quickshop.util.Util
的 getPluginJarFile
函数中打印 StackTrace 如下图所示:
at com.ghostchu.quickshop.util.Util.getPluginJarFile(Util.java:1316)
at com.ghostchu.quickshop.localization.text.SimpleTextManager.loadBundled(SimpleTextManager.java:202)
at com.ghostchu.quickshop.localization.text.SimpleTextManager.load(SimpleTextManager.java:253)
at com.ghostchu.quickshop.QuickShop.onLoad(QuickShop.java:497)
at org.bukkit.craftbukkit.v1_18_R2.CraftServer.loadPlugins(CraftServer.java:423)
at net.minecraft.server.dedicated.DedicatedServer.e(DedicatedServer.java:323)
at net.minecraft.server.MinecraftServer.w(MinecraftServer.java:1179)
at net.minecraft.server.MinecraftServer.lambda$spin$1(MinecraftServer.java:320)
at java.base/java.lang.Thread.run(Thread.java:833)
上图是一个典型的 StackTrace,通常在代码爆炸的时候可以见到。
第一行 at com.ghostchu.quickshop.util.Util.getPluginJarFile(Util.java:1316)
是我的打印代码位置。
第二行是调用第一行所示函数的代码位置。
第三行是调用第二行所示函数的代码位置....以此类推。
从 StackTrace 中可以看出调用的代码的 包名、类名、函数名、文件名和行数。
谁在调用我
通常 StackTrace 的第二行就是调用目标方法的位置。
但是显而易见,位置是不固定的,而且每个方法都要加一串非常复杂的代码会降低可读性。这种情况我们封装一个 trace()
函数,在需要时调用 trace()
函数来解决这个问题。
public String trace() {
StackWalker stackWalker = StackWalker.getInstance(Set.of(StackWalker.Option.RETAIN_CLASS_REFERENCE), 3); // 多了一层调用,所以2变成了3
List<StackWalker.StackFrame> caller = stackWalker.walk(
frames -> frames
.limit(3) // 多了一层调用,所以2变成了3
.toList());
StackWalker.StackFrame frame = caller.get(2); // 多了一层后,读取第三个元素(下标从0开始,所以是2)
String threadName = Thread.currentThread().getName(); // 线程名称
String className = frame.getClassName(); // 类名称(包含包名)
String methodName = frame.getMethodName(); // 函数名
int codeLine = frame.getLineNumber(); // 代码行位置
return className; // 我们只需要 className
}
于是我们在 getPluginJarFile
中调用 trace()
方法时,就可以得到调用 getPluginJarFile
方法的调用者的 thread 名称、class 名称、method 名称和代码行了。
com.test.ClassA 代码如下
public static void test() {
String whoCallMe = trace();
System.out.println("Caller = " + whoCallMe);
}
com.test.ClassB 代码如下
public void apple() {
ClassA.test();
}
当 ClassB 的 apple 函数被执行时控制台输出:
Caller = com.test.ClassB.apple
反射获得 class
这里拿到的 className 是 com.package.name.Classname
这样的格式,因此我们可以直接反射获得目标 Class:
Class<?> callerClass = Class.forName(className);
取得插件 Jar 位置
拿到 class 后,我们可以通过 class 定位到 Jar 包的具体位置:
String jarPath = callerClass.getProtectionDomain().getCodeSource().getLocation().getFile();
jarPath = URLDecoder.decode(jarPath, StandardCharsets.UTF_8); // 路径被 URL 编码过,想要使用必须先 URL 解码
File jar = new File(jarPath);
读取 Bukkit 插件的 plugin.yml
Bukkit 插件在被 CraftBukkit 载入时,会先读取 Jar 内的 plugin.yml
,其中存储了插件名和插件主类位置。
JarFile callerJar = new JarFile(jar);
InputStream is = callerJar.getInputStream(jarFile.getEntry("plugin.yml"));
String result = IOUtils.toString(is, StandardCharsets.UTF_8); // Apache Utils 可以自己换其他方式读
YamlConfiguration pluginYaml = new YamlConfiguration();
pluginYaml.loadFromString(result);
String pluginName = pluginYaml.getString("name"); // 插件名
String mainClass = pluginYaml.getString("main"); // 插件主类
获取调用者插件主类
拿到了 pluginName 之后,就可以通过 Bukkit 的 PluginManager 来获取插件调用者的插件主类了。
Plugin plugin = Bukkit.getPluginManager().getPlugin(pluginName); //调用者插件主类实例