这篇文章是来源于这个问题, 在回答这个问题之后我思考了一下,怎样存储数据才算优雅又高效,
因此有了这篇文章。

开发中的数据存储

本文的代码是 Groovy,你可以看做是没有行末分号、异常不用捕获的 Java。
本文代码以 MIT License 开源。

那么开始。

配置文件

配置文件是最易用也最常用的方法之一,
但是这不是本文的重点,因为配置文件的使用教程实在是太多了。

尽管配置文件的本意是给使用者自定义你的插件/Mod的行为,但是用来存储数据也是可以的。

为方便读者,这里给出一些其他人的教程。

数据库

数据库是个挺好的话题,给出一个 MySQL 教程

这篇文章不会讲解数据库,因为数据库也不是本文重点。但是额外提醒一点,用数据库注意阻塞和线程安全。

手动写二进制数据

存配置文件看起来很不优雅,还占空间;而存数据库对于一般插件好像又有点多此一举了,那么有没有又快又省空间的方法呢?

这节的标题就是,这一节也是真正的重点。

例子,我们要做一个玩家属性插件:

class PlayerData {
    int hp = 20
    double strength = 0
    boolean haveJob
    Job job
}

class Job {
    String name
    int level
    int exp
    String prefix // 假设我们的插件甚至还要搞聊天前缀
}

然后把这个实例变成二进制数据。

模仿 Bukkit 的那个 Serializable,利用 Netty Buffer 库,我们可以搞这么一个二进制数据序列化/反序列化的系统出来:

import com.google.common.collect.Maps
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import org.bukkit.plugin.java.JavaPlugin

import java.nio.charset.StandardCharsets
import java.util.function.Function

interface Serializable {
    void serialize(ByteBuf buf)
}

class PlayerData implements Serializable {
    int hp = 20
    double strength = 0
    boolean haveJob = false
    Job job

    @Override
    void serialize(ByteBuf buf) {
        buf.writeInt(hp)
        buf.writeDouble(strength)
        buf.writeBoolean(haveJob)
        if (haveJob) {
            job.serialize(buf)
        }
    }

    static PlayerData deserialize(ByteBuf buf) {
        PlayerData data = new PlayerData()
        data.hp = buf.readInt()
        data.strength = buf.readDouble()
        data.haveJob = buf.readBoolean()
        if (data.haveJob) {
            data.job = Job.deserialize(buf)
        }
        return data
    }
}

class Job implements Serializable {
    String name
    int level
    int exp
    String prefix // 假设我们的插件甚至还要搞聊天前缀

    @Override
    void serialize(ByteBuf buf) {
        Util.writeString(buf, name)
        buf.writeInt(level)
        buf.writeInt(exp)
        Util.writeString(buf, prefix)
    }

    static Job deserialize(ByteBuf buf) {
        Job job = new Job()
        job.name = Util.readString(buf)
        job.level = buf.readInt()
        job.exp = buf.readInt()
        job.prefix = Util.readString(buf)
        return job
    }
}

class Util {
    static Map<Class<?>, Function<ByteBuf, ?>> deserializers = Maps.newHashMap()
    static <T> void registerDeserializer(Class<T> cl, Function<ByteBuf, T> function) {
        deserializers.put(cl ,function)
    }
    static <T> T deserialize(Class<T> cl) {
        Function<ByteBuf, ?> function = deserializers.get(cl)
        if (function != null) {
            return (T) function.apply(Unpooled.buffer())
        }
        throw new Exception("Not registered class $cl")
    }
    static byte[] serialize(Object obj) {
        if (obj instanceof Serializable) {
            ByteBuf buf = Unpooled.buffer()
            obj.serialize(buf)
            return buf.array()
        }
        return null
    }
    static void writeString(ByteBuf buf, String value) {
        if (value == null) {
            buf.writeInt(0)
            return
        }
        byte[] arr = value.getBytes(StandardCharsets.UTF_8)
        buf.writeInt(arr.length)
        buf.writeBytes(arr)
    }
    static String readString(ByteBuf buf) {
        int len = buf.readInt()
        if (len == 0) return null
        byte[] arr = new byte[len]
        buf.readBytes(arr)
        return new String(arr, StandardCharsets.UTF_8)
    }
}

class Plugin extends JavaPlugin {
    @Override
    void onEnable() {
        Util.registerDeserializer(PlayerData, PlayerData::deserialize)
        Util.registerDeserializer(Job, Job::deserialize)
    }
}