序列化对象

Configurate 库提供了修改自动(反)序列化对象的方法。默认,下列对象可被(反)序列化:

備註

如果你需要在序列化时追加特定规则(比如为 Set 中的元素排序),你应考虑实现自己的 TypeSerializer

但如果你需要在配置文件中使用你自己的数据结构,默认支持的类型则还远远不够。

想象一个用于跟踪一个玩家已经挖掘了多少钻石的数据结构。它可能看起来有点像这样:

public class DiamondCounter {
    private UUID playerUUID;
    private int diamonds;

    [...]
}

此外,我们还假定一些方法用于访问这些字段,一个漂亮的构造函数用于设置,等等。

自定义一个 TypeSerializer

一种非常直接的编写和加载这种数据结构的方法是提供一个自定义的 TypeSerializerTypeSerializer 接口提供了两种方法,一种用于将数据从对象写入配置节点,一种用于从给定的配置节点创建对象。

import com.google.common.reflect.TypeToken;
import ninja.leaping.configurate.objectmapping.ObjectMappingException;
import ninja.leaping.configurate.objectmapping.serialize.TypeSerializer;

public class DiamondCounterSerializer implements TypeSerializer<DiamondCounter> {

    @Override
    public DiamondCounter deserialize(TypeToken<?> type, ConfigurationNode value)
      throws ObjectMappingException {
        UUID player = value.getNode("player").getValue(TypeToken.of(UUID.class));
        int diamonds = value.getNode("diamonds").getInt();
        return new DiamondCounter(player, diamonds);
    }

    @Override
    public void serialize(TypeToken<?> type, DiamondCounter obj, ConfigurationNode value)
      throws ObjectMappingException {
        value.getNode("player").setValue(obj.getPlayerUUID());
        value.getNode("diamonds").setValue(obj.getDiamonds());
    }
}

这一 TypeSerializer 必须通过使用 Configurate 来注册。可以通过全局的注册方式,也就是通过注册到默认的 TypeSerializerCollection ,也可以通过本地的注册方式,也就是在加载配置文件时自定义 ConfigurationOptions 来完成。

備註

ConfigurationOptions 不可变。所有对原始的 ConfigurationOptions 对象的修改都会产生一个新的 ConfigurationOptions 对象,你应当使用链式调用或及时更新你的变量或字段。

代码示例:注册全局的 TypeSerializer

import ninja.leaping.configurate.objectmapping.serialize.TypeSerializers;

TypeSerializers.getDefaultSerializers().registerType(TypeToken.of(DiamondCounter.class), new DiamondCounterSerializer());

代码示例:注册本地的 TypeSerializer

import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.ConfigurationOptions;
import ninja.leaping.configurate.objectmapping.serialize.TypeSerializerCollection;
import ninja.leaping.configurate.objectmapping.serialize.TypeSerializers;

TypeSerializerCollection serializers = TypeSerializers.getDefaultSerializers().newChild();
serializers.registerType(TypeToken.of(DiamondCounter.class), new DiamondCounterSerializer());
ConfigurationOptions options = ConfigurationOptions.defaults().setSerializers(serializers);
ConfigurationNode rootNode = someConfigurationLoader.load(options);

警告

如果你为在你自己的插件中没有引入的类型提供了一个自定义的 TypeSerializer ,那么你应该只在本地注册它们,以避免因为 TypeSerializer 被覆盖,而引起的与其他插件或 Sponge 的冲突。

小訣竅

如果你需要在不止一个地方使用 TypeToken.of(DiamondCounter.class),你应当考虑使用一常量字段保存它,然后转而引用此常量字段。你可以考虑用类似 Sponge 的 TypeTokens 类的解决方案,或者干脆写进序列化器的类里面。

使用 ObjectMapper

因为在许多情况下,序列化和反序列化都可以归结为映射字段到配置节点,编写这样的 TypeSerializer 是一个相当沉闷无聊的事情,因此我们希望 Configurate 自己做。所以让我们使用 ConfigSerializableSetting 两个注解,来注解我们的类。

import ninja.leaping.configurate.objectmapping.Setting;
import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;

@ConfigSerializable
public class DiamondCounter {

    @Setting(value="player", comment="Player UUID")
    private UUID playerUUID;
    @Setting(comment="Number of diamonds mined")
    private int diamonds;

    [...]
}

以上示例现在可以从配置节点序列化和反序列化,而无需进一步注册。 @Setting 这一注解将配置节点映射到已被注解的字段。 它接受两个可选参数, valuecomment 。如果 value 参数存在,它定义了用于保存字段的节点的名称。如果不存在,将使用字段的名称。所以在上面的例子中,字段的注解确保字段 playerUUID 的内容保存到节点“player”,并使用“Player UUID”注释。不过,“diamonds”字段将被直接保存在“diamonds”名称下,因为它的注解仅仅指定了注释。如果实现的配置节点支持注释,那么该注释将写入配置,否则将被丢弃。

