小钢炮君
通过NMS序列化ItemStack 浅谈序列化复杂实体类

个人拙见,有错误请各位大佬指正

前言

对于序列化ItemStack的问题 ,板块内已经有优秀的教程了:此处+此处

而在此处我就简单分享一下了解的开发经验,让萌新少走弯路555~

也娇娇你遇到问题的时候以后怎么去寻根溯源~

本文将从序列化ItemStack的问题开始,讲讲如何直接序列化自己的JavaBean(实体类)。

一般讲,对于数据的存储来说Json的解析与序列化效率比Yaml更优,

而且在spigot 1.8+ 服务端内自带Google的Gson库,因此本文所讲的序列化将基于Gson库实现。你应该至少了解一下Gson的toJson()和fromJson()方法。

如何序列化ItemStack或Location?

如果只需要序列化单个ItemStcak 或者你说你可以遍历然后在yml 里直接set,那么这些可能没有任何帮助。你可以在前言中提到的教程贴了解,此处不过多赘述。

对于插件的数据存储,基本数据类型可能还好,直接new Gson()即可。 可是涉及到复杂的类型,应该如何序列化呢?

源码分析部分,不想看请跳过。

对于Gson 2.9.0 的tojson()方法,
    public String toJson(Object src) {
        return src == null ? this.toJson((JsonElement)JsonNull.INSTANCE) : this.toJson((Object)src, (Type)src.getClass());
    }

    public String toJson(Object src, Type typeOfSrc) {
        StringWriter writer = new StringWriter();
        this.toJson(src, typeOfSrc, (Appendable)writer);
        return writer.toString();
    }

    public void toJson(Object src, Type typeOfSrc, JsonWriter writer) throws JsonIOException {
        TypeAdapter adapter = this.getAdapter(TypeToken.get(typeOfSrc));
        boolean oldLenient = writer.isLenient();
        writer.setLenient(true);
        boolean oldHtmlSafe = writer.isHtmlSafe();
        writer.setHtmlSafe(this.htmlSafe);
        boolean oldSerializeNulls = writer.getSerializeNulls();
        writer.setSerializeNulls(this.serializeNulls);

        try {
            adapter.write(writer, src);
        } catch (IOException var13) {
            throw new JsonIOException(var13);
        } catch (AssertionError var14) {
            throw new AssertionError("AssertionError (GSON 2.8.5): " + var14.getMessage(), var14);
        } finally {
            writer.setLenient(oldLenient);
            writer.setHtmlSafe(oldHtmlSafe);
            writer.setSerializeNulls(oldSerializeNulls);
        }
    }复制代码
对于getAdapter方法这里不多分析。感兴趣自行翻看源码。可定位相关字段是factories

由此看来,Gson的序列化,是获得了对应的TypeAdapter后 调用它的write()方法。 反序列化同理。

但是对与它来说,它哪认识bukkit的ItemStack啊,因此需要对TypeAdapter进行手动注册。

因此现在不能生成默认的Gson,需要使用GsonBuilder并注册TypeAdapter。

如何注册? 对于GsonBuilder的相关源码如下

    public GsonBuilder registerTypeAdapter(Type type, Object typeAdapter) {
        Preconditions.checkArgument(typeAdapter instanceof JsonSerializer || typeAdapter instanceof JsonDeserializer || typeAdapter instanceof InstanceCreator || typeAdapter instanceof TypeAdapter);
        if (typeAdapter instanceof InstanceCreator) {
            this.instanceCreators.put(type, (InstanceCreator)typeAdapter);
        }

        if (typeAdapter instanceof JsonSerializer || typeAdapter instanceof JsonDeserializer) {
            TypeToken typeToken = TypeToken.get(type);
            this.factories.add(TreeTypeAdapter.newFactoryWithMatchRawType(typeToken, typeAdapter));
        }

        if (typeAdapter instanceof TypeAdapter) {
            this.factories.add(TypeAdapters.newFactory(TypeToken.get(type), (TypeAdapter)typeAdapter));
        }

        return this;
    }复制代码
可知传入需要传入的类需要实现或继承: InstanceCreator JsonSerializer JsonDeserializer 和TypeAdapter

此处已有通过实现JsonSerializer JsonDeserializer详细的方案,感兴趣的请前往查看。

以下将会基于创建TypeAdapter类的实现。(实际上效果一样,只是笔者一种习惯)

那么接下来我们看看TypeAdapter是个什么东西?

public abstract class TypeAdapter {
    public TypeAdapter() {
    }

    public abstract void write(JsonWriter var1, T var2) throws IOException;

