Skip to content

Latest commit

 

History

History
442 lines (264 loc) · 19.1 KB

2-1.md

File metadata and controls

442 lines (264 loc) · 19.1 KB

2-1 配置文件与 JavaDocs

欢迎回来!希望你的「Hello World」项目运行顺利。

什么是配置文件

现在我们的插件还只能读取我们预先设定好的数据,如果服主想要输出「Hello Ugly World」呢?如果每次都要修改代码重新编译,未免太麻烦了吧!

于是,配置文件出现了。

配置文件(Config),顾名思义,是一种「设置」,里面记录了许多内容。

配置文件由服主修改,插件读取,这样就不必修改插件的代码了。

YAML 格式

像字典一样

配置文件都是 YAML 格式,类似于这样:

key: value
key2: value2
key3:
  key3-1: 2333

我们说过 YAML 就像字典,那么字典中的一项是不是可以指向另一个字典啊?key3-1 就是一个例子。它是嵌套的一个字典,这叫做子键。子键下面还可以有子键的子键……就是这样。

我们注意到 key3-1 的前面有两个空格,这叫缩进(Indent),如果没有这个缩进,YAML 就不知道这是 key3 的一个子键还是一个独立的键了。因此缩进不能去掉。

按照规范,YAML 中每进入下一级,就要使用两个空格进行缩进。如果是子键的子键,就要空 4 格,子键的子键的子键就空 6 格,以此类推。

字典中都有些什么?

在 YAML 的「字典」中,冒号右边并不是什么都可以放的。实际上,YAML 中能放的东西很有限:

  • 一个数字
  • 一段文本
  • 其它的字典
  • 列表
  • 一个逻辑值(truefalse,真或假)

由于数字也可以被认为是文本,因此 YAML 采用特殊优先原则,也就是,先看看能不能认为它是个数字,如果能,就认为是数字,否则认为是文本。

number: 123456789
text-no-quote: 这是文本
text-quote: "123456789"
bool: true
list:
  - "一个文本"
  - 12345
  - true
  - 其它的文本

有关 YAML 的详细语法,可参见 菜鸟教程上有关 YAML 的内容

采用 YAML 并非因为它的效率或者可读性,而是为了简单的配置方式,正因如此,YAML 适合人类修改,机器读取。

实际上易于修改是一个谎言,YAML 采用严格的缩进结构,据不完全统计,超过 98% 的 YAML 配置文件错误都是缩进不当导致的。

默认的 config.yml

Bukkit 为每个插件提供了一个默认配置文件,要使用它,只需要在 src 下创建 config.yml,并在代码中写上:

saveDefaultConfig();

这个方法会帮我们保存默认的配置文件,也就是 config.yml

Bukkit 很聪明,这个方法会自己查看,如果有了 config.yml 就不进行操作,如果没有就复制一份。

关于读取,可以通过调用 getConfig 方法获得这个文件的对象(面向对象!)并将它保存在内存的某个地方:

FileConfiguration config = getConfig();

这里出现了新的 Java 语法。

这条语句是赋值语句,用于将一个变量「设定」为一个对象。

使用变量有声明赋值两步操作,不过它们可以一起用。

声明的语法:

类型 标识符;

赋值的语法:

标识符 = 右边的值;

可以看到,声明时需要指定类型,后面赋值和使用时就不需要了。

一个变量只能声明一次,但可以多次赋值(final 除外)。

你可以把变量认为是一个小盒子,里面可以装东西。上面这条语句相当于指示 Java 在内存里申请一个盒子用来存放数据。在创建盒子时,Java 需要知道里面要装什么东西,不然就可能出现放不下的问题。

在这里,由于 getConfig 返回的值是 FileConfiguration 类型,我们也需要使用对应的类进行「盛装」。那我是怎么知道这一点的呢?别急,我们在后面就会看到。

操作配置文件

现在我们得到了 FileConfiguration 的一个实例,这个类是由 Bukkit 提供的,可以使用一些方法进行操作它。最常用的是 getset 方法及其变种。

如何使用 JavaDocs

且慢,现在我是可以把使用方法告诉你,但将来如果你还需要知道其它类的方法,怎么办呢?另外,Bukkit 也不是我编写的,我是怎么知道的呢?

其实很简单,Spigot 已经把这些信息都写好了,我们只需要去查就可以了。这种资料叫做文档(Documentation),是给人类读的而不是给 Java 执行的。

