跳到主要内容
AZ Tools

Java .properties 解析器

.properties 看起来是最简单的配置格式,却也是手写解析器最容易搞错的格式。本页把 java.util.Properties.load 真正使用的流程(JDK 的 LineReader,然后是 load0 和 loadConvert)跑在你粘贴或拖入的文本上,逐个键显示解析结果,并给出 JSON 视图和 Properties.store 会写回的文件。 分隔符是 =、: 或一段空白,所以 "c three" 是键 c、值 three,"key with spaces=x" 的键是 "key"、值是 "with spaces=x"。分隔符两侧的空格会消失,值尾部的空格不会,而且编辑器里看不出来。行尾的反斜杠会把下一行接上来并去掉其缩进,但偶数个反斜杠根本不是续行,以反斜杠结尾的注释行也不会续行。# 和 ! 只有作为一行的第一个非空白字符时才是注释,因此 URL 片段里的 # 会留在值里。转义只有 \t \n \r \f 和 \uXXXX,其余反斜杠都会被悄悄丢掉。 这个格式在现实中最大的坑是编码。几乎所有框架调用的 load(InputStream) 都按 ISO-8859-1 解码字节,因此以 UTF-8 保存的文件会把一个带重音的字符变成两三个。当输入含有非 ASCII 字符时,本页会同时给出两种读法并指出哪些键会变,方便你在上线前决定用 \uXXXX 转义还是用指定字符集的 load(Reader)。 全部处理都在浏览器内完成,不会上传任何内容。它无法告诉你重复键中哪一个才是你想要的,也无法判断看起来奇怪的值是否真的写错了:它报告的是 JDK 对这些字节会做什么,而不是你本来想写什么。

.properties 文件内容

唯一键

11

赋值次数

12

注释行

1

续行条目

1

提示

6

字节读法:

几乎所有框架默认使用的 load(InputStream) 始终按 ISO-8859-1 读取。切换两种读法即可看出哪些键会变。

解析器发现的问题
  • 非 ASCII 字符会乱码 · 行 10

    load(InputStream) 按 ISO-8859-1 解码字节,因此以 UTF-8 保存的文件会把一个字符变成两三个拉丁字符。请写成 \uXXXX,或用指定字符集的 Reader 读取。

    • greeting: "Grüße" → "Grüße"
  • 空白作为分隔符 · 行 4

    这一行没有 = 也没有 :,第一段空格或制表符把键和值分开,所以 "c three" 就是 c = three。

    app.mode

  • 值尾部空格被保留 · 行 9

    分隔符两侧的空格会被丢弃,但值尾部的空格属于值本身。编辑器看不到,也不会报错。

    app.token (+3)

  • 键里含有分隔符 · 行 6

    这个键里有被转义的 =、: 或空格,所以整行只是一个键。用第一个 "=" 切分的代码会切错位置。

    menu.label 1

  • 这不是注释 · 行 14

    # 和 ! 只有作为一行的第一个非空白字符时才开启注释。这里它在值的后面,因此留在值里。

    note

  • 重复的键 · 行 2, 12

    同一个键被赋值多次。load 只保留最后一个值且不做任何提示,前面几行是看起来还在生效的死配置。

    app.name → "orders-api-v2"

分隔符备注
1# Spring Boot style configuration — and every trap in the format注释
2app.nameorders-api=被覆盖
3server.port8080:
4app.modeproduction空白空白作为分隔符
5jdbc.urljdbc:postgresql://db:5432/orders?ssl=true=
6menu.label 1Save as…=键里含有分隔符
7–8welcome.messageHello and welcome=
9app.tokenabc123···=值尾部空格被保留
10greetingGrüße=
11greeting.escapedGrüße=
12app.nameorders-api-v2=
13pathC:\\Users\\dev\\app=
14notevalue # this is not a comment=这不是注释

表格会把控制字符显示为 \n、\t,并用 · 标出值尾部的空格;JSON 视图则是原样值。