    public abstract T read(JsonReader var1) throws IOException;
}复制代码
这个抽象类拥有这两个抽象方法! 这个write和read就是在toJson/fromJson方法中被调用。

因此我们实现这两个方法即可。

那么JsonWriter和JsonReader又是什么? (恼)

这次看看doc!

这里给出了一个Message的例子:

Suppose we'd like to parse a stream of messages such as the following:
[
    {
      "id": 912345678901,      
      "text": "How do I read a JSON stream in Java?",      
      "geo": null,      
      "user": {        
        "name": "json_newb",        
        "followers_count": 41      
        }   
    },   
    {      
       "id": 912345678902,      
       "text": "@json_newb just use JsonReader!",      
       "geo": [50.454722, -104.606667],      
       "user": {        
         "name": "jesse",        
         "followers_count": 2      
        }   
    }  
]复制代码public Message readMessage(JsonReader reader) throws IOException {   
        long id = -1;   
        String text = null;   
        User user = null;   
        List geo = null;      
        reader.beginObject();   
        while (reader.hasNext()) {      
            String name = reader.nextName();      
            if (name.equals("id")) {        
                id = reader.nextLong();      
            } else if (name.equals("text")) {        
                text = reader.nextString();      
            } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {        
                geo = readDoublesArray(reader);      
            } else if (name.equals("user")) {
                user = readUser(reader);      
            } else {
                reader.skipValue();      
            }   
        }   
        reader.endObject();   
        return new Message(id, text, user, geo);  
    }复制代码
这里相当于从Json变成Message,可见需要beginObject 然后 nextName (键) 和nextString (值)。 然后endObject。

那么write也是类似, 那就请读者自行分析。这里只使用结论。

总而言之就是在write中存入字段信息,然后在read中构造一个对象返回。

那么如何重写write 和read就显而易见了。

对于序列化Location :

首先我们先看看Location的结构~

public class Location implements Cloneable, ConfigurationSerializable {
    private Reference world;
    private double x;
    private double y;
    private double z;
    private float pitch;
    private float yaw;
}复制代码
Reference代表字段是引用。

什么 x y z pitch yaw 都好说, 那么world该怎么变成String?

此处我们可以绕路,只需要记录这个world就可以了,而不用真的把world序列化。Bukkit拥有一个Bukkit.getWorld()方法,因此我们仅需要获得world的UUID或name即可了,为了可读性,此处选择了name。

简单的得出了这样的代码:

    class LocationAdapter extends TypeAdapter {
        @Override
        public void write(JsonWriter json, Location location) throws IOException {
            json.beginObject();
            json.name("world").value(Objects.requireNonNull(location.getWorld()).getName());
            json.name("x").value(location.getX());
            json.name("y").value(location.getY());
            json.name("z").value(location.getZ());
            json.name("yaw").value(location.getYaw());
            json.name("pitch").value(location.getPitch());
            json.endObject();
        }

        @Override
        public Location read(JsonReader json) throws IOException {
            json.beginObject();
            String worldName = null;
            double x = 0;
            double y = 0;
            double z = 0;
            float yaw = 0;
            float pitch = 0;

            while (json.hasNext()) {
                String name = json.nextName();
                switch (name) {
                    case "world":
                        worldName = json.nextString();
                        break;
                    case "x":
                        x = json.nextDouble();
                        break;
                    case "y":
                        y = json.nextDouble();
                        break;
                    case "z":
                        z = json.nextDouble();
                        break;
                    case "yaw":
                        yaw = (float) json.nextDouble();
                        break;
                    case "pitch":
                        pitch = (float) json.nextDouble();
                        break;
                    default:
                        json.skipValue();
                        break;
                }
            }
            json.endObject();
            return new Location(Bukkit.getWorld(worldName), x, y, z, yaw, pitch);
        }
    }复制代码
简单的添加一些判空处理~

