简介
- 不管是移动应用,桌面程序还是后台服务,经常需要从配置文件中读取配置信息,进行程序初始化和改变运行时的状态。以什么样的格式来存储配置信息,这是开发人员需要面临的一个文件。
- 常用的配置文件格式主要用:
- 键值对
- JSON
- XML
- YAML
- TOML
键值对
- 键值对是一个非常简单易用的配置文件格式。每一个键值对表示一项配置,键值对的分隔符一般使用等号或者冒号。解析时,可以将#号开始的行视为注释行,以达到注释的功能
- 以键值对为表现形式的配置文件格式常见的用Windows .ini文件和Java中的.properties文件
- 例如下面是一个使用键值对表示的后台服务配置。
1
2
3
4
5
6
7
8
9# This is a comment
name=UserProfileServer
maxconns=1000
queuecap=10000
queuetimeout=300
loglevel=ERROR
logsize=10M
lognum=10
logpath=/usr/local/app/log - 在解析上面的配置时,可以按行读取,然后放到 map 中。下面以 Go 为例,完成对上面配置文件的解析。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51package main
import(
"bufio"
"io"
"os"
"fmt"
"errors"
"strings"
)
func ParseConf(confPath string) (map[string]string, error) {
if confPath == ""{
return nil, errors.New("param is ill")
}
f, err := os.Open(confPath)
if err != nil {
return nil, err
}
defer f.Close()
//store config info
m := make(map[string]string)
bfRd := bufio.NewReader(f)
//read by line, the line terminator is '\n'
for{
line, err := bfRd.ReadString('\n')
if err == nil || err == io.EOF{
//ignore blank and comment line
if strings.TrimSpace(line) != "" && strings.TrimSpace(line)[0] != '#'{
vKV := strings.Split(line, "=")
if len(vKV) == 2 {
m[vKV[0]] = vKV[1]
}
}
if err == io.EOF{
return m, nil
}
} else {
return nil, err
}
}
return m, nil
}
func main(){
mConf, _ := ParseConf("server.kv")
fmt.Println(mConf)
} - 运行结果:
1
2
3
4
5
6
7
8
9map[loglevel:ERROR
lognum:10
logpath:/usr/local/app/log
logsize:10M
maxconns:1000
name:UserProfileServer
queuecap:10000
queuetimeout:300
]
JSON
JSON(JavaScript Object Notion),是轻量级的文本数据交换格式,独立于语言,具有自我描述性。JSON类似于XML,但是比XML更小,更快,更易解析
JSON语法是JavaScript对象表示法语法的子集:
- 数据在名称/值对中
- 数据由逗号分隔
- 花括号保存对象
- 方括号保存数组
名称/值包括字段名称(在双引号中),后面写一个冒号,然后是值
1
"firstName": "John"
一个合法的JSON可以是:
- 数字(整数或者浮点数)
- 字符串(在双引号中)
- 布尔(true或者 false)
- 数组(在方括号中)
- 对象(在花括号中)
- null
下面以 JSON 表示一个简单的后台服务配置:
1
2
3
4
5
6
7
8
9
10
11
12{
"-name": "UserProfileServer",
"maxconns": "1000",
"queuecap": "10000",
"queuetimeout": "300",
"loginfo": {
"loglevel": "ERROR",
"logsize": "10M",
"lognum": "10",
"logpath": "/usr/local/app/log"
}
}其中 -name 表示服务的名称,前面一个横杠表示该值可以转换为 XML 的标签属性。其它的键值对表示服务的各个配置项
XML
XML(Extensible Markup Language)是可扩展标记语言,用来传输和存储数据。因为其允许用户自定义标记名称,具有自我描述性,可灵活地用于存储服务配置信息
XML文档结构是一种树结构,它从根部开始,然后扩展到枝叶。XML文档必须有一个唯一的根节点,根节点包含所有其他节点。所有节点均可拥有文本内容和属性(名称/值的对)。XML节点也叫做XML元素
YAML
YAML(YAML Ain’t a Markup Language)是专门用来写配置文件的语言,简洁强大,相比于JSON和XML,更加便于开发人员读写。
YAML配置文件后缀为.yml或者.yaml
YAML 的基本语法规则如下:
- 数据结构采用键值对的形式 key: value。
- 键冒号后面要加空格(一般为 1 个空格)。
- 字母大小写敏感。
- 使用缩进表示层级关系。
- 缩进只允许使用空格,不允许使用 Tab 键。
- 缩进空格数可以任意,只要相同层级的元素左侧对齐即可。
- 字符串值一般不使用引号,必要时可使用。使用双引号表示字符串时,会转义字符串中的特殊字符(例如\n)。使用单引号时不会转义字符串中的特殊字符。
- 数组中的每个元素单独一行,并以 - 开头。或使用方括号,元素用逗号隔开。注意短横杆和逗号后面都要有空格。
- 对象中的每个成员单独一行,使用键值对形式。或者使用大括号并用逗号分开。
- 文档以三个连字符—表示开始,以三个点号…表示结束,二者都是可选的。
- 文档前面可能会有指令,在这种情况下,需要使用—来表示指令的结束。指令是一个%后跟一个标识符和一些形参。
- 目前只有两个指令:%YAML指定文档的 YAML 版本,%TAG用于 tag 简写。二者都很少使用。
- #表示注释,从这个字符一直到行尾,都会被解析器忽略。
YAML支持的数据结构有三种:
- 对象:键值对的集合,又称为映射(mapping),散列(hashes),字典(dictionary)
- 数组:一组按次序排列的值,又称为序列(sequence),列表(list)
- 标量:单个不可再分的值
对象
- 对象的一组键值对,使用冒号结构表示
1
name: Steve
- YAML也允许另一种写法,将所有键值对写成一个行内对象
1
who: {name: Steve, age: 18}
- 当然,如果对象元素太多一行放不下,那么可以换行
1
2
3who:
name: Steve
age: 18
- 对象的一组键值对,使用冒号结构表示
数组
- 一组以连字符开头的行,构成一个数组。注意,连字符后需要添加空格
1
2
3
4animals:
- Cat
- Dog
- Goldfish - 连字符前可以没有缩进,也就是说下面的这种写法也是OK的,但是还是建议缩进,因为更加易读
1
2
3
4animals:
- Cat
- Dog
- Goldfish - 数组也可以采用行内表示法
1
animal: [Cat, Dog, Goldfish]
- 如果数组元素是一个数组,则可以在连字符下面再缩进输入一个数组
1
2
3
4
5
6
7animals:
-
- Cat
- Dog
-
- Fish
- Goldfish - 如果是行内表示,则为:
1
animals: [[Cat, Dog], [Fish, Goldfish]]
- 如果数组元素是一个对象,可以写作:
1
2
3
4
5
6animals:
- species: dog
name: foo
- species: cat
name: bar
- 对应的 JSON 为:
1
2
3
4
5
6
7
8
9
10
11
12{
"animals": [
{
"species": "dog",
"name": "foo"
},
{
"species": "cat",
"name": "bar"
}
]
} - 对象和数组可以结合使用,形成复合结构。
1
2
3
4
5
6
7
8
9languages:
- Ruby
- Perl
- Python
websites:
YAML: yaml.org
Ruby: ruby-lang.org
Python: python.org
Perl: use.perl.org - 对应的 JSON 表示如下:
1
2
3
4
5
6
7
8
9
10
11
12
13{
"languages": [
"Ruby",
"Perl",
"Python"
],
"websites": {
"YAML": "yaml.org",
"Ruby": "ruby-lang.org",
"Python": "python.org",
"Perl": "use.perl.org"
}
}
- 一组以连字符开头的行,构成一个数组。注意,连字符后需要添加空格
标量是最基本、不可再分的值。有以下 7 种:
- 字符串
- 布尔值
- 整数
- 浮点数
- Null
- 时间
- 日期
使用一个例子来快速了解标量的基本使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21boolean:
- TRUE # true、True 都可以
- FALSE # false、False 都可以
float:
- 3.14 # 数值直接以字面量的形式表示
- 6.8523015e+5 # 可以使用科学计数法
int:
- 123 # 数值直接以字面量的形式表示
- 0b1010_0111_0100_1010_1110 # 二进制表示
null:
nodeName: 'node'
parent: ~ # 使用~表示 null
string:
- hello # 字符串默认不使用引号
- "Hello world" # 使用双引号或单引号包裹含有空格或特殊字符(如冒号)的字符串
- newline
newline1 # 字符串可以拆成多行,每一换行符会被转化成一个空格
date:
- 2018-02-17 # 日期必须使用 ISO 8601 格式,即 yyyy-MM-dd
datetime:
- 2018-02-17T15:02:31+08:00 # 时间使用 ISO 8601 格式,时间和日期之间使用 T 连接,+08:00 表示时区YAML 字符串有三种表示方式:
- 无引号
- 单引号
- 双引号
字符串默认不需要引号,但是如果字符串包含空格或特殊字符(如冒号),需要加引号。
双引号字符串允许在字符串中使用转义序列来表示特殊字符,例如 \n 表示换行,\t 表示制表符,以及 " 表示双引号。
单引号字符串被视为纯粹的字面字符串,不支持转义序列。
如果字符串含有单引号,可以使用双引号包裹,反之亦然
锚点 & 和别名 *,可以用来完成引用
1
2
3
4
5
6
7defaults:
adapter: postgres
host: localhost
development:
database: myapp_development
<<:等同于下面的配置。
1
2
3
4
5
6
7
8defaults:
adapter: postgres
host: localhost
development:
database: myapp_development
adapter: postgres
host: localhost& 用来建立锚点(defaults),<< 表示合并到当前数据,* 用来引用锚点。
如果想引入多行的文本块,可以使用 |,|+,|-,>,>+,>-
当内容换行时,保留换行符
如果最后一行有多个换行符,只保留一个换行符
1
2
3
4
5
6
7
8
9
10
11linefeed1: |
some
text
linefeed2: |
some
text
linefeed3: |
some
text对应的 JSON 为:
1
2
3
4
5{
"linefeed1": "some\ntext\n",
"linefeed2": "some\ntext\n",
"linefeed3": "some\n\ntext\n"
}当内容换行时,保留换行符。
与 | 的区别是,如果最后一行有多个换行符,则保留实际数目。
1
2
3
4
5{
"linefeed1": "some\ntext\n",
"linefeed2": "some\ntext\n\n",
"linefeed3": "some\n\ntext\n"
}当内容换行时,保留换行符,但最后的换行符不保留
1
2
3
4
5
6
7
8
9
10
11linefeed1: |-
some
text
linefeed2: |-
some
text
linefeed3: |-
some
text对应的 JSON 为:
1
2
3
4
5{
"linefeed1": "some\ntext",
"linefeed2": "some\ntext",
"linefeed3": "some\n\ntext"
}当内容换行时,替换为空格,但保留最后一行的换行符
如果最后一行有多个换行符,只保留一个换行符
1
2
3
4
5
6
7
8
9
10
11linefeed1: >
some
text
linefeed2: >
some
text
linefeed3: >
some
text对应的 JSON 为:
1
2
3
4
5{
"linefeed1": "some text\n",
"linefeed2": "some text\n",
"linefeed3": "some\ntext\n"
}当内容换行时,替换为空格,但保留最后一行的换行符。
与 > 的区别是,如果最后一行有多个换行符,则保留实际数目。
1
2
3
4
5
6
7
8
9
10
11linefeed1: >+
some
text
linefeed2: >+
some
text
linefeed3: >+
some
text对应的 JSON 为
1
2
3
4
5{
"linefeed1": "some text\n",
"linefeed2": "some text\n\n",
"linefeed3": "some\ntext\n"
}-(缺省行为)
当内容换行时,替换为空格,不保留最后一行的换行符。
1
2
3
4
5
6
7
8
9
10
11linefeed1: >-
some
text
linefeed2: >-
some
text
linefeed3: >-
some
text对应的 JSON 为:
1
2
3
4
5{
"linefeed1": "some text",
"linefeed2": "some text",
"linefeed3": "some\ntext"
}注意:以上 6 个特殊字符,|- 和 >- 用得最多
有时我们需要显示指定某些值的类型,可以使用 !(感叹号)显式指定类型。! 单叹号通常是自定义类型,!! 双叹号是内置类型
1
2
3
4# !!str 指定为字符串
string.value: !!str HelloWorld!
# !!timestamp 指定为日期时间类型
datetime.value: !!timestamp 2021-04-13T02:31:00+08:00内置的类型如下:
1
2
3
4
5
6
7
8
9
10
11!!int:整数类型
!!float:浮点类型
!!bool:布尔类型
!!str:字符串类型
!!binary:二进制类型
!!timestamp:日期时间类型
!!null:空值
!!set:集合类型
!!omap,!!pairs:键值列表或对象列表
!!seq:序列
!!map:散列表类型一个 yaml 文件可以包含多个 yaml 文档,使用三个连字符—分隔
1
2
3
4
5
6
7
8
9
10a: 10
b:
- 1
- 2
- 3
a: 20
b:
- 4
- 5这种情况在 K8S 和 SpringBoot 中非常常见。
比如 SpringBoot 在一个 application.yml 文件中,通过 — 分隔多个不同配置,根据 spring.profiles.active 的值来决定启用哪个配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# 公共配置
spring:
profiles:
active: prod #使用名为 prod 的配置,这里可以切换成 dev。
datasource:
url: jdbc:mysql://localhost:3306/test_db?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
password: 123456
username: root
# 开发环境配置
spring:
profiles: dev #profiles属性代表配置的名称。
server:
port: 8080
# 生产环境配置
spring:
profiles: prod
server:
port: 80下面以 YAML 表示一个简单的后台服务配置:
1
2
3
4
5
6
7
8
9name: UserProfileServer
maxconns: 1000
queuecap: 10000
queuetimeout: 300
loginfo:
loglevel: ERROR
logsize: 10M
lognum: 10
logpath: /usr/local/app/log
TOML
GitHub 联合创始人 Tom Preston-Werner 觉得 YAML 不够简洁优雅,如缩进要严格对齐,因此和其他几位开发者一起捣鼓了一个 TOML(Tom’s Obvious Minimal Language)。TOML 旨在成为一个语义显著且易于阅读的极简配置文件格式,能够无歧义地转化为哈希表,且能够简单地解析成形形色色语言中的数据结构,用于取代 YAML 和 JSON
TOML 的基本语法规则如下:
- TOML 是大小写敏感的
- TOML 文件必须是合法的 UTF-8 编码的 Unicode 文档
- 空白的意思是 Tab(0x09)或空格(0x20)
- 换行的意思是 LF(0x0A)或 CRLF(0x0D0A)
- 井号将此行剩下的部分标记为注释
配置文件格式的选择
面对常见配置文件格式,使用时该如何选择呢?这里给几个选择的原则:
- 支持嵌套结构。仅仅支持 KV 结构的键值对表达能力有点弱;
- 支持注释。不支持注释的 JSON 是给机器读的,不是给人读的;
- 支持不同的数据类型,而不仅仅是 string。这一点,键值对和 XML 表现的非常逊色;
- 最好支持 include 其他配置文件,方便配置模块化。复杂的配置也是无奈之举,但如果支持 include 语法,可以方便的把配置文件模块化。
通过以上几个对配置文件的要求,发现键值对不支持层级关系,JSON 不支持注释,可读性较差,虽然 XML 支持注释和层级结构,且可读性较好,但是因为起始标签一定要有个与之对应的结束标签,文件内容较大,解析时占用较多内存,传输时占用较多带宽。所以这里推荐使用 YAML 和 TOML,很多语言都有其 library 实现,跨语言不成问题