java 使用代理 API 声明匿名类

java declare anonymous class using proxy api

提问人:Th0rgal 提问时间:8/31/2018 最后编辑:Th0rgal 更新时间:8/31/2018 访问量:222

问:

今天早上,我陷入了一个以前从未发生在我身上的特殊案例。我正在使用 minecraft 服务器 API 开发一个 Minecraft 插件,该 API 通常称为 NMS,参考其软件包的名称(例如 1.13 版的 net.minecraft.server.v1_13_R1)。

使用minecraft服务器API的主要问题是很难编写跨版本代码:事实上,每个新版本的软件包名称都会发生变化。

当插件只支持两个版本时,根据版本的不同,使用接口编写两个不同的代码通常更容易。但是当你必须支持十几个不同的版本时(这是我的情况),这是一个坏主意(插件太重了,它必须导入 IDE 中的每个 jar,并且我必须重做每个新版本的代码)。 在这些情况下,我通常使用反射,但我认为这里是不可能的:

            packet = packetConstructor.newInstance(
                    new MinecraftKey("q", "q") {
                        @Override
                        public String toString() {
                            return "FML|HS";
                        }
                    },
                    packetDataSerializerConstructor.newInstance(Unpooled.wrappedBuffer(data)));

正如您可能猜到的那样,MinecraftKey 是 NMS 中的一个类,我被告知要使用 Java 动态代理 API。我从未使用过它,想知道您是否知道一个可以向我解释如何简单地做到这一点的地方?如果您知道另一种我也感兴趣的更好的方法!

仔细想想,我觉得这对于一小段代码x来说真的是麻烦多了)

编辑: 我的插件使用 PacketPlayOutCustomPayload(又名插件消息)与玩家的模组进行通信。它允许我在特定通道(字符串)上发送消息(字节 [])。但是在 1.13 中,这个字符串已被 MinecraftKey(String 的包装器,用于替换某些字符并需要使用 “:”) 替换。当玩家在我的 1.13 服务器上连接到 1.12 时,这会带来一个问题,所以我别无选择:在这种情况下,我必须覆盖 MinecraftKey 对象。

java 反射 代理 Minecraft

评论

0赞 LeoColman 8/31/2018
如果你正在做插件,使用 Bukkit 或 Spigot API 不是更容易吗?
0赞 Th0rgal 8/31/2018
这是真的,但不幸的是,它不允许我做我想做的事(正如你所看到的,我覆盖了 toString 方法 MinecraftKey,否则我无法发送正确的字符串)。
0赞 LeoColman 8/31/2018
龙头论坛中的这个帖子对你有什么帮助吗?
0赞 LeoColman 8/31/2018
如果可能的话,我会逃离 NMS
1赞 Th0rgal 8/31/2018
不,它对我没有真正的帮助。但我欠你一个解释:我的插件使用 PacketPlayOutCustomPayload(又名插件消息)与玩家的模组进行通信。它允许我在特定通道(字符串)上发送消息(字节 [])。但是在 1.13 中,这个字符串已被 MinecraftKey(String 的包装器,用于替换某些字符并需要使用 “:”) 替换。当玩家在我的 1.13 服务器上连接到 1.12 时,这会带来一个问题,所以我别无选择:在这种情况下,我必须覆盖 MinecraftKey 对象。

答:

0赞 GotoFinal 8/31/2018 #1

我真的不认为在这里使用代理类是很好的解决方案,它只会使调试变得更加困难,但如果你需要这样的东西,你应该使用像 ByteBuddy 这样的库: (由于 java 无法为类生成代理,因此只允许接口)

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.FixedValue;
import static net.bytebuddy.matcher.ElementMatchers.*;

public class Main {
    public static void main(String[] args) throws Exception {
        SomeKey someKey = new SomeKey("my", "key");
        System.out.println(someKey); // :<
        // this class should be cached/saved somewhere, do not create new one each time.
        Class<? extends SomeKey> loaded = new ByteBuddy()
                .subclass(SomeKey.class)
                .method(named("toString").and(returns(String.class).and(takesArguments(0))))
                .intercept(FixedValue.value("something"))
                .make()
                .load(Main.class.getClassLoader()).getLoaded();
        someKey = loaded.getConstructor(String.class, String.class).newInstance("what", "ever");
        System.out.println(someKey); // YeY
    }
}
class SomeKey {
    final String group;
    final String name;
    public SomeKey(String group, String name) {
        this.group = group;
        this.name = name;
    }
    public String getGroup() { return this.group; }
    public String getName() { return this.name; }
    @Override public String toString() {
        return group+":"+name;
    }
}

但我只会在我的项目中创建单独的模块,一个仅适用于真正的 bukkit API 的模块,并且包含许多接口以某种规范化和可读的方式表示 NMS 类型。
并且每个版本都有单独的模块,那么您将没有太多代码可以复制,因为其中大部分代码将由该“核心/基础”模块抽象和处理。
然后,您可以将其构建为一个胖罐或每个版本单独的.jar。

其他解决方案可能是使用一些模板引擎和预处理器在构建时生成 java 源代码,请参阅 fastutil 是如何做到这一点的: https://github.com/vigna/fastutil

对于简单的类和部分代码,另一种解决方案是使用内置的 javascript 或外部脚本语言(如 groovy)来创建此模式线,但在运行时。但我只会将其用于最简单的东西。

此外,对于仅使用方法,您可以只使用普通反射。

您也可以随时注入 netty,而不是使用默认的数据包序列化程序,只需写入自己的字节,那么您根本不需要该密钥。

评论

0赞 Th0rgal 8/31/2018
非常感谢它的工作!ByteBuddy 有点重,所以我要看看我是否可以直接使用 asm。
0赞 GotoFinal 8/31/2018
不要太担心 .jar 的大小...没有理由那么关心它。但也有“Javassist”库,它允许你只编写类似 java 的代码,或者你甚至可以使用 javascript 引擎内置到 jre 中扩展该类。