Minecraft(我的世界)中文论坛

标题: HOCON.md 的非官方简体中文翻译

作者: 土球球    时间: 2018-3-17 05:03
标题: HOCON.md 的非官方简体中文翻译
本帖最后由 ustc_zzzz 于 2018-3-17 05:09 编辑



前言

HOCON 是 Sponge 服务端中非常常见的一种配置格式,其很多高级用法也使其配置起来十分灵活,但是相应的 HOCON 规范,却只有官方的英文版本。因此,为方便国内服主配置服务端,我萌生了将其翻译到简体中文的想法,并与 3TUSK( @u.s.knowledge )共同完成了 HOCON 规范的翻译。

原文和本翻译均使用 Apache 2.0 协议发布。

以下是翻译状态:




HOCON(人性化配置对象表示法,Human-Optimized Config Object Notation)

这并非正式文档,不过我觉得我讲得比较清楚了。



目标/背景

HOCON 的主要目标是:保证 JSON 的语义(如树形结构;类型集合;编码/转义等)的同时,作为一个供人类编辑的配置文件,使其编辑起来更方便。

我们为方便编辑添加了以下新特性:


实现上,这一格式需要满足以下特征:


HOCON 比 JSON 难描述也难解析得多。想象一下一些维护配置文件的工作,本来是人类负责,结果被转移到机器负责,会发生什么。



定义




语法

这部分的大量内容都一定程度上借用了 JSON 的相关概念;当然你可以在https://json.org/json-zh.html找到 JSON 的语法规范。



和 JSON 一样的地方




注释

所有以 //# 开头,并以一个新的换行符结尾的部分将被视作注释处理,除非 //# 出现在了加引号的字符串中。



对根结构更宽松的要求

JSON 格式要求根结构必须为数组或者对象。空文件不合法,只含有字符串等既不是数组也不是对象的元素的文件,也不合法。

HOCON 文件如果不以方括号或花括号开头,那么它将以被 {} 包围的方式解析。

一个省略了开头 { 却没有省略结尾 } 的 HOCON 文件不合法;HOCON 格式要求括号必须匹配。



键值分隔符

字符 = 可以被用在所有 JSON 要求使用 : 的场合,例如:用于分隔键值。