Spigot 使用一种叫做 JavaDoc 的工具来生成文档,生成的文档已经发布到了 Spigot 的官网上。由于 Spigot 的官网比较慢,而且还不能看以前的版本,这可不行啊!因此,我把 Paper 的文档链接交给你:

https://papermc.io/javadocs/paper/1.16/overview-summary.html

这里就是 Paper 的 JavaDocs。Paper 是基于 Spigot 改进而来的(我们似乎说过这个?),因此 Paper 的文档中也具有 Bukkit API 的全部介绍。

那我们要怎么使用它呢?首先打开上面那个网址,右上角有一个「搜索」栏,在那里,你就可以搜索类了。不仅如此,JavaDocs 还可以搜索方法和变量呢!太聪明了~

JDOC.png

那么我们现在就来试试吧!首先我们知道,FileConfiguration 类是我们需要找的类,那么我们搜索它……(注意大小写!)

SEARCH.png

呃……好多结果啊,但由于我们找的是类,因此我们要看「Types」。下面的「Members」表示各个类具有的方法和变量,我们不需要管。

类在 Java 中被称为 Class,但在英语中,「类型」一词的正确翻译是 Type,所以搜索结果中也使用了 Type 而非 Class。

FileConfigurationOptions 显然不是我们需要的类,那么我们忽略掉,就只剩下一个了,点击一下,打开之后像这样:

CLAZZ1.png

CLAZZ2.png

上面有两张图,我给你标出来了几个部分。

紫色区域是包名,这个你应该知道是什么吧?

蓝色区域是继承关系,可以告诉你当前的这个类继承了哪一个类,而那个类又继承了哪一个类等等。

棕色区域是类签名,那什么是签名呢?

签名(Signature)就是去掉 {} 之外的部分。

我们定义类时,是这么定义的:

public class SomeClass extends XXXXClass {
    // ... 一堆东西
};

去掉后面的 {} 以及里面的东西,剩下的就叫签名。上面这个类的签名就是:

public class SomeClass extends XXXXClass

签名能够用于识别一个类。不过下面很快我们就要看到它的另一个重要用途。


现在我们接着往下看。「Field Summary」是什么东西啊?

「Field」是类的成员变量,也叫实例变量,比如,如果一条狗是一个对象,身高就是它的一个「Field」,如果一个插件是一个对象,版本可能就是它的一个「Field」。总之,成员变量就是属于一个对象的变量。访问成员变量和访问成员方法一样,都是使用点(.),这个点同样表示「的」。

这里值得说明的一点是,变量本身也是一个对象,它也有自己的成员变量,所以可能会出现这样的情况:「我的邻居家的亲戚家养的狗的牙齿的长度」,这当然是没问题的。很简单?确实也没什么难的嘛……

那么「Field Summary」提供的信息就很明显了吧,它提供的是变量名而没有显示变量的类型,用你的鼠标单击变量名就能查看它的类型了。

不对!这里为什么还有「Fields inherited from」这样的东西?这是什么意思?

嗯,我们之前讲到过继承,「Inherite」就是继承的意思。图中的信息表示,MemoryConfiguration 具有两个叫做 defaultsoptions 的成员变量,FileConfiguration 继承了它,因此也具有了这两个成员变量。而 MemoryConfiguration 继承了 MemorySectionMemorySection 具有一个名为 map 的成员变量,它将这个变量交给MemoryConfigurationMemoryConfiguration 再将它交给 FileConfiguration,这样 FileConfiguration 中就也有了 map 变量。

说了这么多,继承来的东西无非就是:「你有的我都有,你没有的我也有。」


我们接着往下看,下面出现了「Constructor Summary」,这是构造方法

构造方法是一个特殊的方法,它用创建一个对象。构造方法在定义时无法指定返回值。

我们说过,一般的成员方法都要通过 <对象>.方法 进行调用,那构造方法怎么办呢?我们还没有对象呢!

这就不得不说到 new 关键字了,new 用于指示 Java 创建一个对象。语法如下:

new 类型(参数);

构造方法的名称和类名称完全一样,返回值就是一个新的对象(它叫构造方法嘛),可以交给变量存储起来。

现在再看「Constructor Summary」,就很明白了吧?它告诉我们,要构造一个新的 FileConfiguration,可以通过调用这两个方法之一。

不过等等啊,这两个方法的名字为什么完全一样呢?

那当然了,构造方法的名字必须和类名一样嘛~

