简介

  • 不管是移动应用,桌面程序还是后台服务,经常需要从配置文件中读取配置信息,进行程序初始化和改变运行时的状态。以什么样的格式来存储配置信息,这是开发人员需要面临的一个文件。
  • 常用的配置文件格式主要用:
    • 键值对
    • 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 为例,完成对上面配置文件的解析。 ```go package 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”) }

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
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
9
10
11
+ 运行结果:
```go
map[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
      3
      
      who:
      name: Steve
      age: 18
      
  • 数组
    • 一组以连字符开头的行,构成一个数组。注意,连字符后需要添加空格 ```yaml animals:
    • Cat
    • Dog
    • Goldfish ```
    • 连字符前可以没有缩进,也就是说下面的这种写法也是OK的,但是还是建议缩进,因为更加易读 ```yaml animals:
  • Cat
  • Dog
  • Goldfish ```
    • 数组也可以采用行内表示法
      1
      
      animal: [Cat, Dog, Goldfish]
      
    • 如果数组元素是一个数组,则可以在连字符下面再缩进输入一个数组 ```yaml animals: -
      • Cat
      • Dog

      • Fish
      • Goldfish ```
    • 如果是行内表示,则为:
      1
      
      animals: [[Cat, Dog], [Fish, Goldfish]]
      
    • 如果数组元素是一个对象,可以写作: ```yaml animals:
    • species: dog name: foo
    • species: cat name: bar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  + 对应的 JSON 为:
```json
{
    "animals": [
        {
            "species": "dog",
            "name": "foo"
        },
        {
            "species": "cat",
            "name": "bar"
        }
    ]
}
  • 对象和数组可以结合使用,形成复合结构。 ```yaml languages:
  • 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
    • 时间
    • 日期
  • 使用一个例子来快速了解标量的基本使用: ```yaml boolean:
    • 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 表示制表符,以及 " 表示双引号。
  • 单引号字符串被视为纯粹的字面字符串,不支持转义序列。
  • 如果字符串含有单引号,可以使用双引号包裹,反之亦然

  • 锚点 & 和别名 *,可以用来完成引用 ```yaml defaults: &defaults adapter: postgres host: localhost

development: database: myapp_development «: *defaults

1
2
3
4
5
6
7
8
9
10
+ 等同于下面的配置。
```yaml
defaults:
  adapter:  postgres
  host:     localhost

development:
  database: myapp_development
  adapter:  postgres
  host:     localhost
  • & 用来建立锚点(defaults),« 表示合并到当前数据,* 用来引用锚点。

  • 如果想引入多行的文本块,可以使用 +, -,>,>+,>-
  • 当内容换行时,保留换行符
  • 如果最后一行有多个换行符,只保留一个换行符 ```yaml linefeed1: | some text linefeed2: | some text

linefeed3: | some

text

1
2
3
4
5
6
7
+ 对应的 JSON 为:
```json
{
    "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"
    }
    
  • 当内容换行时,保留换行符,但最后的换行符不保留 ```yaml linefeed1: |- some text linefeed2: |- some text

linefeed3: |- some

text

1
2
3
4
5
6
7
+ 对应的 JSON 为:
```json
{
    "linefeed1": "some\ntext",
    "linefeed2": "some\ntext",
    "linefeed3": "some\n\ntext"
}
  • 当内容换行时,替换为空格,但保留最后一行的换行符
  • 如果最后一行有多个换行符,只保留一个换行符 ```yaml linefeed1: > some text linefeed2: > some text

linefeed3: > some

text

1
2
3
4
5
6
7
+ 对应的 JSON 为:
```json
{
    "linefeed1": "some text\n",
    "linefeed2": "some text\n",
    "linefeed3": "some\ntext\n"
}
  • 当内容换行时,替换为空格,但保留最后一行的换行符。
  • 与 > 的区别是,如果最后一行有多个换行符,则保留实际数目。 ```yaml linefeed1: >+ some text linefeed2: >+ some text

linefeed3: >+ some

text

1
2
3
4
5
6
7
+ 对应的 JSON 为
```json
{
    "linefeed1": "some text\n",
    "linefeed2": "some text\n\n",
    "linefeed3": "some\ntext\n"
}
  • -(缺省行为)

  • 当内容换行时,替换为空格,不保留最后一行的换行符。 ```yaml linefeed1: >- some text linefeed2: >- some text

linefeed3: >- some

text

1
2
3
4
5
6
7
+ 对应的 JSON 为:
```json
{
    "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 文档,使用三个连字符—分隔 ```yaml a: 10 b:
    • 1
    • 2
    • 3

      a: 20 b:

    • 4
    • 5 ```
  • 这种情况在 K8S 和 SpringBoot 中非常常见。
  • 比如 SpringBoot 在一个 application.yml 文件中,通过 — 分隔多个不同配置,根据 spring.profiles.active 的值来决定启用哪个配置。 ```yaml

    公共配置

    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

1
2
3
4
5
6
7
8
9
10
11
12

+ 下面以 YAML 表示一个简单的后台服务配置:
```yaml
name: 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 实现,跨语言不成问题