小訣竅

你也可以使用简写的 @Setting("someNode") 而不是 @Setting(value="someNode")

通过 @ConfigSerializable ,我们不需要任何的注册,因为它允许 Configurate 为被注解的类生成一个对应的 ObjectMapper 。唯一的限制是 Configurate 需要一个空的构造函数来实例化一个新的对象,然后填充被注解的字段。

備註

在你标记有 @ConfigSerializable 的类中也可以有不带 @Setting 的字段。这些字段将不会被持久化,你可以利用这一点来保存临时引用。

在 ConfigSerializable 类型中使用默认值

你可以通过使用字段初始化或访问器来为那些 @ConfigSerializable 类型的字段提供默认值。只要访问它们的方式不暴露在外,它们的值就不会被意外覆盖。

@ConfigSerializable
public class DiamondCounter {

    @Setting(value="player", comment="Player UUID")
    private UUID playerUUID;

    @Setting(comment="Number of diamonds mined")
    private int diamonds = 0;

    @Setting(comment="The time the player found a diamond last.")
    private LocalDateTime diamonds = LocalDateTime.now();

    [...]
}

代码示例:加载使用 ConfigSerializable 的注解,带默认值

你可以要求 Configurate 帮你在缺失配置文件时直接新建一个,这样就不用自己从自己的插件的 jar 中提取默认配置文件了。

try {
    this.config = this.configManager.load().<Configuration>getValue(Configuration.TYPE, Configuration::generateDefault);
} catch (ObjectMappingException | IOException e) {
    this.logger.error("Failed to load the config - Using a default", e);
    this.config = Configuration.generateErrorDefault();
}

在这个例子中,你的整个配置文件都会载入一个 Configuration 对象中,也就是说这个对象包括了你的插件的所有配置选项。使用这样一个类有这些优点:

  • 类型安全有保证

  • 无需更新你的插件捆绑携带的默认配置

  • 不需要为一个选项保存一堆引用

  • 你可以将整个配置类的对象(或它的一部分)当方法参数传递,或在别的类中直接引用

  • 可以就地为配置选项写注释,方便开发

備註

在这个示例中,Configuration.generateDefault() 会在配置文件缺失或为空的时候调用。如果你仍然想用你的 assets 中自带的默认配置,你可以把这个调用换成加载你自带的配置。在读取或解析配置出错时,会调用 Configuration.generateErrorDefault()。除此之外,你还可以直接使用零参构造器,甚至是你自己的一套解决方案。

代码示例:保存基于 ConfigSerializable 的配置文件

保存基于 @ConfigSerializable 的配置文件也很简单,如下所示:

try {
    this.configManager.save(this.configManager.createEmptyNode().setValue(Configuration.TYPE, this.config));
} catch (IOException | ObjectMappingException e) {
    this.logger.error("Failed to save the config", e);
}

提供自定义的 ObjectMapperFactory

然而,如果我们使用不同的 ObjectMapperFactory ,例如 GuiceObjectMapperFactory ,那么这个限制可以被解除。它不需要一个空的构造函数,因此可以工作在任何类,因为 Guice 可以通过依赖注入创建。这也允许混合使用 @Inject@Setting 用于注解字段。

你的插件只需通过依赖注入(参见 相依注入 )获取一个 GuiceObjectMapperFactory ,然后将其传递给 ConfigurationOptions

import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.state.GamePreInitializationEvent;
import org.spongepowered.api.plugin.Plugin;
import com.google.inject.Inject;
import ninja.leaping.configurate.commented.CommentedConfigurationNode;
import ninja.leaping.configurate.loader.ConfigurationLoader;
import ninja.leaping.configurate.objectmapping.GuiceObjectMapperFactory;

@Plugin(name="IStoleThisFromZml", id="shamelesslystolen", version="0.8.15", description = "Stolen")
public class StolenCodeExample {

    @Inject private GuiceObjectMapperFactory factory;
    @Inject private ConfigurationLoader<CommentedConfigurationNode> loader;

    @Listener
    public void enable(GamePreInitializationEvent event) throws IOException, ObjectMappingException {
        CommentedConfigurationNode node =
          loader.load(ConfigurationOptions.defaults().setObjectMapperFactory(factory));
        DiamondCounter myDiamonds = node.getValue(TypeToken.of(DiamondCounter.class));
    }
}

備註

上面的代码只是一个例子。为了简洁,上面的代码缺少适当的异常处理。