那为什么要成为两个方法呢?Java 怎么知道我们要调用哪个方法呢?

事实上,Java 在调用方法时,并不是使用方法名进行判断的,而是使用方法签名进行判断的。

按照上面类签名的思路,我们把方法的大括号(方法体)去掉:

public void SomeMethod(String arg1, int arg2)

你看,不只有方法名嘛!参数列表,返回值(构造方法无法直接指定返回值),访问修饰符,只要有一个不一样,也是可以区分的嘛!

因此,我们完全可以编写很多不同的方法,以处理不同的参数,调用方法的人也很方便,无论怎么调用,名字都一样,多方便啊!

以后我向你展示某个方法时,如未特别注明,给你看的都是方法签名,这也是大多数开发人员的习惯。

现在我们再看这两个 FileConfiguration 的构造方法,就很明白了吧?一个不接受参数,一个接受一个类型为 Configuration 的参数。


最后是「Method Summary」,看到这里你应该已经能够独自阅读了吧?

「Modifier and Type」表示修饰符和返回值,「Method」表示方法名和参数列表,「Description」是 Bukkit 开发人员为我们写的注释。

修饰符由访问修饰符和其它修饰符构成。

说了那么久的访问修饰符,到底啥是访问修饰符捏?

访问修饰符用于控制一个变量方法是否能够被外部访问,有且仅有三个访问修饰符:

  • public:这个类、方法或变量可以在任何地方被使用
  • protected:由于不能修饰外部类,因此它表示该方法或变量只能被子类(继承者)使用,「外族人不得擅入」!
  • private:由于不能修饰外部类,因此它表示该方法或变量只能被自己(当前类)使用,子类和其它类都不能使用

另外还有几个其它修饰符:

  • abstract:不能修饰变量,修饰方法时表示该方法没有被完整实现(只是一个空壳),不能被调用,如果一个类中有 abstract 的方法,这个类也要 abstract;如果要实例化一个 abstract 的类,必须继承它并重写(@Override)那些 abstract 方法(完成先辈未竟的事业)
  • final:不能与 abstract 一起用,修饰类时表示该类不能被继承,修饰方法时表示子类不可以重写该方法,修饰成员变量时表示该变量一经创建不可修改
  • static:不能修饰外部类,修饰方法时表示该方法不属于哪个对象,而属于整个类共有;修饰变量时表示该变量不属于哪个对象,属于整个类共有,要调用静态方法和变量,不使用 <对象名>.<方法或变量的名字>,而使用 <类名>.<方法或变量的名字>

这里简单了解即可。在 JavaDocs 中,public 会被省略掉以便查看。


现在我们终于可以回到正题上来了!我们来找找 setget 方法……注意,我们要找的方法可能不只是叫 getset,它们的变种可能有类似的名字。

咦?怎么没有?

啊哈,找到了,它不在「Method Summary」中,而在「Methods inherited from interface org.bukkit.configuration.ConfigurationSection」中。

GETSET.png

可以看到这里有非常多的 getXXX 版本用于取得不同类型的数据,而只有一个 set 方法,这一个 set 就可以对付所有情况了。点击上面的方法可以获得它的详细签名。

好啦!到这里,你就会使用 JavaDocs 了,以后,我们还会经常见到它的~

YAML 寻址

假设有这么一份配置文件:

mysql:
  use-mysql: true
  host: localhost
  auth-data:
    password: 123456
auto-login-delay: 300

我们怎么读取其中的值呢?

要知道怎么读取其中的值,我们需要知道每一个键的「地址」,找出这个键的过程就叫寻址

YAML 的寻址规则很简单:

<上一个键>.<子键名>.<子键名>

譬如,上面 password 的地址就是:

mysql.auth-data.password

host 的地址就是:

mysql.host

auto-login-delay 的地址就是:

auto-login-delay

这样我们就能确定各个键的地址了。知道这一点有啥用捏?

直接读取

上面我们看到了那么多的 get 以及一个万能的 set,要让它们工作,我们需要给它们提供参数。查询 JavaDocs,我们查到了 get 的方法签名:

@Nullable
Object get(@NotNull String path);

@Nullable 表示返回值可能是 null@NotNull 则相反,表示这个值不能为 null,去掉它们也不影响阅读,去掉它们之后,方法签名终于出现在我们的面前:

Object get(String path);

返回值是 Object,需要提供一个参数:String 类型,名叫 path

再看看 Bukkit 的说明:「Gets the requested Object by path.」