ps: json.peek() 就是... csgo 会peek对吧 ? 说的就是那个意思233

    class LocationAdapter extends TypeAdapter {
        @Override
        public void write(JsonWriter json, Location location) throws IOException {
            if (location == null) {
                json.nullValue();
                return;
            }
            json.beginObject();
            json.name("world").value(Objects.requireNonNull(location.getWorld()).getName());
            json.name("x").value(location.getX());
            json.name("y").value(location.getY());
            json.name("z").value(location.getZ());
            json.name("yaw").value(location.getYaw());
            json.name("pitch").value(location.getPitch());
            json.endObject();
        }

        @Override
        public Location read(JsonReader json) throws IOException {
            if (json.peek() == null) {
                return null;
            }
            json.beginObject();
            String worldName = null;
            double x = 0;
            double y = 0;
            double z = 0;
            float yaw = 0;
            float pitch = 0;

            while (json.hasNext()) {
                String name = json.nextName();
                switch (name) {
                    case "world":
                        worldName = json.nextString();
                        break;
                    case "x":
                        x = json.nextDouble();
                        break;
                    case "y":
                        y = json.nextDouble();
                        break;
                    case "z":
                        z = json.nextDouble();
                        break;
                    case "yaw":
                        yaw = (float) json.nextDouble();
                        break;
                    case "pitch":
                        pitch = (float) json.nextDouble();
                        break;
                    default:
                        json.skipValue();
                        break;
                }
            }
            json.endObject();
            if (worldName != null) {
                return new Location(Bukkit.getWorld(worldName), x, y, z, yaw, pitch);
            }
            return null;
        }
    }复制代码
此时你想序列化一个Location的时候,就不用存什么x y z 然后world给设置到yml里面了。

为什么不讲讲ItemStack呢!

当然你也可以像上面一样...查看字段然后这样这样那样那样的。

但它对特殊nbt的兼容很差!因此本文将使用NMS进行这些操作。

如何找到自己想要的NMS内容?

此处基于1.16.5

你应该大致了解  NMS-OBC-Bukkit 的关系!
大概很多萌新对于NMS的内容了解却不知道如何使用。

就像以上面的问题为例,  如何使用NMS 把ItemStack变成nmsItemStack后变为String保存?如何找到相关方法?

首先我们需要找到自己想要的类,例如ItemStack, 类名在NMS里面大概也没多少变化。

那么可找到net.minecraft.server.v1_16_R3.ItemStack (在1.17+为net.minecraft.world.item.ItemStack其余自行查阅)

而位于两者之间的OBC 则为org.bukkit.craftbukkit.v1_16_R3.inventory.CraftItemStack  可以看到它继承了bukkit的ItemStack。

字段与方法如下:

    net.minecraft.server.v1_16_R3.ItemStack handle;

    public static net.minecraft.server.v1_16_R3.ItemStack asNMSCopy(ItemStack original) {
        if (original instanceof CraftItemStack) {
            CraftItemStack stack = (CraftItemStack)original;
            return stack.handle == null ? net.minecraft.server.v1_16_R3.ItemStack.b : stack.handle.cloneItemStack();
        } else if (original != null && original.getType() != Material.AIR) {
            Item item = CraftMagicNumbers.getItem(original.getType(), original.getDurability());
            if (item == null) {
                return net.minecraft.server.v1_16_R3.ItemStack.b;
            } else {
                net.minecraft.server.v1_16_R3.ItemStack stack = new net.minecraft.server.v1_16_R3.ItemStack(item, original.getAmount());
                if (original.hasItemMeta()) {
                    setItemMeta(stack, original.getItemMeta());
                }

                return stack;
            }
        } else {
            return net.minecraft.server.v1_16_R3.ItemStack.b;
        }
    }

    public static ItemStack asBukkitCopy(net.minecraft.server.v1_16_R3.ItemStack original) {
        if (original.isEmpty()) {
            return new ItemStack(Material.AIR);
        } else {
            ItemStack stack = new ItemStack(CraftMagicNumbers.getMaterial(original.getItem()), original.getCount());
            if (hasItemMeta(original)) {
                stack.setItemMeta(getItemMeta(original));
            }

            return stack;
        }
    }

    public static CraftItemStack asCraftMirror(net.minecraft.server.v1_16_R3.ItemStack original) {
        return new CraftItemStack(original != null && !original.isEmpty() ? original : null);
    }复制代码
此处由bukkit ItemStack变为nms ItemStack无疑是使用:asNMSCopy

那么反过来应该用哪个呢? 是asBukkitCopy吗?  并不是! 因为这个方法中使用的是bukkit ItemStack的构造方法,所以某些情况下bukkit它不认识物品的特殊tag! 于是我们在这里选择asCraftMirror 由于返回值为CraftItemStack 它继承了bukkit的ItemStack ,不是吗! 所以很多情况你可直接把它当作bukkit的 ItemStack进行操作。

(实际上你在插件里获得到游戏中的ItemStack实际上是CraftItemStack,不信你在游戏中getItemInMainHand后输出 getClass试试?)

那么你完成了第一步,转换。转换后又该如何使用呢?

nms.NBTTagCompound! 都说nbt nbt 它到底是什么呢?NBT(Named Binary Tag)它是一种Mojang设计的 用于存储和传输数据的二进制格式。

所以我们要保存物品nbt 肯定就是依赖它了!

