Bukkit 通过 StackTrace 追踪调用者插件实例

Bukkit 通过 StackTrace 追踪调用者插件实例

有时候在编写 Bukkit 的插件的时候,需要追踪特定方法调用者,以做出不同的行为和响应。但由于 Bukkit 的插件生态系统不同的插件由不同的作者维护,请求其他开发者适配你的接口通常比较困难。 好在,有一种方式可以间接的获取调用者的插件名,进而可以通过 PluginManager 取得插件实例。

有时候在编写 Bukkit 的插件的时候,需要追踪特定方法调用者,以做出不同的行为和响应。但由于 Bukkit 的插件生态系统不同的插件由不同的作者维护,请求其他开发者适配你的接口通常比较困难。

好在,有一种方式可以间接的获取调用者的插件名,进而可以通过 PluginManager 取得插件实例。

获取谁调用的方法通常通过 StackTrace 进行。低版本的 Java 可以通过 new 一个 Exception 获得,而高版本中,Java 提供了一个 StackWalker 更高性能的方式获取。本文中使用 StackWalker 进行演示,演示环境为(JDK 17 + Spigot 1.18)。

分析 StackTrace

我在 com.ghostchu.quickshop.util.UtilgetPluginJarFile 函数中打印 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); //调用者插件主类实例

除特殊说明以外,本站原创内容采用 知识共享 署名-非商业性使用 4.0 (CC BY-NC 4.0) 许可。转载时请注明来源,以及原文链接。
Comment