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

有时候在编写 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 许可。转载时请注明来源,以及原文链接
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
呼呼
派蒙
巴巴托斯
上一篇
下一篇