一个项目刚开始,配置少,随手扔个 config.json 或者 application.properties 就够。没人纠结格式------能跑就行。
后来项目长大了。配置要分环境------开发一套,测试一套,生产一套。要嵌套------一个数据库连接下面有连接池,连接池下面有超时。要注释------为什么这个超时是 30 秒不是 60 秒,得写下来,不然下个人不知道。要校验------配错了端口号,服务启动不起来,最好在启动的时候就报错,别等到请求进来了才发现。
每个需求冒出来,你才发现当初选的格式少了一样东西。JSON 不能写注释。Properties 没法嵌套。YAML 缩进一错就静默变行为。ENV 超过十二个变量就开始失控。换格式?换一个要动所有配置------代码里所有地方都要跟着改。不是选的时候纠结,是换的时候代价太大。
选配置文件格式,不是选哪个最好,是选的时候先想清楚:你将来会卡在哪一层。
INI / Properties:最老的,也是最平的
Windows 3.1 时代就有 INI 了。[section] 加 key=value,比 properties 还早十几年。Java 的 .properties 继承了这个传统------一行一条,没有层级,没有类型。
server.host=0.0.0.0
server.port=8080
好处明显。人看一眼就懂,工具随便解析,几十行配置不费脑子。
但它的暗面在"点"里。server.host 和 server.port,你看过去知道它们都属于 server,解析器不知道。解析器看到的是三个平铺的键,谁跟谁一组靠的是命名约定------点不是层级,是假装有层级。当配置真的需要层级------一个数组、一个嵌套对象------properties 就装不了。
最典型的翻车:用 properties 写一个服务列表。
services.0.name=order
services.0.url=http://order:8080
services.1.name=user
services.1.url=http://user:8081
能跑。但删掉第 0 个之后,数字要重排;加一个中间项,后面的全要改下标;写的人手动数 0,1,2,漏一个就丢一个服务。把数组硬塞进平面结构,格式能装,但写的人要替格式扛它不该扛的复杂度。
用 properties 的前提是你的配置天然就是平的------键值对就够,嵌套不存在,列表不存在。一旦需要结构,不要硬撑,换 JSON。
ENV:十二个环境变量,一个容器
.env 和 properties 看起来一样------KEY=VALUE。但底层逻辑不一样:properties 是文件被应用读,env 是变量被运行时注入。一个是你主动加载的配置源,一个是操作系统或容器编排塞进来的环境参数。
这个区别决定了 .env 的战场:配置和代码物理分离。代码一个仓库,环境变量在 K8s ConfigMap 里、在 CI 变量里------代码碰不到。改环境不用改代码。
但 .env 的暗面在数量。十二个以内,没毛病。十二个以后,你再往里塞就是自己坑自己。
最典型的翻车:把数据库连接池配置全塞进 .env。
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp
DB_USER=admin
DB_PASSWORD=secret
DB_POOL_MIN=5
DB_POOL_MAX=20
DB_POOL_TIMEOUT=30000
DB_SSL=true
第九行开始出问题。DB_POOL_TIMEOUT=30000 是毫秒还是秒?写的人知道,读的人要翻文档。DB_SSL=true 是字符串 "true" 还是布尔值?环境变量永远是字符串,类型是你自己的代码强加上去的。没有注释,没有分组,没有类型------.env 不是"配置格式",是"配置的入口"。
**.env 的正确用法:十二个变量以下,一层结构,没有复杂类型。**超过这个量级,把你的复杂配置挪到 YAML 或 JSON 里,.env 只留一个 CONFIG_FILE_PATH 指向那个文件。
JSON:机器读得最舒服,人写得最难受
JSON 是 Web 时代的通用货币。REST API、前端构建配置、后端配置------几乎都选 JSON。为什么?生态:任何语言都自带 JSON 解析器,不用额外装库。JavaScript 更爽------import config from './config.json',直接拿到对象,零解析。
而且 JSON 天生表达结构:{}、[]、字符串、数字、布尔值、null------六种类型,简单,但够用。properties 的"假装层级"在 JSON 里是真的:
json
{
"server": {
"host": "0.0.0.0",
"port": 8080
},
"services": ["order", "user", "payment"]
}
数组就是数组,对象就是对象,不用数下标。
但 JSON 有两个硬伤。
第一个:不能写注释。规范不允许,不是某个实现不支持。你只能在键名里塞:
json
{
"_comment": "缓存过期时间,单位秒,改这个要同步改网关的 timeout",
"cache_ttl": 3600
}
下一个接手的人看到 _comment 第一反应是"这个字段是干嘛的",查完才明白是注释------注释的存在就是给后来的人看的,JSON 把这条通道关上了。
第二个:没有 schema。port 可以是 8080 也可以是 "8080",解析器不做类型检查。生产环境里 JSON 配错了字段名、配错了类型,服务静默启动------不是崩了,是用了默认值,行为全变了。改字段名忘了改代码里对应的 key,老代码还在读旧字段,新字段没赋值默认 null 往下走------数据没丢,逻辑全错,排查了一个下午。
**JSON 在"不出错"这件事上做得太成功了------它不出错,所以错才藏得深。**人和机器之间,JSON 选了机器。
YAML:缩进就是结构,空格就是逻辑
YAML 是冲着 JSON 的两个痛点来的:人要能读,人要能写注释。它的设计哲学是"人类可读第一",靠缩进表达层级------两个空格一层,四个空格更深一层。JSON 的 {} 和 [] 变成空格。
yaml
server:
host: "0.0.0.0"
port: 8080
services:
- order
- user
- payment
同样的配置,JSON 十六行括号,YAML 省一大半。加注释就加一行 #,不用装 _comment。而且 YAML 有一项 JSON 想都不敢想的:锚点引用。两个服务共享同一份数据库配置,写一次 &db_config 锚定,别处 *db_config 引用------改一处两边同步。JSON 里你只能复制粘贴,然后祈祷改的时候两边都记得。
但 YAML 的暗面在"空格"里。JSON 的括号写错了 IDE 立刻红线。YAML 的缩进写错了,没有红线,没有报错,只有行为变了。
最典型的翻车:一个缩进错了一层。
yaml
server:
host: "0.0.0.0"
port: 8080
timeout: 30
你一眼看过去,timeout 在 server 下面。其实不在------少缩了两个空格,timeout 在顶层。服务启动找不到 server.timeout,你盯着缩进看了十分钟。每个写过 K8s YAML 的人都有这个经历------十几个服务,每个几十行,缩进错了就这个代价。
更隐蔽的:YAML 的类型推断。你写 port: 8080,YAML 推断它是整数。你写 port: "8080",它才是字符串。大多数时候没问题。但有些值是犄意的------某国邮编 02601,不加引号 YAML 当成八进制(以 0 开头的数字),解析出来 1409,不是 2601。类型不是写错了,是 YAML 替你猜错了。
**YAML 把"格式正确"买成了"逻辑正确",买贵了。**JSON 的括号不会错,YAML 的缩进会错。它用可读性换来了一个自己会出错的解析器。
TOML:堵 YAML 的缩进,接 JSON 的注释
TOML 是 2013 年由 GitHub 创始人 Tom Preston-Werner 推出来的。设计思路很简单:取 YAML 的简洁和注释,堵 YAML 的缩进地狱。取 JSON 的类型明确,堵 JSON 不能写注释。用 [section] 代替空格,用 key = value 当核心语法。
toml
[server]
host = "0.0.0.0"
port = 8080
[services]
names = ["order", "user", "payment"]
[server] 是标题,标题下的键都属于这个组。缩进不再是逻辑------你缩不缩、缩几个空格,TOML 不管,它只管 [ ] 在哪。一个空格错了,YAML 静默变行为;TOML 里你多打几个空格,解析器理都不理。
而且 TOML 的类型是显式的:8080 是整数,"8080" 是字符串,true 是布尔,2024-01-01 是日期------不猜,不推断。JSON 的 "8080" 到底是字符串还是数字全看你的解析器;YAML 的 02611 是字符串还是八进制全看推断规则;TOML 不猜,你写什么类型它就是什么。
但 TOML 的暗面在生态。JSON 是任何语言都自带,YAML 是 DevOps 的通用语,TOML 是 Rust 和 Python 的新宠------出了这两个圈子,工具链不一定认。
最典型的翻车:你兴冲冲把配置全换成 TOML。写得舒服,类型安全,注释工整。然后 CI 平台说"配置文件只支持 JSON 和 YAML"。配置中心不支持 TOML。运维问"这什么格式,能转回 YAML 吗"。选了 TOML 不是因为 TOML 不好,是生态还没跟上------格式选对了,场景选错了。
**选了 TOML,你自己舒服;但要保证你上下游所有工具都认得它。**不认得,你的舒服是别人的麻烦。
XML:最强的验证,最重的代价
XML 是 90 年代末的产物,比所有这些都早。但它走的不是"人怎么好读"的路线,是"机器怎么好验证"的路线。
XML 有 schema。XSD 能定义每个字段的类型、是否必填、取值范围,用 xmllint 校验------不符合就拒绝,不启动。JSON 配错字段名,服务静默用默认值;XML 配错字段名,直接拒绝启动,日志里告诉你哪个字段错了。
而且 XML 有命名空间:<person:name> 和 <company:name> 是两回事------不靠命名约定,靠结构区分。JSON 里 name 只有一个含义,要区分只能靠键名。
但 XML 的暗面在重量。十个配置,两百行。
xml
<server>
<host>0.0.0.0</host>
<port>8080</port>
</server>
同样的配置,properties 两行,JSON 六行,YAML 三行------XML 八行。每个标签打开再关上,标签名比配置值还长。这是"结构税"------你花在标签上的时间比你花在配置值上的还多。
最典型的翻车:一个团队选了 XML,因为要 schema 验证、要 IDE 提示------理由都站得住。写了三个月,配置文件撑到上千行,加一个配置要在三个地方改(XSD schema、XML 配置、Java 解析代码)。改完才意识到:三个月里 schema 拦住的错误,一次都没有。schema 拦住了零个错,标签写了上千行。
**XML 用最强的验证买了最重的语法。**大部分项目不需要这个级别的验证------你只需要十个配置,不是两百行 XML。
怎么选
每种配置文件格式,都不"最好"。每个都带着一个它最擅长的战场,和一个它一进去就翻车的坑。选格式不是问哪个最先进,是问你最怕什么。
问自己四句话:
- 配置有没有深层嵌套? 没有------.env 或 properties 就够了。有------JSON 或 YAML。
- 配置要不要注释? 要------JSON 出局。不要,机器之间的中间格式------JSON 可以。
- 配置是人写还是机器生成? 人写------YAML 或 TOML,人要读得舒服。机器生成------JSON,编译器吐出来的,注释用不上。
- 工具链认什么? CI 平台、配置中心、运维工具------它们只认 JSON/YAML,你选了别的就是自己的舒服换别人的麻烦。这一条是最终裁决。
卡在生态兼容,退回去用 JSON------不是 JSON 最好,是生态替你选了。卡在缩进怕了,退到 TOML------YAML 的缩进地狱是真实的,不用硬扛。卡在啰嗦,别碰 XML------除非你真的要 schema 验证,否则十个配置不值得两百行。
每一个格式都在一个维度上解决了一个问题,也在同一个维度上埋了一个新坑。选择的过程不是比谁更好,是搞清楚你的痛点落在哪个格式的坑里------避开了,就用它。