如果一个键随后的字符为 {,那么中间的 = 可以省略。也就是说,"foo" {}"foo" : {} 是一样的。



逗号

对于数组里的值,以及对象里的键值对,只要它们之间有至少一个 ASCII 回车(\n,ASCII 码为 10)分隔,那么逗号就是可有可无的。

最后一个数组里的元素后,或者最后一个对象里的键值对后,可以跟一个逗号。多出来的逗号将被忽略。




空白

JSON 语法规范只简单提到了“空白”("whitespace")一词;在 HOCON 中,“空白”定义如下:


在 Java 中,isWhitespace() 方法可以覆盖除了不换行空格和 BOM以外的上述所有字符。

尽管所有 Unicode 中定义的分隔符都应视作空格,本规范中所称“换行符”("newline")指且仅指 ASCII 换行符 0x000A。



重复键与对象合并

JSON 规范中并没有明确同一个对象下重复键的处理方式。在 HOCON 中,重复的键的处理方式是以后来者为准,即后出现的键的值覆盖先前出现的;但如果重复的键对应的值都是对象,那么两个对象会合并在一起。

注意:如果你假定 JSON 中重复的键有特定行为,HOCON 将不再是 JSON 的超集。本规范中假定 JSON 不允许重复的键。

合并对象的过程如下:


对象的合并可以通过预先给键赋另外一个值来避免。这是因为,合并总是围绕两个值进行的。如果你先给一个键赋值为对象,然后赋一个非对象的值,接着再赋值为另一个对象,那么首先第一个对象会被那个非对象的值覆盖(非对象总是会覆盖对象),然后第二个对象将原本的值覆盖掉(没有合并,直接赋值)。因此两个对象之间什么事也没有发生。

下面两段 HOCON 是等价的:

  1. {
  2.     "foo" : { "a" : 42 },
  3.     "foo" : { "b" : 43 }
  4. }

  5. {
  6.     "foo" : { "a" : 42, "b" : 43 }
  7. }
复制代码

下面两段 HOCON 也是等价的:

  1. {
  2.     "foo" : { "a" : 42 },
  3.     "foo" : null,
  4.     "foo" : { "b" : 43 }
  5. }

  6. {
  7.     "foo" : { "b" : 43 }
  8. }
复制代码

注意中间为 "foo" 赋值为 null 的操作阻止了对象的合并。



不加引号的字符串

不被引号括起来的字符串序列(unquoted string),在符合下列条件时,会被认为是字符串值:


不加引号的字符串将按其字面值解析,也就是说不支持转义。如果你想要使用特殊字符,而这种特殊字符不允许在不加引号的字符串中出现的话,那你或许总是要加上引号的。

truefoo 将被解析面一个布尔值 true 跟随着一个字符串 foo。不过,footrue 将被解析成不加引号的 footrue。类似的情况还有,10.0bar 将被解析成数值 10.0 和不加引号的 bar 的组合,而 bar10.0 将被解析成不加引号的 bar10.0。(实际情况是,由于值连结的存在,这种区别无关紧要;请看后续章节。)

通常情况下,不加引号的字符串将在出现"//"这种两字符字符串,或者不允许在不加引号的字符串中出现的字符串结束。在其中(非开头)出现的布尔值,空值(null),以及数值等将不会被特殊对待,而是被看作字符串的一部分。

不加引号的字符串不能以数字 0 到 9 或连字符(-,0x002D) 开头,因为它们作为 JSON 数值开头是合法的。开始的数字字符以及随后的所有在 JSON 中作为数值合法的字符,都一定会被解析成数值。再强调一次,这种字符在不加引号的字符串 中间 是不被特别对待的;只有在开头出现的情况才会被按照数字解析。

JSON 中被引号括起来的字符串不允许包含控制字符(一些控制字符同时作为空白字符使用,如换行符等)。JSON 规范规定了这一行为。不过,对于不加引号的字符串,没有针对控制字符的限制,除非控制字符同时是上面提到的不允许出现的字符。

上面提到的字符不允许出现,一部分是由于它们在 JSON 或 HOCON 中已经有其含义,另一部分作为保留字使用,以方便未来扩展这一规范。



多行字符串

和 Python 以及 Scala 等语言类似,多行字符串使用三个引号。如果在解析时解析到了 """ 三个字符的序列,那么在下一个用作闭合字符序列的 """ 出现之前,其中所有 Unicode 字符都将被不加修改地用作字符串值的组成部分。不管是空格还是换行符,都不作特殊处理。和 Scala 的处理方式,以及 JSON 对待被引号括起来的字符串的处理方式不同,转义符在被三个引号括起来的字符串中不作处理。

在 Python 中,诸如 """foo"""" 的形式会导致语法错误(三个引号的字符串序列后紧跟着一个悬空引号)。在 Scala 中,这种形式将被看作由四个字符组成的字符串 foo"。HOCON 的解析方式和 Scala 类似;序列中的最后三个引号被看作多行字符串的闭合字符序列,而所有“多出来的”引号将被看作多行字符串的一部分。



值连结

对象中键值对的值或者数组元素可能表现为多个合在一起的值的组合。有三种值连结的方式:


除键值对的值和数组元素外,键值对的键也支持字符串值连结。对于键值对的键来说,对象或数组的组合没有意义。

注意:Akka 2.0(因此也包括 Play 2.0)针对配置文件的内置实现不支持针对数组或对象的值连结;其支持的只有字符串值连结。

字符串值连结

字符串值连结保证了未加引号的字符串正常工作;字符串值连结同时提供了对引用(诸如 ${foo} 的形式)的支持。

字符串值连结只允许简单值的组合。再次强调简单值的定义为除数组和对象外的其他类型值。

只要简单值仅由换行符之外的空白分隔,那么 它们之间的空白就会被保留,使值与空白连结组成一个字符串。

字符串值连结将不会跨过换行符,或者任何不属于简单值的字符。

所有字符串可能出现的地方都可能出现字符串值连结,比如说对象的键和值以及数组元素。

无论何时,如果一个值本当出现在 JSON 中,那么一个 HOCON 解析器会在对应位置尝试收集多个值(包括它们之间的空白),并将这些值连接成一个字符串。

在第一个值前或最后一个值后的空白将会被忽略。只有值 之间 的空白会被保留。

所以比如说 foo bar baz 会被解析成三个未加引号的字符串,然后这三个字符串会被连结成一个字符串。中间的空白将会被保留,但是两边的空白会被去除。因为等价的,被引号括起来的字符串形式为 "foo bar baz"

值连结后的 foo bar(两个未加引号的字符串以及中间的空白)和被引号引用的 "foo bar" 在解析后内存里的形式是一样,都是七个字符的字符串。

为保证字符串值连结,非字符串类型的值将会以以下规则转换成字符串(以下转换结果使用被引号引用的形式):


单个值不应转换成字符串。换言之,如果你试图使用值连结的方式对待 true 本身,那么解析结果就会出错;因为解析时应该当作布尔值对待。只有诸如 true footrue 后面同一行跟着另一个简单值)的形式才能以值连结的方式解析并转换成字符串。

数组值连结和对象值连结

数组可以和数组之间值连结,对象也可以和对象之间值连结,但如果混着来就会出错。

为保证值连结,“数组”同时也包括“值为数组的引用”,同时“对象”同时也包括“值为对象的引用。”

在键值对的值或数组元素中,如果第一个数组或对象或引用的末尾,以及第二个数组或对象或引用的开头,只有换行符之外的空白分隔,那么两个值将会进行值连结。

对于对象来说,“连结”意味着“合并”,因此后一个值将会覆盖前一个。

不管是否存在值连结,数组和对象都不能成为键值对的键。

下面的几种方式定义的对象 a 是完全等价的:

  1. // one object
  2. a : { b : 1, c : 2 }
  3. // two objects that are merged via concatenation rules
  4. a : { b : 1 } { c : 2 }
  5. // two fields that are merged
  6. a : { b : 1 }
  7. a : { c : 2 }
复制代码

下面的几种方式定义的数组 a 是完全等价的:

  1. // one array
  2. a : [ 1, 2, 3, 4 ]
  3. // two arrays that are concatenated
  4. a : [ 1, 2 ] [ 3, 4 ]
  5. // a later definition referring to an earlier
  6. // (see "self-referential substitutions" below)
  7. a : [ 1, 2 ]
  8. a : ${a} [ 3, 4 ]
复制代码

一种常见的对象值连结用法和“继承”类似:

  1. data-center-generic = { cluster-size = 6 }
  2. data-center-east = ${data-center-generic} { name = "east" }
复制代码

一种常见的数组值连结用法被用于文件路径集合:

  1. path = [ /bin ]
  2. path = ${path} [ /usr/bin ]