使用方法

  1. 把文件内容粘贴到输入框,或把 .properties 文件拖到虚线区域:文件只在浏览器里读取,不会上传。
  2. 看逐行表格:每个条目来自第几行、实际使用的分隔符是什么,以及该行有什么不寻常之处的标记。
  3. 逐条处理上方的提示。重复键、值尾部空格、吞掉下一条目的续行,这三项最容易在生产环境悄悄改变行为。
  4. 如果文件包含非 ASCII 字符,在 UTF-8 与 ISO-8859-1 两种读法之间切换,看清 load(InputStream) 会交给应用什么内容。
  5. 需要做配置对比就复制 JSON 视图;需要写回文件就复制 Properties.store 视图,其中分隔符和非 ASCII 字符都已转义。

常见问题

"c three" 里没有等号,为什么被拆成键和值?
因为在这个格式里空白也是分隔符。Properties.load 会寻找第一个未转义的 =、: 或空白字符,谁先出现就用谁,并把它前面的部分当作键。所以 "c three" 是键 c、值 three;"key with spaces=x" 的键是 "key"、值是 "with spaces=x",等号根本没有机会充当分隔符。如果键里确实要有空格,必须写成 "\ ",Properties.store 写回这种键时用的正是这种写法。
我的中文或重音字符变成了乱码,发生了什么?
java.util.Properties.load(InputStream) 的规范就是把流按 ISO-8859-1 解码,一个字节一个字符。以 UTF-8 保存的文件里一个汉字占三个字节,这三个字节就变成三个拉丁字符,也就是典型的乱码。整个过程不会抛异常,所以坏掉的字符串会一路传到界面上。有三种修法:把非 ASCII 写成 \uXXXX(native2ascii 和 Properties.store 的做法)、用 UTF-8 的 InputStreamReader 调用 load(Reader),或者改用默认 UTF-8 的 loadFromXML。
值尾部的空格真的会保留吗?
会,而且它制造了一整类 bug 报告。行首的空白、分隔符两侧的空白、续行的缩进都会被丢掉,但值尾部的空白会原样保留,因为行到此结束,没有人再去修剪它。一个多了空格的密码或 URL 能正常解析、只是连不上,而且在编辑器和代码评审里跟正确版本看起来一模一样。本页的表格会把这些字符显示出来。
什么样的行会续到下一行?
以奇数个反斜杠结尾时。最后一个反斜杠被去掉,下一行接上来,并去掉那一行的前导空白,所以缩进的续行读起来很自然。偶数个则不是续行:它们是字面反斜杠,数量减半,条目到此结束。由此产生两个意外。续行可能吞掉看起来像下一个条目的行:如果某个值末尾多了一个反斜杠,下面那行 "key=value" 就成了这个值的一部分,而那个键根本不存在。另外,以反斜杠结尾的注释行完全不会续行,因为注释是整行丢弃的。
两行设置了同一个键,哪一个生效?
后一行,而且悄无声息。Properties 是 Hashtable,load 只是一边扫描一边为每个条目调用 put,所以后面的行会覆盖前面的行,没有警告、没有错误,也不留任何记录。当一个文件由多个片段拼装而成,或者同一个键一次用 =、一次用 : 定义时,很容易在无意中造成这种情况。本页会列出每个重复的键以及设置它的所有行号,并把落败的行变淡。
它和 .env 文件是同一种格式吗?
不是,把两者当成一回事是静默故障的常见来源。.env 没有空白分隔符,用引号保护值和容纳多行字符串,把值后面的 # 当作行内注释,而且常常支持 ${VAR} 展开。.properties 一样都没有:引号只是会进入值里的普通字符,值后面的 # 是值的一部分,格式本身没有变量展开(那是 Spring 另外加的),续行靠反斜杠而不是引号。转义也不同,.properties 只有 \t \n \r \f 和 \uXXXX。

相关工具