欢迎回来!希望你的「Hello World」项目运行顺利。
现在我们的插件还只能读取我们预先设定好的数据,如果服主想要输出「Hello Ugly World」呢?如果每次都要修改代码重新编译,未免太麻烦了吧!
于是,配置文件出现了。
配置文件(Config),顾名思义,是一种「设置」,里面记录了许多内容。
配置文件由服主修改,插件读取,这样就不必修改插件的代码了。
配置文件都是 YAML 格式,类似于这样:
key: value
key2: value2
key3:
key3-1: 2333
我们说过 YAML 就像字典,那么字典中的一项是不是可以指向另一个字典啊?key3-1
就是一个例子。它是嵌套的一个字典,这叫做子键。子键下面还可以有子键的子键……就是这样。
我们注意到 key3-1
的前面有两个空格,这叫缩进(Indent),如果没有这个缩进,YAML 就不知道这是 key3
的一个子键还是一个独立的键了。因此缩进不能去掉。
按照规范,YAML 中每进入下一级,就要使用两个空格进行缩进。如果是子键的子键,就要空 4 格,子键的子键的子键就空 6 格,以此类推。
在 YAML 的「字典」中,冒号右边并不是什么都可以放的。实际上,YAML 中能放的东西很有限:
- 一个数字
- 一段文本
- 其它的字典
- 列表
- 一个逻辑值(
true
或false
,真或假)
由于数字也可以被认为是文本,因此 YAML 采用特殊优先原则,也就是,先看看能不能认为它是个数字,如果能,就认为是数字,否则认为是文本。
number: 123456789
text-no-quote: 这是文本
text-quote: "123456789"
bool: true
list:
- "一个文本"
- 12345
- true
- 其它的文本
有关 YAML 的详细语法,可参见 菜鸟教程上有关 YAML 的内容。
采用 YAML 并非因为它的效率或者可读性,而是为了简单的配置方式,正因如此,YAML 适合人类修改,机器读取。
实际上易于修改是一个谎言,YAML 采用严格的缩进结构,据不完全统计,超过 98% 的 YAML 配置文件错误都是缩进不当导致的。
Bukkit 为每个插件提供了一个默认配置文件,要使用它,只需要在 src
下创建 config.yml
,并在代码中写上:
saveDefaultConfig();
这个方法会帮我们保存默认的配置文件,也就是 config.yml
。
Bukkit 很聪明,这个方法会自己查看,如果有了 config.yml
就不进行操作,如果没有就复制一份。
关于读取,可以通过调用 getConfig
方法获得这个文件的对象(面向对象!)并将它保存在内存的某个地方:
FileConfiguration config = getConfig();
这里出现了新的 Java 语法。
这条语句是赋值语句,用于将一个变量「设定」为一个对象。
使用变量有声明和赋值两步操作,不过它们可以一起用。
声明的语法:
类型 标识符;
赋值的语法:
标识符 = 右边的值;
可以看到,声明时需要指定类型,后面赋值和使用时就不需要了。
一个变量只能声明一次,但可以多次赋值(final
除外)。
你可以把变量认为是一个小盒子,里面可以装东西。上面这条语句相当于指示 Java 在内存里申请一个盒子用来存放数据。在创建盒子时,Java 需要知道里面要装什么东西,不然就可能出现放不下的问题。
在这里,由于 getConfig
返回的值是 FileConfiguration
类型,我们也需要使用对应的类进行「盛装」。那我是怎么知道这一点的呢?别急,我们在后面就会看到。
现在我们得到了 FileConfiguration
的一个实例,这个类是由 Bukkit 提供的,可以使用一些方法进行操作它。最常用的是 get
和 set
方法及其变种。
且慢,现在我是可以把使用方法告诉你,但将来如果你还需要知道其它类的方法,怎么办呢?另外,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 还可以搜索方法和变量呢!太聪明了~
那么我们现在就来试试吧!首先我们知道,FileConfiguration
类是我们需要找的类,那么我们搜索它……(注意大小写!)
呃……好多结果啊,但由于我们找的是类,因此我们要看「Types」。下面的「Members」表示各个类具有的方法和变量,我们不需要管。
类在 Java 中被称为 Class,但在英语中,「类型」一词的正确翻译是 Type,所以搜索结果中也使用了 Type 而非 Class。
FileConfigurationOptions
显然不是我们需要的类,那么我们忽略掉,就只剩下一个了,点击一下,打开之后像这样:
上面有两张图,我给你标出来了几个部分。
紫色区域是包名,这个你应该知道是什么吧?
蓝色区域是继承关系,可以告诉你当前的这个类继承了哪一个类,而那个类又继承了哪一个类等等。
棕色区域是类签名,那什么是签名呢?
签名(Signature)就是去掉 {}
之外的部分。
我们定义类时,是这么定义的:
public class SomeClass extends XXXXClass {
// ... 一堆东西
};
去掉后面的 {}
以及里面的东西,剩下的就叫签名。上面这个类的签名就是:
public class SomeClass extends XXXXClass
签名能够用于识别一个类。不过下面很快我们就要看到它的另一个重要用途。
现在我们接着往下看。「Field Summary」是什么东西啊?
「Field」是类的成员变量,也叫实例变量,比如,如果一条狗是一个对象,身高就是它的一个「Field」,如果一个插件是一个对象,版本可能就是它的一个「Field」。总之,成员变量就是属于一个对象的变量。访问成员变量和访问成员方法一样,都是使用点(.
),这个点同样表示「的」。
这里值得说明的一点是,变量本身也是一个对象,它也有自己的成员变量,所以可能会出现这样的情况:「我的邻居家的亲戚家养的狗的牙齿的长度」,这当然是没问题的。很简单?确实也没什么难的嘛……
那么「Field Summary」提供的信息就很明显了吧,它提供的是变量名而没有显示变量的类型,用你的鼠标单击变量名就能查看它的类型了。
不对!这里为什么还有「Fields inherited from」这样的东西?这是什么意思?
嗯,我们之前讲到过继承,「Inherite」就是继承的意思。图中的信息表示,MemoryConfiguration
具有两个叫做 defaults
和 options
的成员变量,FileConfiguration
继承了它,因此也具有了这两个成员变量。而 MemoryConfiguration
继承了 MemorySection
,MemorySection
具有一个名为 map
的成员变量,它将这个变量交给MemoryConfiguration
,MemoryConfiguration
再将它交给 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
会被省略掉以便查看。
现在我们终于可以回到正题上来了!我们来找找 set
和 get
方法……注意,我们要找的方法可能不只是叫 get
和 set
,它们的变种可能有类似的名字。
咦?怎么没有?
啊哈,找到了,它不在「Method Summary」中,而在「Methods inherited from interface org.bukkit.configuration.ConfigurationSection」中。
可以看到这里有非常多的 getXXX
版本用于取得不同类型的数据,而只有一个 set
方法,这一个 set
就可以对付所有情况了。点击上面的方法可以获得它的详细签名。
好啦!到这里,你就会使用 JavaDocs 了,以后,我们还会经常见到它的~
假设有这么一份配置文件:
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
也可能引发喜闻乐见的 NullPointerException
,getString
想到了这个情况,因此用同一个名字,创建了下面这个方法:
String getString(String path, String def)
第二个参数 def
指定一个默认值,当 getString
无法转换时将返回 def
指定的值,而不是 null
。
上面那个配置文件示例中还有数字,这要怎么获取呢?
整数在 Java 中的类型是 Integer
或者 int
,因此在 FileConfiguration
中可以通过以下方法获得:
int getInt(String path, int def)
同样,逻辑(true
和 false
)可以使用:
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
,你却能把 String
、int
和 boolean
给它们?为什么这里不需要 (Object) "123456"
这样的操作?
首先,你回答我一个问题:
String
是不是一种 Object
?
当然是啦!
那,String
既然是一种 Object
,为什么 Java 不能把它当作 Object
看待呢?
……可是,你说,上面那个 get
……哦!我明白了!因为 get
返回的是 Object
,Object
不一定是一种 String
,对不对?
是的。从子类转向父类没有风险,Java 会毫不犹豫地自动完成,而从父类转向子类有风险,需要我们亲自指示 Java 这样做。
写入之后还没完,我们只修改了内存中的一个副本,要将修改写入硬盘,还要保存:
saveConfig();
这个方法是 JavaPlugin
的一个方法,Bukkit 自动帮我们完成写入工作。记住:只有保存后,文件才会被真正写入硬盘!
读取时则不需要保存,因为读取没有修改那个副本。
如果要你删除一个键,已经学过 JavaDocs 的你会想到什么?
唔……有没有 delete
方法?你看,set
可以设置任何键,应该有这样的 delete
吧?
很遗憾,没有。
啥?
是的,没有。因为 YAML 不需要额外的一个方法,set
方法就可以删除键了。
YAML 中所有不存在的键,它的值都是 null
。
利用这个特点,要删除键,我们只需要把它的值设为 null
就可以了,保存时 Bukkit 会忽略为 null
的键。
哇!这一节真长!不过想想也是,与其说这是配置文件教程,不如说这是 Java 教程啦。
加油!我敢说,最难的部分已经过去了!