我们可以在nms.ItemStack中发现这些方法:

   private void load(NBTTagCompound nbttagcompound) {
        this.item = (Item)IRegistry.ITEM.get(new MinecraftKey(nbttagcompound.getString("id")));
        this.count = nbttagcompound.getByte("Count");
        if (nbttagcompound.hasKeyOfType("tag", 10)) {
            this.tag = nbttagcompound.getCompound("tag").clone();
            this.getItem().b(this.tag);
        }

        if (this.getItem().usesDurability()) {
            this.setDamage(this.getDamage());
        }

    }

    private ItemStack(NBTTagCompound nbttagcompound) {
        this.load(nbttagcompound);
        this.checkEmpty();
    }

    public static ItemStack a(NBTTagCompound nbttagcompound) {
        try {
            return new ItemStack(nbttagcompound);
        } catch (RuntimeException var2) {
            LOGGER.debug("Tried to load invalid item: {}", nbttagcompound, var2);
            return b;
        }
    }
    public NBTTagCompound save(NBTTagCompound nbttagcompound) {
        MinecraftKey minecraftkey = IRegistry.ITEM.getKey(this.getItem());
        nbttagcompound.setString("id", minecraftkey == null ? "minecraft:air" : minecraftkey.toString());
        nbttagcompound.setByte("Count", (byte)this.count);
        if (this.tag != null) {
            nbttagcompound.set("tag", this.tag.clone());
        }

        return nbttagcompound;
    }
复制代码
可通过 save(NBTTagCompound nbttagcompound) 和 a(NBTTagCompound nbttagcompound) 这两个方法通过NBTTagCompound来实现序列化与反序列化。

如何让NBTTagCompound 和String转换呢? 只差这一步了!