复制代码

注意:引用之间含有空白的值连结

如果你试图使用 ${foo} ${bar} 等形式连结两个引用,那么被连结的引用可能会转换成字符串(这使得其之间的空白十分重要),可能会转换成对象或者列表(在这里其之间的空白无关紧要)。对于其值为对象或者列表的引用,其之间的空白应该被忽略。如果空白被引号括了起来,将产生语法错误。

注意:不含有逗号或换行符的数组

在数组中,你可以使用换行符代替逗号,不过你不能使用空格代替逗号。因此换行符之外的空白将导致数组元素值连结而不是数组元素值分隔。

  1. // this is an array with one element, the string "1 2 3 4"
  2. [ 1 2 3 4 ]
  3. // this is an array of four integers
  4. [ 1
  5.   2
  6.   3
  7.   4 ]

  8. // an array of one element, the array [ 1, 2, 3, 4 ]
  9. [ [ 1, 2 ] [ 3, 4 ] ]
  10. // an array of two arrays
  11. [ [ 1, 2 ]
  12.   [ 3, 4 ] ]
复制代码

如果你对此感到迷惑,你应该用一用逗号。在下面的情况下,值连结行为是足够有用的,而不是令人惊讶的:

  1. [ This is an unquoted string my name is ${name}, Hello ${world} ]
  2. [ ${a} ${b}, ${x} ${y} ]
复制代码

换行符之外的空白不会被用作元素和键值对的分隔符。



路径表达式

路径表达式(path expression)被用于表示对象树中的一个路径。一些诸如 ${foo.bar} 等使用引用的场合,以及诸如 { foo.bar : 42 } 等使用键值对的键的场合会用到路径表达式。

路径表达式在语法上与值连结相同,但不会包含引用。这意味着你不能在引用中使用引用,以及你也不能在键值对的键中使用引用。

在路径表达式中,被引号括起来的字符串外的 . 被当作分隔路径的分隔符处理,而被引号括起来的字符串内的 . 不作特殊处理。因此 foo.bar."hello.world" 代表一个有三个组成部分的路径表达式,前两个分别是 foobar,最后一个是 hello.world

需要注意的一点是,数字之间的 . 将被当作分隔符处理。因此如果将数字作为路径表达式的一部分进行处理,那么必须将其以文件中出现的 原始 字符串表示形式处理(而不是使用一些通用的函数将其从数字转换回字符串)。


和值连结不同,路径表达式应 总是 被转换成字符串,即使其只代表一个值。

如果在解析时遇到一个数组,其中一个元素的值为单个 true,那么解析时应当作值连结的方式处理,也就是应以布尔值的方式处理。

如果在解析(键值对的键或者引用)时遇到一个路径表达式,那么其应总是当作字符串处理,因此 true 应被当作一个字符串,其被引号括起来的形式是 "true"

如果路径表达式是空字符串,那么它应永远被引号括起来。换言之,a."".b 代表一个有着三个元素的路径表达式。不过,a..b 是不合法的,并应在解析时报错。按照这样的规则,所有在开头或者结尾时出现 . 的路径表达式,都应被当作不合法的情况在解析时报错处理。



作为键的路径表达式

如果一个键同时也是一个包含有多个元素的路径表达式,那么在解析时除最后一个元素外的所有元素都将被展开成对象。路径的最后一个元素与值结合,从而最后形成嵌套对象中的一个键值对。

换言之:

  1. foo.bar : 42
复制代码

和:

  1. foo { bar : 42 }
复制代码

是等价的。以及:

  1. foo.bar.baz : 42
复制代码

和:

  1. foo { bar { baz : 42 } }
复制代码

也是等价的。对象的值会进行合并;也就是说:

  1. a.x : 42, a.y : 43
复制代码

和:

  1. a { x : 42, y : 43 }
复制代码

是等价的。因为路径表达式和值连结类似,所以说你可以在键值对的键中使用空格,比如说:

  1. a b c : 42
复制代码

和:

  1. "a b c" : 42
复制代码

是等价的。此外,因为路径表达式总是被转换成字符串,因此即使是拥有其他类型含义的单个值,也会被转换成字符串类型。


有一条特殊的规则,就是不加引号的 include 如果被用于键值对的键,那么它不能作为路径表达式的开头,因为其有特殊含义(见后续章节)。



引用

引用(substitution)配置文件树中的其他部分是 HOCON 允许的一种形式。

引用的语法是这样的:${pathexpression}${?pathexpression}。其中,pathexpression 便是上文中提及的路径表达式。这里用到的路径表达式的语法与用作对象的键的语法是一样的。