嗯,我们没找错地方。这里的 path 就是地址。

因此我们要读取上面配置文件中的 password,用 Java 就该这么写:

FileConfiguration config = getConfig();
String password = (String) config.get("mysql.auth-data.password");

这里出现了一点新东西:(String),这表示「强制转换」。因为 get 方法不是针对字符串设计的,它返回 Object 类,这个类是任何类的父类。

但是这样可不行啊,明明是一个 String,却只能当 Object 用,太憋屈了!在这种情况(我们比 Java 更清楚这是什么)下,我们可以使用强制类型转换(Cast)尝试进行转换。(String) 的作用正是如此。Java 会先判断是否可以转换,如果可以,它就勉为其难地转换;如果不行,已经火冒三丈的 Java 就会抛出一个错误并结束当前线程。这样做太危险了!

安全读取

实际上,我们可以使用 get 的姐妹 getString 来获得 String 的结果,它会帮我们「温柔地」完成转换,即使转换不了,她也最多只会返回一个 null

这样很好,但我们还不满足。即使返回 null 也可能引发喜闻乐见的 NullPointerExceptiongetString 想到了这个情况,因此用同一个名字,创建了下面这个方法:

String getString(String path, String def)

第二个参数 def 指定一个默认值,当 getString 无法转换时将返回 def 指定的值,而不是 null

上面那个配置文件示例中还有数字,这要怎么获取呢?

整数在 Java 中的类型是 Integer 或者 int,因此在 FileConfiguration 中可以通过以下方法获得:

int getInt(String path, int def)

同样,逻辑(truefalse)可以使用:

boolean getBoolean(String path, boolean def)

现在我们就把各个值都读出来吧!

boolean useMySQL = config.getBoolean("mysql.use-mysql", false);
String host = config.getString("mysql.host", "localhost");
// 这是注释,一行有效
String password = config.getString("mysql.auth-data.password", "");
String autoLoginDelay = config.getInt("auto-login-delay", 300);

怎么样?很简单吧?实际上我是为了介绍 Java 才说了这么多内容,只谈配置文件的话,没什么内容的。

这里我再多说一点,getXXXX 方法可以 get 很多类型,比如 ItemStack(表示一个物品堆),Color(表示一种颜色)等等,这是 Bukkit 针对游戏进行的优化,将来我们也许会见到它们的。如果实在等不及,那就去查查 JavaDocs 看看它们怎么用吧!

写入

写入只有一个 set 方法,我们来看看它的签名:

void set(String path, Object value)

没有返回值,提供一个 path 和一个 value

那么我们像上面读取这些值一样,演示一下设置的方法:

config.set("mysql.use-mysql", true);
config.set("mysql.auth-data.password", "123456");
config.set("mysql.host", "127.0.0.1");
config.set("auto-login-delay", 1000);

这样很简单吧?

等等!为什么第二个参数是 Object,你却能把 Stringintboolean 给它们?为什么这里不需要 (Object) "123456" 这样的操作?

首先,你回答我一个问题:

String 是不是一种 Object

当然是啦!

那,String 既然是一种 Object,为什么 Java 不能把它当作 Object 看待呢

……可是,你说,上面那个 get……哦!我明白了!因为 get返回的是 ObjectObject 不一定是一种 String,对不对?

是的。从子类转向父类没有风险,Java 会毫不犹豫地自动完成,而从父类转向子类有风险,需要我们亲自指示 Java 这样做。

写入之后还没完,我们只修改了内存中的一个副本,要将修改写入硬盘,还要保存:

saveConfig();

这个方法是 JavaPlugin 的一个方法,Bukkit 自动帮我们完成写入工作。记住:只有保存后,文件才会被真正写入硬盘

读取时则不需要保存,因为读取没有修改那个副本。

删除

如果要你删除一个键,已经学过 JavaDocs 的你会想到什么?

唔……有没有 delete 方法?你看,set 可以设置任何键,应该有这样的 delete 吧?

很遗憾,没有。

啥?

是的,没有。因为 YAML 不需要额外的一个方法,set 方法就可以删除键了。

YAML 中所有不存在的键,它的值都是 null

利用这个特点,要删除键,我们只需要把它的值设为 null 就可以了,保存时 Bukkit 会忽略为 null 的键。


哇!这一节真长!不过想想也是,与其说这是配置文件教程,不如说这是 Java 教程啦。

加油!我敢说,最难的部分已经过去了!