答案是toString和 MojangsonParser (JsonParser? Mojangson好像可以理解为特殊的Json吧?

它拥有一个静态方法

    public static NBTTagCompound parse(String var0) throws CommandSyntaxException {
        return (new MojangsonParser(new StringReader(var0))).a();
    }复制代码
传入String 输出NBTTagCompound

至此!我们把ItemStack通过NMS 与String转换的路捋清了!

即为ItemStack (CraftItemStack)   nmsItemStack nbtTagCompound String

都用NMS了 你总不会不了解反射吧!

相关实现代码样例 (代码丑勿喷555)

public class NMSUtils {
    private static Class nbtTagCompound;
    private static Class itemStack;
    private static Class MojangsonParser;
    private static Class CraftItemStack;
    private static Method parse;
    private static Method a; //nbtTagCompound->nmsItemStack
    private static Method asCraftMirror;
    private static Method asNMSCopy;
    private static Method save;
    private static Method toString;

    private static String NMS_PACKAGE = "";
    private static String OBC_PACKAGE = "";
    private final static String version;
    public static int versionToInt;

    static
    {
        String packet = Bukkit.getServer().getClass().getPackage().getName();
        version = packet.substring(packet.lastIndexOf('.') + 1);
        String nmsHead = "net.minecraft.server.";
        versionToInt = Integer.parseInt(version.split("_")[1]);
        try {
            if(versionToInt
                Class.forName(nmsHead + version +".ItemStack");
            else
                Class.forName("net.minecraft.world.item.ItemStack");
            NMS_PACKAGE = nmsHead + version;
            OBC_PACKAGE = "org.bukkit.craftbukkit." + version;
        } catch (ClassNotFoundException ignored){
        }
        try {
            CraftItemStack = Class.forName(OBC_PACKAGE+".inventory.CraftItemStack");
            if(versionToInt
                nbtTagCompound = Class.forName(NMS_PACKAGE+ ".NBTTagCompound");
                itemStack = Class.forName(NMS_PACKAGE+".ItemStack");
                MojangsonParser = Class.forName(NMS_PACKAGE+".MojangsonParser");
                parse = MojangsonParser.getMethod("parse",String.class);
                save = itemStack.getMethod("save",nbtTagCompound);
            }else {
                nbtTagCompound = Class.forName("net.minecraft.nbt.NBTTagCompound");
                itemStack = Class.forName("net.minecraft.world.item.ItemStack");
                MojangsonParser = Class.forName("net.minecraft.nbt.MojangsonParser");
                parse = MojangsonParser.getMethod("a",String.class);
                save = itemStack.getMethod("b",nbtTagCompound);
            }
            if(versionToInt>12)
                a = itemStack.getMethod("a",nbtTagCompound);
            asCraftMirror = CraftItemStack.getMethod("asCraftMirror",itemStack);
            asNMSCopy = CraftItemStack.getMethod("asNMSCopy", ItemStack.class);
            toString = nbtTagCompound.getMethod("toString");
        } catch (ClassNotFoundException | NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

    public static ItemStack toItem(String NBTString){
        Object nbt; //(nbtTagCompound)
        ItemStack item = null;
        Object nmsItemStack;
        try {
            nbt = parse.invoke(MojangsonParser,NBTString);
            if(versionToInt>12){
                nmsItemStack = a.invoke(itemStack,nbt);
            }else {
                if(versionToInt>9) {
                    Constructor constructor = itemStack.getConstructor(nbtTagCompound);
                    nmsItemStack = constructor.newInstance(nbt);
                }
                else {
                    //1.7.10
                    Method createStack = itemStack.getMethod("createStack", nbtTagCompound);
                    nmsItemStack = createStack.invoke(itemStack,nbt);
                }
            }
            item = (ItemStack) asCraftMirror.invoke(CraftItemStack,nmsItemStack);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return item;
    }
    public static String toNBTString(ItemStack itemStack){
        Object nbt;
        Object str = null;
        try {
            Object nmsItemStack = asNMSCopy.invoke(CraftItemStack,itemStack);
            Constructor constructor = nbtTagCompound.getConstructor();
            nbt = constructor.newInstance();
            save.invoke(nmsItemStack,nbt);
            str = toString.invoke(nbt);
        }catch (Exception e){
            e.printStackTrace();
        }
        return (String)str;
    }
}复制代码
扯了好多233  那么到这里,你总会如何创建一个ItemStack的 TypeAdapter了吧!

    class ItemAdapter extends TypeAdapter{

        @Override
        public void write(JsonWriter json, ItemStack item) throws IOException {
            if(item == null){
                json.nullValue();
                return;
            }
            json.beginObject();
            json.name("item").value(NMSUtils.toNBTString(item));
            json.endObject();
        }
        @Override
        public ItemStack read(JsonReader json) throws IOException {
            if (json.peek() == null) {
                return null;
            }
            json.beginObject();

            ItemStack item = null;
            while (json.hasNext()) {
                String name = json.nextName();
                if ("item".equals(name)) {
                    item = NMSUtils.toItem(json.nextString());
                } else {
                    json.skipValue();
                }
            }
            json.endObject();
            if(item==null){
                item = new ItemStack(Material.STONE);
            }
            return item;
        }
    }复制代码
具体使用就是。

    public static String ItemToJson(ItemStack item){
        Gson gson = new GsonBuilder()
                .setPrettyPrinting()
                .registerTypeAdapter(ItemStack.class, new ItemAdapter())
                .create();
        return gson.toJson(item);
    }复制代码
出来的String不是想存哪存哪?

当然上面的序列化并不是只能传入单一的ItemStack!

如何序列化复杂、嵌套的JavaBean?

JavaBean的概念...

JavaBean 是一种JAVA语言写成的可重用组件。为写成JavaBean,类必须是具体的和公共的,并且具有无参数的构造器。JavaBean 通过提供符合一致性设计模式的公共方法将内部域暴露成员属性,set和get方法获取。众所周知,属性名称符合这种模式,其他Java 类可以通过自省机制(反射机制)发现和操作这些JavaBean 的属性。
比如我现在有一个类MyPlayer 如下 我希望序列化与反序列化它。

@AllArgsConstructor
@NoArgsConstructor
@Data
public class MyPlayer {
    private String name;
    private int age;

}复制代码
此时直接使用new Gson()即可,对吧?

那么添加一个ItemStack字段呢?

@AllArgsConstructor
@NoArgsConstructor
@Data
public class MyPlayer {
    private String name;
    private int age;
    private ItemStack item;
}复制代码
用上文的ItemAdapter即可实现。

同时Gson的序列化对于数组、List与Map,也无需太大改变即可直接序列化。

Ps 昨天通过群u Vincen 的尝试Map的key必须为String。 当然为了避免内存泄漏,请勿以实体类当Key!

可是当我希望序列化一个如下的类:

public class Group{
    private List players;
    private int size;
}复制代码
此时并不可以直接

Gson gson = new GsonBuilder()
                .setPrettyPrinting()
                .registerTypeAdapter(ItemStack.class, new ItemAdapter())
                .create();复制代码
其实此时的MyPlayer已经类似于ItemStack了,你仍需要给与它的TypeAdapter

Gson gson = new GsonBuilder()
                .setPrettyPrinting()
                .registerTypeAdapter(ItemStack.class, new ItemAdapter())
                .registerTypeAdapter(MyPlayer.class, new MyPlayerAdapter())
                .create();复制代码
至此 完结撒花~

第一页 上一页 下一页 最后一页