${?pathexpression} 中的 ? 前不能有空格。换言之,使用这种形式的引用时,${? 必须原样组合在一起使用。

某个实现可以通过查询系统环境变量或其他外部配置来解析在配置树中没有找到的引用。(关于环境变量的细节将在后文中阐述。)

引用不会尝试解析包含在其中的加引号的字符串。如果你需要在字符串中使用引用,你必须使用值连结把引用和不加引号的字符串连接起来:

  1. key : ${animal.favorite} is my favorite animal
复制代码

你也可以用引号把非引用部分括起来:

  1. key : ${animal.favorite}" is my favorite animal"
复制代码

引用通过查询整个配置来解析。路径从根对象开始解析,换言之路径是绝对路径,而非相对路径。

引用处理是解析的最后一步,所以引用也可以向后查询。如果一个配置包含了多个文件,最终引用还可以解析到别的文件上去。

如果一个键出现了多次,引用只会解析到最后一次出现的值(换言之,它会解析到所有包含的文件中该键的最终赋值,或最终合并出来的对象)。

如果有一个选项设定为 null,那么解析它的键时就永远不会从外部来源中解析。不幸的是,这个操作是不可逆的;如果你的根对象中有类似 { "Home" : null } 的东西,那么解析 ${HOME} 就永远不会解析到系统环境变量上去。换言之,HOCON 中没有等价于 JavaScript 的 delete 的操作。

若引用无法匹配到任何配置中出现的值,同时也不能通过外部来源解析成任何值,那么这个引用会成为未定义引用。以 ${foo} 形式出现的未定义引用是非法的,应当按照错误处理。

若形如 ${?foo} 的引用没有定义:


引用只能用于键值对的值或数组元素(值连结)中,不能用于键名,亦不能嵌入路径表达式等其他引用中。

引用会被任意一种值类型(数字、对象、字符串、数组、truefalsenull)替换。如果最终值只由引用组成,值类型会保留;否则,会通过值连结组成字符串。

自引用

总的来说:


通过这种方式我们得以允许基于键值对的旧值设置新值:

  1. path : "a:b:c"
  2. path : ${path}":d"
复制代码

自引用键值对 指:


自引用键值对的示例:


需注意的一点是,如果一个数组或对象中的值含有一个指向自身值的引用,在解析时将 考虑自引用键值对的相关规则。也就是说,以下情况相关规则 作考虑:


这种形式的循环应该直接在解析时报错。(假设允许“向前看”的话,一些诸如 a={ x : 42, y : ${a.x} } 的形式会在解析 ${a.x} 时试图解析不存在的 a。)

可能的实现有:


最简单的实现形式会在解析时将循环当作不存在的引用处理;比如说在解析 a : ${a} 时,你会首先把 a : ${a} 本身移除然后再解析 ${a},也就是在一个空文件中检索对应的 ${a} 的值。更复杂一点的做法是在被移除的键值对处添加一个标记符,从而在发现循环时产生可读性更高的错误信息。然后,在回到标记符对应的引用本身时报错。

对于可选引用(诸如 ${?foo} 的形式)来说,对待循环的解析方式应同样按照不存在的值处理。如果 ${?foo} 引用了自身,那么解析时就应该当作不存在的值处理。

键值分隔符 +=

除了 :=,键与值之间还可以用 += 分割。使用 += 分隔的键值对会令值变为自引用数组,例如:

  1. b += a
复制代码

会变成:

  1. b = ${?b} [a]
复制代码

+= 起到了在数组结尾追加元素的作用。如果 b 之前的值不是数组,它会产生和 b = ${?b} [a] 一样的报错。注意,b 的值不一定必须存在(${?b} 而非 ${b}),换言之 b += a 这样的声明可以是全文件中第一次出现 b 的地方(即不需要 b = [] 这样的显式声明)。

注意:Akka 2.0(因此也包括 Play 2.0)针对配置文件的内置实现不支持 +=

自引用举例

在没有合并的情况下,自引用的键值对是非法的,因为其具体值无法解析:

  1. foo : ${foo} // an error
复制代码

然而,当 foo : ${foo}foo 之前的值合并时,这个引用就能解析到之前的值上。合并对象时,覆盖键值对中的引用指向被覆盖的键值对。例如:

  1. foo : { a : 1 }
复制代码

在它之后又有:

  1. foo : ${foo}
复制代码

此时 ${foo} 会解析为 { a : 1 },即被覆盖的键值对的值。

如果两者顺序颠倒一下,就会产生错误。比如:

  1. foo : ${foo}
复制代码

在它之后又有:

  1. foo : { a : 1 }
复制代码

在这里 ${foo} 自引用出现在了 foo 被赋值之前,所以此时的 foo 是没有定义的,无异于引用一个整个文件中没有定义的路径。

概念上来说,foo : ${foo} 是需要查找 foo 之前的定义以决定具体的解析结果的,所以它的报错应当是“没有定义(undefined)”而非“不可跳出的循环引用(intractable cycle)”。也因此,使用可选引用(optional substitution)即可避免循环引用的问题:

  1. foo : ${?foo} // 这个键会静静地消失
复制代码

如果引用被无法合并的值(非对象的值)隐藏起来了,那么它就不会被解析,也因此不会报错。例如:

  1. foo : ${does-not-exist}
  2. foo : 42
复制代码

在这个情况下,不管 ${does-not-exist} 解析结果如何,我们都能确定 foo42,所以 ${does-not-exist} 不会被解析,也因此不会产生任何错误。对于形如 foo : ${foo}, foo : 42 这样的循环引用也是如此——第一个循环引用会被直接忽略。

即便是出现在路径表达式中的自引用,它也会解析到“下一层”的值上去。举例说明:

  1. foo : { a : { c : 1 } }
  2. foo : ${foo.a}
  3. foo : { a : 2 }
复制代码

在这里,${foo.a} 会指向 { c : 1 } 这个对象,而非 2 这个数字,所以最终 foo 的值会是合并之后的 { a : 2, c : 1 }

回想一下,自引用键值对必须使用引用或值连结来确定最终值。举个例子,如果键值对的值是对象或数组,那么即使在这个对象或数组中有对这个键值对的引用,它也不会算作自引用。

HOCON 的实现必须小心对待在对象中引用自己的路径的情况,举例:

  1. bar : { foo : 42,
  2.         baz : ${bar.foo}
  3.       }
复制代码

在种情况下,如果某个实现选项将解析整个 bar 对象的过程作为解析引用 ${bar.foo} 过程的一部分,那么就会产生循环引用。这种情况下,HOCON 的实现应当只尝试解析 bar 对象 foo 中的 foo,而非整个 bar 对象。

因为没有循环继承,引用有必要“向前解析”(包括查找正在定义中的键值对)以确定解析结果。举例说明:下面的 HOCON 中,bar.baz 最终会解析成 43

  1. bar : { foo : 42,
  2.         baz : ${bar.foo}
  3.       }
  4. bar : { foo : 43 }
复制代码

相互引用的对象也是成立的,同时也不会认为是自引用(因为也会“向前解析”):

  1. // bar.a should end up as 4
  2. bar : { a : ${foo.d}, b : 1 }
  3. bar.b = 3
  4. // foo.c should end up as 3
  5. foo : { c : ${bar.b}, d : 2 }
  6. foo.d = 4
复制代码

另一种**情况是值连结中的可选自引用,下面的 HOCON 中 a 首先会被解析为 foo 而非 foofoo,因为自引用会“向后解析”并成功解析没有定义的 a

  1. a = ${?a}foo
复制代码

总体上来说,解析引用的实现应当:


举例,下列 HOCON 无法解析:

  1. bar : ${foo}
  2. foo : ${bar}
复制代码

像是这样由多个键值对组成的循环也应能识别为非法 HOCON:

  1. a : ${b}
  2. b : ${c}
  3. c : ${a}
复制代码

在某些情况下,解析结果依赖于解析顺序,但具体解析顺序没有定义时,就会产生未定义行为。例如:

  1. a : 1
  2. b : 2
  3. a : ${b}
  4. b : ${a}
复制代码

HOCON 的实现可以在“ab 都解析为 1”、“都解析为 2”或者产生错误三种行为之间选择。理论上,这种情况应当产生错误,但这种行为可能会很难实现。令这种行为有确定结果一定需要有序表而非无序表的支撑,这也会制造一些限制。理论上,HOCON 的实现只需要追踪相同键值对的重复实例(即合并)。

然而,HOCON 的实现必须选择将 ab 解析为相同的值。在实践中,这意味着所有的引用都必须存储起来(只解析一次,保存解析结果)。存储解析的方式应当以引用它本身为键,而非 ${} 表达式中的路径,因为根据其所在文件中的位置不同,稍后的解析结果可能会有所差异。



跨文件引用

跨文件引用语法

跨文件引用声明 由未加括号的 include 和随后的空白符及之后的:


跨文件引用声明应用于原为键值对的地方。

如果 include 出现在一个路径表达式的开头,而该路径表达式本身作为对象的键存在,那么它将不会被以路径表达式或者键的方式解析。

作为替代键值对的声明,include 后必须跟随一个被引号 括起来的 字符串,或者一个被引号括起来,又被 url()file()、或者 classpath() 括起来的字符串。该字符串值被称为 资源名称

总的来说,include 以及其后的资源名称被用于原为键值对的地方,因此语法上,跨文件引用声明应通过逗号(如果有换行符的话逗号可以省略)和其他键值对分隔。

如果 include 出现在对象的键的位置,而随后没有出现被引号括起来的字符串或者 url("")/file("")/classpath("") 等形式,那么这种声明是不合法的,从而在解析时应该报错。

include 和资源名称之间可以有任意多的空白,包括换行符。对于 url() 等声明形式,() 内(以及引号外)同样允许出现空白。

include 后或 url() 等形式中,不允许使用值连结。其值只能使用被引号括起来的字符串形式。引用形式也不被允许,换言之,除被引号括起来的字符串,其他情形都不被允许。

在对象的键的开始位置之外的 include 没有特殊意义。

include 可以出现在对象的键的声明中:

  1. # this is valid
  2. { foo include : 42 }
  3. # equivalent to
  4. { "foo include" : 42 }
复制代码

或者作为对象或者数组的值:

  1. { foo : include } # value is the string "include"
  2. [ include ]       # array of one string "include"
复制代码

如果你想使用以 "include" 开头的字符串作为对象的键,你可以将其括起来,也就是 "include" 的形式,只有不加引号的 include 是特殊的:

  1. { "include" : 42 }
复制代码

注意:Akka 2.0(因此也包括Play 2.0)针对配置文件的内置实现不支持 url()/file()/classpath() 形式的跨文件引用。相应的实现只支持启发式的 include "foo" 等形式。

跨文件引用语义:合并

我们定义 文件引用者(including file) 为跨文件引用声明的文件,同时定义 被引用文件(included file) 为跨文件引用声明中的值对应的文件。(文件引用者和被引用文件不一定总是文件系统中的文件,不过这里我们先假设它们都是。)

被引用文件必须包含一个对象,而不是数组。这很重要,因为不管是 JSON 还是 HOCON 都允许数组或者对象作为文件的根节点。

如果被引用文件包含了一个数组,那么跨文件引用声明就是不合法的,也就是说解析时会报错。

被引用文件会被解析成一个根对象。根对象的键在概念上代替了文件引用者中的跨文件引用声明。


跨文件引用语义:引用

被引用文件中的引用会使用两种策略在文件中检索;首先会检索被引用文件本身的根节点;然后再检索文件引用者的根节点。

再次强调一点,引用的解析发生在语法分析 ,解析的最后阶段。对于引用的解析应该针对所有文件,而不应将文件隔离开来。

因此,一个包含有引用的被引用文件在解析时必须将相对于被引用文件本身的引用路径“调整”成文件引用者决定的根节点的相对路径。

我们选取这样一个文件引用者作为示例:

  1. { a : { include "foo.conf" } }
复制代码

然后“foo.conf”看起来是这样的:

  1. { x : 10, y : ${x} }
复制代码

如果你对“foo.conf”单独解析的话,那么 ${x} 的值将被解析成 x 路径对应的 10。如果你在一个对象中,键为 a 的地方引用了“foo.conf”,那么相应的路径应该被调整成 ${a.x} 而不是 ${x}

如果文件引用者重新定义了 a.x,如下所示:

  1. {
  2.     a : { include "foo.conf" }
  3.     a : { x : 42 }
  4. }
复制代码

那么“foo.conf”中被调整成 ${a.x}${x},在解析时将会检索到 42 而不是 10 这一数值。因为引用的解析位于语法分析

不过,被引用文件本身可能会大量出现引用文件以外的值的情况。比如说引用一个系统环境变量的值,或者说某些文件中的对应值。因此单单解析被调整后的路径不总是够用的,你还需要解析原本的路径。

跨文件引用语义:不存在的文件和强制要求的文件

默认情况下,如果文件引用者试图引用一个不存在的文件,那么该引用本身应该被静默忽略(就像被引用文件本身代表一个空的对象一样)。

但如果被引用文件本身被强制要求存在,同时跨文件引用声明使用了 required(),那么在解析不存在的被引用文件时应该报错。

合法的声明格式包括

  1. include required("foo.conf")
  2. include required(file("foo.conf"))
  3. include required(classpath("foo.conf"))
  4. include required(url("http://localhost/foo.conf"))
复制代码

等。其他类型的 IO 错误在解析时按理说也不应忽略,不过相应的实现需要在这方面权衡,也就是说在解析时决定将其作为一个可忽略的文件处理,还是决定提醒用户报错。

跨文件引用语义:文件类型及格式

HOCON 的相应实现可能会支持引用其他类型的文件。支持的其他类型必须和 JSON 类型系统兼容,或者说能够提供到 JSON 类型系统的映射。

若相应实现支持多类型文件的跨文件引用,跨文件引用声明中的文件后缀名有可能会被省略:

  1. include "foo"
复制代码

如果未加后缀名,那么解析时应将其当作文件名的前缀对待,并试图添加所有的已知后缀然后试图加载文件。

如果满足条件的文件存在有多个,那么它们应该被 全部 加载,然后合并到一起。

HOCON 格式的文件总是应该被最后解析。JSON 格式的文件应该作为倒数第二个文件解析。

换言之,include "foo" 可能和:

  1. include "foo.properties"
  2. include "foo.json"
  3. include "foo.conf"
复制代码

等价。对于以 classpath 为来源的资源,基于文件后缀名的相应规则同样适用。

对于 URL 来说,跨文件引用声明中不允许不含有文件后缀名;你只能使用整个未加删减的 URL。相应的解析方式可能由返回数据的 Content-Type 决定,或者当 Content-Type 不存在时使用文件后缀名决定。使用 file: 格式的 URL 同样要求如此。

跨文件引用语义:资源定位

启发式的检索将会在声明中未出现url()file()、或classpath()时进行。启发式的检索策略如下:


不同的具体实现对于能够引用的不同类型资源的定义可能大相径庭。

对于 Java 语言的 classpath 来说:


对于文件系统中的文件来说:


对于 URL 来说:


特定实现不必总是支持文件,Java 语言的 classpath 资源,以及 URL;同时特定实现也不必一定支持某个特定的协议。不过如果支持的话,相应的检索策略应该和上面描述的相同。

需要注意的一点是,如果指定了 url()/file()/classpath(),被引用的节点将不会相对于引用者解析。这种解析方式只用于启发式的解析,也就是针对 include "foo.conf" 等声明格式的解析。该条规定可能会在未来发生变化。



数字索引对象到数组的转换

在某些文件格式或者上下文,比如说 Java 的 properties 文件格式等情况下,定义数组比较困难。考虑到这种情况,HOCON 的相应实现应支持将数字格式键的对象转换到数组。比如说下面这个对象:

  1. { "0" : "a", "1" : "b" }
复制代码

可以被当作下面这种形式处理:

  1. [ "a", "b" ]
复制代码

一些诸如 properties 等格式的文件就可以使用这种方式定义一个数组:

  1. foo.0 = "a"
  2. foo.1 = "b"
复制代码

相关细节:




MIME 类型

在诸如 Content-Type 等情况下,MIME 类型使用“application/hocon”。



对于 API 的建议

完美的 HOCON 格式实现应遵守下面这些约定,并以可预测的方式正常工作。



自动类型转换

如果解析时需要用到一个特定类型的值,那么相应实现应该按照以下规则转换类型:


下面的类型转换永远都不应该出现:


对象或者数组和字符串之间的相互转换听起来很吸引人,但是实际应用中,引号及多重转义等问题会让人非常苦恼。



单位格式

HOCON 的实现可以选择支持解释某些类型的单位,比如时间单位和内存尺寸单位:10ms512K 这样的。HOCON 本身并不无可拓展的类型系统,也没有原生的“持续时间“类型的支持。但是,若应用程序要求以毫秒为单位的数据,HOCON 的实现可以尝试将值解释为毫秒数。

若有 API 支持,对于每个类型的单位都应当有其默认的单位。例如,时间类单位的默认单位可以是毫秒(细节参见下文)。HOCON 的实现应当按下列方式解释:




时间单位

HOCON 的实现可以提供对 getMilliseconds() 及其他类似时间单位的支持。

时间单位可以利用上文中提到的一般“单位格式”:不带单位的数字视作使用毫秒为单位,字符串视作数字和可选的单位的组合。

受支持的时间单位的字符串应当大小写敏感,并只支持小写。下列字符串是所有支持的单位的准确形式:




日期单位

getDuration() 方法类似,getPeriod() 可用来获取时间单位并转化为 java.time.Period

日期单位可以利用上文中提到的一般“单位格式”:不带单位的数字视作使用天为单位,字符串视作数字和可选单位的组合。

受支持的时间单位的字符串应当大小写敏感,并只支持小写。下列字符串是所有支持的单位的准确形式:




字节单位描述的尺寸

HOCON 的实现可以选择支持 getBytes(),它返回以字节单位描述的尺寸。

它可以利用上文中提到的一般“单位格式”;不带单位的数字视作使用字节为单位,字符串视作数字和可选单位的组合。

单字母的单位可以使用大写字母(注意:时间单位永远都是小写,这个规定仅针对尺寸单位)。

然而不幸的是,单位标准的不同可能会招来麻烦——这个问题就是以 2 为底和以 10 为底的问题。业界标准采取的做法和大众的用法不尽相同,以至于使用业界标准会令普通人困惑。更棘手的是大众的用法还会因为“是在讨论内存还是硬盘空间”而有所变化,操作系统和应用程序的不同更是令在给这个问题火上浇油。详细的案例可参考 https://zh.wikipedia.org/wiki/%E4%BA%8C%E8%BF%9B%E5%88%B6%E4%B9%98%E6%95%B0%E8%AF%8D%E5%A4%B4#%E4%BA%8C%E8%BF%9B%E5%88%B6%E5%92%8C10%E8%BF%9B%E5%88%B6%E8%AF%8D%E5%A4%B4%E5%A4%A7%E7%BA%A6%E6%AF%94%E7%8E%87。显然,在不先制造混乱的情况下是没办法理清这里面的头绪的。

对于单个字节来说,下列字符串是所有支持的单位的准确形式:


对于 10 为底的单位来说,下列字符串是所有支持的单位的准确形式:


对于 2 为底的单位来说,下列字符串是所有支持的单位的准确形式:


使用单字母缩写的时候(比如 "128K" 这样的)会产生歧义;但诸如 java -Xmx 2G、GNU 工具中的 ls 表这样的先例使用的是以 2 为底的单位,所以本规范也遵从这些先例。当然,你也能找到将这些单位映射到以 10 为底的单位的例子。如果你不想制造歧义,那就不要用单字母的单位。

注意:zetta/zebi、yotta/yobi 以及更大的单位肯定会导致 64 位整数的溢出。现实世界中,API 和应用程序通常不会支持这些大单位。提供这些单位的实现通常只为了追求完美,但实际上实用性不高(至少 2014 年是如此)。



配置对象合并与文件合并

提供合并两个对象的方法也许会有用。若提供了这样一个方法,它的工作方式应当和处理重复键的方式一样。(关于重复键的处理请参考前文。)

和处理重复键一样,中间插入的非对象值会“隐藏”之前的对象。比方说如果你按下列顺序合并对象:


结果会是 { a : { x : 1 } }。两个对象因为“不相邻”所以没有合并;合并是成对进行的,42{ y : 2 } 合并时,42 优先的规则使得后者的信息完全丢失。

但如果合并的顺序改成这样:


此时的结果就是 { a : { x : 1, y : 2 } },因为两个对象现在相邻了。

合并两个文件中不同的对象的规则和合并同一文件中重复键值对的规则 完全相同。所有的合并都使用同一套规则。

某一个配置的值应当是数字还是对象这样的规则不需要重复。两种类型混淆在一起的情况从一开始就不应该出现。

但这样的情况还是有用的:你可以通过赋值为 null 的方式来清空之前的值,然后重新来过。若如此做,就可以避开默认的备选项。



与 properties 文件之间的映射

将 Java 语言的 properties 数据与 JSON 或 HOCON 中的数据在某些时候是有用的。关于 Java 的 properties 文件的规范,可参考这里: https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html#load-java.io.Reader-

Java poperties 通常被解析为字符串到字符串的单射。

将 Java properties 转换为 HOCON 时,首先要将键按 . 分割,保留所有开头和结尾的空格。注意这和解析路径表达式 大不相同

. 分割键后会得到一系列路径元素。据此,键 . 分割后的结果是两个空字符串。a. 分割后的结果是 a 和一个空字符串。(注意 Java 的 String.split 完全不是这样工作的。)

在 properties 中不可能使用 . 作为键。如果在 JSON/HOCON 通过引号等方式是用来 . 作为键名,那么这个键就无法表达为 Java property。因为这样的键不论什么时候都只可能导致混乱,我们不推荐在 HOCON 的键名中使用 .

当所有的值对应的路径解析完毕后,根据这些路径构造 JSON 风格的对象树。

Properties 中解析出的值 永远 都是字符串,即使能解析成其他类型的值也应如此。若应用程序要求整数,HOCON 的实现应按照前文中描述过的方法进行类型转换。

不幸的是 Java 加载 properties 时不保留顺序。结果就是,当有一个键同时对应一个对象和一个字符串时,就没有办法正确处理了。例如,如果有如下 properties 文件:

  1. a=hello
  2. a.b=world
复制代码

在这个情况下,a 需要同时作为对象和字符串两个值的键。在这个情况下,相应的 对象 必须作为最终的解析结果……以“对象优先”为原则时,只会丢弃最多一个值(字符串),但如果以“字符串优先”为原则的话,整个对象的值都会丢失。然而,在将 properties 映射为 JSON 结构后,那个与对象冲突的字符串值就再也无法访问到了。

HOCON 通常的原则是“后来者优先”而非“对象优先”,但实现这个效果需要再实现一个自定义的 Java properties 解析器,但这样并不值得,也对系统属性没什么帮助。



常规的 JVM 应用配置文件

通常,JVM 上的应用的配置文件由两部分组成:


对类加载器来说,它应当首先加载、合并并解析参考配置,以供通过同样类加载器加载的应用的配置使用。应用配置不会影响参考配置的引用,因为参考配置的解析只依靠参考配置它自己。

应用配置应在参考配置加载完成后加载,并以参考配置中的值为备选项,在此基础上进行解析。这意味着应用配置的引用可以来自参考配置。



常规的系统属性覆盖

对于一个应用的配置来说,Java 的系统属性 应覆盖 配置文件中的定义。如此做即可支持通过命令行指定配置选项。



环境变量用作引用解析的备选项

回想这样的情况:某个引用无法在其配置树中解析为任何值(甚至不是 null),HOCON 的实现可以根据外部来源进行解析。其中,环境变量就可以是一种外部来源。

我们推荐 HOCON 中所有的键都使用小写字母,因为环境变量通常都是全大写字母命名的,这样可以避免冲突。(尽管 Windows 下的 getenv() 通常忽略大小写,但在开始查找环境变量之前 HOCON 都是大小写敏感的。)

同时请留意下文中对 Windows 平台及大小写问题的备注。

应用程序可以通过设定用同名键值对的方式,显式阻止引用查询环境变量。举例,在根对象中设置 HOME : null 这样的键值对可以防止 ${HOME} 解析到环境变量上去。

环境变量的解析过程如下:




连字符还是小写驼峰?

推荐使用 hyphen-separated 也就是连字符形式,而非 camelCase 也就是小写驼峰式,作为键名的命名规范。



注意:和 Java 语言的 properties 文件的相似性

你完全可以把一个 HOCON 格式的文件写成和 properties 文件类似的样子,同时大量的 properties 文件也可以被当作合法的 HOCON 格式文件解析。

但是,HOCON 并不是 Java 语言的 properties 文件的超集,对于一些特殊情况来说,HOCON 会按照类似于 JSON 的方式,而不是 properties 文件的方式解析。

不同之处包括但不限于:




注意:Windows 平台以及大小写敏感的环境变量

HOCON 检索环境变量永远采取大小写敏感的策略,但具体解析时 Linux 和 Windows 等平台的处理方式并不相同。

在 Linux 中你可以定义多个名称相同,但大小写不同的环境变量;因此 Linux 中可能会同时出现 "PATH" 和 "Path" 两个不同的环境变量。HOCON 在 Linux 平台采用直接的检索策略;换言之,请确保你的定义中,大小写都是正确的。

Windows 的情况更令人迷惑一些。Windows 中环境变量的名称可能包含大小写字符的混合,例如 "Path" 等,但是 Windows 不允许定义多个同名但大小写不同的环境变量。在 Windows 中访问环境变量不区分大小写,访问 HOCON 中的 env 变量区分大小写。在 Windows 中访问环境变量不区分大小写,不过在 HOCON 中访问环境变量是区分大小写的。因此如果你清楚你的 HOCON 文件需要 "PATH" 这一环境变量,那么你必须确保该变量被定义为 "PATH" 而不是诸如 "Path" 或者 "path" 等。不过,Windows 不允许我们改变一个已有环境变量的大小写;我们不能简单地把一个环境变量换成大写的形式。确保环境变量具有我们想要的大小写形式的唯一方法是首先将所有需要用到的环境变量取消定义,然后使用我们想要的大小写形式重新定义它们。

比如说我们可能有这样的环境变量定义……

  1. set Path=A;B;C
复制代码

……管他值是什么样的。不过如果 HOCON 需要用到 "PATH" 的话,那么启动脚本可能需要做一些预防性工作,以应对各种可能情况……

  1. set OLDPATH=%PATH%
  2. set PATH=
  3. set PATH=%OLDPATH%

  4. %JAVA_HOME%/bin/java ....
复制代码

在你的程序执行时,你没有办法了解周围环境中可能存在的环境变量,也没有办法知道这些定义可能会出现什么情况。因此,唯一安全的做法是重新定义你需要用到的所有变量,如上所示。


作者: 3TUSK    时间: 2018-3-17 06:21
其实我只是打了个酱油。我好像只翻译了 1/3。


作者: 舞麟    时间: 2018-3-17 12:04
打个酱油,评论一下
作者: 2254513908    时间: 2018-5-18 20:34
同上同上同上
作者: 15025321822    时间: 2019-7-20 09:56
灵活牛逼!
作者: 梅子紫转青    时间: 2020-3-6 17:01
提示: 作者被禁止或删除 内容自动屏蔽
作者: 尹雨程    时间: 2020-6-8 17:24
厉害厉害,MCBBS有你更精彩
作者: mhy1993mhy    时间: 2020-6-10 18:14
nbnbnbnbnb