配置节点

在内存中,配置文件是以若干 ConfigurationNode 的组合表达出来的。一个 ConfigurationNode 要么持有一个值(一个数、字符串、一个 List、等等),要么持有若干呈树状结构的子节点。使用 ConfigurationLoader 加载或创建配置文件时返回的是根节点。我们建议你在某个地方保存这个根节点引用,省得每次读取配置文件都需要重新加载一遍配置。这样做的一个副作用是配置会连同其注释一起保存起来。另外一种解决方案是使用可序列化的配置类对象来保存你的插件的全部配置选项。

注解

通过对 ConfigurationLoader 的使用,你甚至可以获取到一个 CommentedConfigurationNode 。除了正常的 ConfigurationNode 提供的行为之外, CommentedConfigurationNode 还能够保留配置文件中保存的注释。

数据值

基本值

一些基本的值诸如 intdoubleboolean 、或者 String 每个都有对应的方便的 Getter ,在该节点不包含对应类型的值时返回默认值。让我们检查我们的服务器管理员是否想要我们的插件允许启用 blockCheats 模块,也就是检查 modules.blockCheats.enabled 中配置的值。

boolean shouldEnable = rootNode.getNode("modules", "blockCheats", "enabled").getBoolean();

是的,它就是这么简单。和上面的示例类似,一些诸如 ConfigurationNode#getInt()ConfigurationNode#getDouble() 、或者 ConfigurationNode#getString() 等方法同样可以用于方便地获取想要的数据类型。

只需调用 ConfigurationNode#setValue(Object) 即可设定某一节点的值。不要被它接受 Object 所迷惑了——这样只是能让它接受任何值,具体的处理方式是由节点自己决定的。

想象一下 blockCheats 模块可以通过用户命令停用。你可以通过下面的代码将其反映在配置中:

rootNode.getNode("modules", "blockCheats", "enabled").setValue(false);

警告

任何非“基本类型”均无法通过这样的基础方法处理,必须通过下文中描述的(反)序列化方法进行读写。这里的“基本类型”指那些 ConfigurationLoader 的底层实现中已经能处理的类型,比如原生数据类型、String 及包含前两者的 ListMap

序列化和反序列化

如果你想要读取或者存储一个并不属于上面提到的数据类型的对象,你需要首先将其反序列化。在用于创建你的 ConfigurationNodeConfigurationOptions 中,有一系列的 TypeSerializer 可以被 Configurate 用于把你的对象序列化成一个 ConfigurationNode ,反之亦然。

为了告诉 Configurate 你需要处理的是什么数据类型,我们需要提供一个 Guava 的 TypeToken。现在想像我们需要通过 towns.aFLARDia.mayor 对应的节点读取玩家的 UUID。为了达到目的,我们需要通过调用 getValue(…) 方法,并传入一个代表 UUIDTypeToken

import java.util.UUID;

UUID mayor = rootNode.getNode("towns", "aFLARDia", "mayor").getValue(TypeToken.of(UUID.class));

这会告知 Configurate 查找适用于 UUIDTypeSerializer 以将存储的值转化为 UUID。如果 TypeSerializer(乃至上面的方法)获取到了不完整或者不正确的数据,它可能会抛出一个 ObjectMappingException

用于把一个新的 UUID 写入该配置节点的代码格式十分相似。只需要通过使用 setValue(…) 方法,并传入一个 TypeToken 和你想要序列化的对象。

rootNode.getNode("towns","aFLARDia", "mayor").setValue(TypeToken.of(UUID.class), newUuid);

注解

如果找不到适用于给定 TypeTokenTypeSerializer ,执行序列化数据的代码就会抛出一个 ObjectMappingException

对于像 UUID 这样的简单类,你只需要通过 TypeToken#of(Class) 这一静态方法创建一个 TypeToken 即可,虽然像 UUID 这样的常用类已经有对应的 TypeToken 可用了(例如 TypeTokens#UUID_TOKEN),你没必要自己再新建一个。不过当你想要使用带有泛型的类型(诸如 Map<String,UUID>)时,代码格式会稍稍麻烦一点。在大多数情况下你只需要通过匿名内部类的方式继承这一 TypeTokennew TypeToken<Map<String,UUID>>() {} 就可以了。这样在编译后的代码中仍然会保存相应的泛型,因此即使是带有泛型,我们仍然可以很方便地存储和读取数据。

参见

关于 TypeToken 的更多信息,请参阅 Guava 官方文档

小技巧

SpongeAPI 提供了一个包含大量预先定义的class供开发者使用。如果插件开发者需要用到大量不同的或有复杂结构的 TypeToken,抑或需要频繁使用这些 TypeToken,我们建议你使用类似的策略,即使用一个专门的类来保存这些一份这些 TypeToken 的引用,以提高代码可读性。(请注意,这不代表它们都有对应的 TypeSerializer。)

你可以在对象序列化的介绍页中找到一份不完整的原生支持的类型列表,以及添加新的支持类型的方式。

默认值

和 SpongeAPI 不同,Configurate 库并不使用 Optional 以表示可能返回 null 的数据。尽管用于原始数据类型的 Getter 方法(诸如 getBoolean()getInt() )会返回诸如 false 或者 0 这样的值,但一些会返回对象的方法(诸如 getString() )可能会在不存在值时返回 null 。如果你并不想手动处理这些特殊的情况,你可以使用 默认值 以解决问题。上面提到的所有诸如 getXXX() 的方法,都分别有一个重载的方法用于传入额外的作为默认值的参数。

让我们再一次看一看读取布尔值的代码示例。

boolean shouldEnable = rootNode.getNode("modules", "blockCheats", "enabled").getBoolean();

这一方法在对应值不存在或者对应值被设置为 false 时都将返回 false 。由于我们根本没有办法分辨这两个情况,因此如果我们想要把配置文件中的默认值指定为 false ,我们也没有什么更简单的方法了。除非我们想要指定默认值为 true

boolean shouldEnable = rootNode.getNode("modules", "blockCheats", "enabled").getBoolean(true);

你同样可以指定你从配置中获取到的任何数据的默认值,因此避免了返回一个 null ,或者抛出一个 ObjectMappingException ,从而导致数据的缺失。当然你在反序列化 getValue() 方法时也可以使用默认值,下面就是一些例子:

String greeting = rootNode.getNode("messages", "greeting")
        .getString("FLARD be with you good man!");

UUID mayor = rootNode.getNode("towns", "aFLARDia", "mayor")
        .getValue(TypeTokens.UUID_TOKEN, somePlayer.getUniqueId());

另一个用途是在需要的时候把默认值复制到你的配置中。在创建你的根节点时,你可以传入调用了 setShouldCopyDefaults(true) 方法的 ConfigurationOptions 。随后你只要提供了一个默认值, Configurate 将首先检查值是否存在,如果不存在,那么它将会在使用默认值返回之前保存这一默认值到你的配置当中。

让我们假设你的插件是第一次运行,也就是配置文件并不存在。你试着通过设置 ConfigurationOptions 以在加载时允许复制默认值,然后你就获取到了一个空的配置节点。当你执行代码 rootNode.getNode("modules", "blockCheats", "enabled").getBoolean(true) 时,因为节点并不存在, Configurate 便自动创建了这么一个节点,并根据之前 ConfigurationOptions 决定的方式,写入一个 true 。这一 true 值将一直存在于节点中,我们并不需要显示设置它。