配置文件语法和特性
Play使用的配置文件是基于 Typesafe 配置库.
Play应用程序的配置文件必须定义在conf/application.conf
。它使用HOCON 格式。
除application.conf
文件外, 配置也可以来自其它地方。
- 从任何
reference.conf
文件加载的默认设置都是建立在类路径之上。多数Play JARs 包含一个reference.conf
文件及其默认设置。在application.conf
中的设置会覆盖reference.conf
文件中的设置。 - 配置也可以使用系统属性。系统属性覆盖
application.conf
设置。
指定替换的配置文件
在运行时, 默认的application.conf
从类路径加载。系统属性可用于强制不同的配置源:
config.resource
指定源名称要包括扩展名, 如application.conf
而不是application
config.file
指定文件路径, 它应该包括扩展名, 不只是基本名称
这些系统属性为application.conf
指定一个替代, 不是一个附加。如果你还想使用一些application.conf
文件中的值,那么你可以包含application.conf
到你的其它.conf
文件,通过在这个文件的顶部编写include "application"
。在你包含了application.conf
的设置后,在你的新.conf
文件你可以指定任何你想要覆盖的设置。
使用Akka
Akka 会使用和Play应用程序相同的配置文件,这意味着你可以在application.conf
的目录中配置Akka中的任何东西。在Play, Akka 从play.akka
内读取它的设置, 不是从akka
设置。
使用run
命令
当用run
命令运行你的应用程序时,关于配置有几个特殊的东西要知道。
额外的devSettings
你可以在build.sbt
中为run
命令配置额外的设置。当你部署应用程序时这些设置不会被使用。
devSettings := Map("play.server.http.port" -> "8080")
在application.conf
中的 HTTP 服务设置
在 run
模式,Play的HTTP 服务器部分在应用程序编译之前就已经启动了。这也就是说当启动时HTTP服务器不能访问application.conf
文件。如果你想要用run命令覆盖HTTP服务务器设置,你不用使用application.conf
文件。相反, 你需要使用系统属性或像上面说的 devSettings
设置。服务器设置的一个例子就是HTTP端口。其它服务器设置可以查阅这里。
> run -Dhttp.port=1234
HOCON 语法
HOCON 类似于 JSON; 当然你可以在http://json.org/ 找到JSON规范。
不改变JSON
- 文件必须是有效的 UTF-8
- 带引号的字符串和JSON字符串格式相同
- 可用的值类型: string, number, object, array, boolean, null
- 允许数字格式匹配JSON; 作为 JSON, 和其它
浮点值不能表示,如 NaN
注释
任何在//
或#
之间,并且下一行是新行的内容被看做注释和被忽略,除非//
或#
在是引号内部的字符串。
省略根大括号
JSON 文档必须有任何数组或对象作为根。空文件是无效的文档, 比如文件仅包含一个非数组非对象值,如一个字符串。
在 HOCON, 如果文件不是由一个方括号或大花括号开始, 它会解析为我们用{}
大括号包起来。
一个 HOCON 文件如果它省略了打开的{
但是却有一个关闭的}
,那它是无效的; 花括号必须平衡。
键-值 分隔符
=
字符可以用在JSON允许:
的任何地方, 比如 分隔键和值。
如果键后面跟着{
, :
或=
可以省略。 所以 "foo" {}
的意思也是"foo" : {}"
逗号
数组中的值, 和对象的字段, 如果他们至少之间有一个ASCII换行符(\n
, decimal value 10),那么他们之间就不需要逗号。
数组中的最后一个元素或对象中的最后一个字段可跟一个逗号。这个多余的逗号将被忽略。
[1,2,3,]
和[1,2,3]
是相同的数组(多了一个逗号)。[1\n2\n3]
和[1,2,3]
是相同的数组。[1,2,3,,]
是无效的,因为它尾部有二个逗号。[,1,2,3]
是无效的,因为前部有一个逗号。[1,,2,3]
是无效的,因为行内有二个逗号。- 这些逗号规则同时应用到对象中的字段。
重复的键
JSON规范没有讲清在同一个对象中重复的键将被如何处理。在HOCON, 重复键中,后面出现的重写那些更早出现的, 除非两个值都是对象。如果两个值都是对象, 那么对象会合并。
注意: 如果你假定JSON需要重复键来实现一个行为,这会让 HOCON无法扩展JSON。这里的假设是,重复键是无效的JSON。
合并的对象:
- 在两个对象中不同的字段合并到新对象中。
- 对于在两个对象都有的非对象值字段,仅使用第二个对象中找到的。
- 对于在两个对象都有的对象值, 根据这些相同的对象合并规则以递归方式合并。
对象合并可以防止首先由设置键到另一个值。这是因为合并两个值总是做一次; 如果你设置一个键到一个对象, 一个非对象, 然后是一个对象, 每一个非对象回退到对象(非对象总是赢的), 然后对象回到非对象(不合并, 对象变成新值)。因此两个对象永远都看不到对方。
这二个是等价的:
{
"foo" : { "a" : 42 },
"foo" : { "b" : 43 }
}
{
"foo" : { "a" : 42, "b" : 43 }
}
这二个也是等价的:
{
"foo" : { "a" : 42 },
"foo" : null,
"foo" : { "b" : 43 }
}
{
"foo" : { "b" : 43 }
}
中间设置为null
的"foo"
值,是用来防止对象合并。
路径作为键
如果一个键是一种多元素的路径表达式, 它会为展开,并为路径每个元素创建一个对象,除了最后一个。最后一个路径元素, 结合值, 成为最后的嵌套对象的一个字段。
换言之:
foo.bar : 42
等价于:
foo { bar : 42 }
以及:
foo.bar.baz : 42
等价于:
foo { bar { baz : 42 } }
如此等等。这些值以通常的方式合并; 这意味着:
a.x : 42, a.y : 43
等价于:
a { x : 42, y : 43 }
因为路径表达式的工作方式就像值的串联, 你可以在值中留有空格:
a b c : 42
等价于:
"a b c" : 42
因为路径表达式总是转换为字符串, 甚至单个值,它通常会有另一种形式变成字符串。
true : 42
是"true" : 42
3.14 : 42
是"3.14" : 42
作为一种特殊的规则, 不带引号的字符串 include
不能在键的路径表达式, 因为它有特殊的解释(下面讲到)。
代入
代入是一个在配置树中引用其它部分的一种方式。
语法是${pathexpression}
或${?pathexpression}
,这里 pathexpression
就是上面讲到的路径表达式。这个路径表达式有相同的语法,你可以为对象键使用它。
在${?pathexpression}
中的?
前面必须没有空格; ${?
这三个字符必须就像一个组合一样在一起。
在配置树中没有找到的代入, 执行时可能会尝试通过查找系统环境变量或其它外部配置源来解析他们。(关于环境变量的细节在后面章节讲到)
代入不解析在引号里面的字符串。要想用一个字符串包含代入, 你必须使用代入和值串联并不加引号:
key : ${animal.favorite} is my favorite animal
或者你可以在非代入部分加引号:
key : ${animal.favorite}" is my favorite animal"
代入是通过查找配置中的路径来解析。路径从根配置对象开始,比如它是 “绝对” 的而不是 “相对”的。
代入处理是作为最后一步来执行, 所以代入可以在配置向前查找。如果一个配置由多个文件组成, 它甚至可以最终从另一个文件中获取值。如果一个键被多次指定, 代入总是等于它最后一次分配的值 (设为合并对象或最后的非对象值)。
如果一个配置设一个值为null
,那么它不会在外部源中查找。不幸的是这是最后的配置文件,没有“undo”的方式; 如果你在根对象中有{ "HOME" : null }
, 然后${HOME}
永远不会去查找环境变量。换言之这不等同于JavaScript的delete
操作。
如果一个代入不匹配目前配置中的任何值和不从外部源解析, 那么它就是未定义的。一个未定义的代入用${foo}
语法是无效的,并且会产生一个错误。
如果一个未定义的代入用${?foo}
语法:
- 如果它是一个对象字段的值,那么字段应该不会被创建。如果字段会覆盖之前相同字段设置的值, 那么原来的值仍然保留。
- 如果它是一个数组元素,那么元素不会被添加。
- 如果它是串联的值的一部分,那么它应该成为一个空字符串。
- 如果
bar
是未定义的,foo : ${?bar}
会避免创建字段foo
,但foo : ${?bar} ${?baz}
会是一个值串联,因此如果bar
或baz
是未定义的, 结果会是空字符串。
代入仅允许在对象字段值和数组元素(值串联)中使用, 他们不允许作为键或者在其它代入(路径表达式)中嵌套使用。
一个代入可以替换为任何类型的值(数字,对象,字符串, 数组, 布尔值, null)。如果代入是值仅有的一部分, 那么值类型会被保存。否则, 它是一个值串联,形成一个字符串。
循环的代入是无效的,并会产生一个错误。
实现时必须小心, 然而, 允许对象引用他们自己内部路径。举例, 这个可以工作:
bar : { foo : 42,
baz : ${bar.foo}
}
这里, 如果一个实现解析所有在 bar
中的代入作为解析代入${bar.foo}
的一部分, 这会成为一个循环。实现必须仅解析bar
中的字段foo
, 而不是重复整个bar
对象。
include
include的语法
一个include语句用不带引号的字符串include
并在后面马上跟着一个单引号的字符串。一个include语句可以出现在一个对象的字段中。
如果不带引号的字符串include
出现在对象键期望的路径表达式的开头,那么它不解析为一个路径表达式或键。
相反, 下一个值必须是一个带引号的字符串。带引号的字符串被解释为一个文件名或包含资源的名称。
放在一起, 就是不带引号的include
和带引号的字符串,而后者替换为对象字段, 以及从后面的对象字段隔开或者通过常用的逗号包含(如果有换行符,和往常一样可以省略逗号)。
如果一个不带引号的include
在键的开头并且后面跟着任何不是加引号的字符串, 它是无效的并会产生错误。
在不带引号的include
和带引号字符串之间,可以有任意数量的空格, 包括新行。
值串联不能作为include
的 “参数”执行。参数必须是加引号的字符串。不允许代入, 并且参数不是能一个不带引号字符串或任何其它类型的值。
如果不是在键的路径表达式的开头部分,不带引号的 include
没有什么特殊含义。
它可能在键的后面出现:
# 这是有效的
{ foo include : 42 }
# 等价于
{ "foo include" : 42 }
它可能作为一个对象或数组值出现:
{ foo : include } # 值是字符串 "include"
[ include ] # 一个字符串"include"的数组
仅仅不带引号的include
才是特殊的,如果你想一个键以单词"include"
开头,你可以用带引号的"include"
:
{ "include" : 42 }
include语义: 合并
一个include的文件中有include语句,并且一个include的文件是include语句中指定的一个。(他们不需要是文件系统上常规的文件, 但假设此刻他们是。)
一个include的文件必须有一个对象, 不是一个数组。这是很重要的,因为JSON和HOCON允许数组在文档中作为根值。
如果任何include文件包含一个数组作为根值, 它是无效的,并且会产生一个错误。
included文件应被解析, 产生一个根对象。从根来的键在概念上是在include的文件中代入include语句。
- 如果一个在包含的对象中的键是在包含对象中的include语句之前的, 包含进来的键的值会被重写,或合并早期的值, 正如与在单个文件中发现重复键。
- 如果包含文件从先前包含的对象得到重复键, 包含文件的值会重写或和包含进来的文件合并。
include语义: 代入
在包含文件中的代入是按两个不同的路径查找; 第一种是相对于被包含文件的根; 第一种是相对于被包含的配置的根。
记得代入发生在最后一步, 是在解析后。它在整个app的配置解析后再做, 不是为分离的单个文件。
因此, 如果一个included文件中有代入, 他们必须 “修复” 以相对于app的配置根。
例如说,这是根配置:
{ a : { include "foo.conf" } }
而 “foo.conf” 会是这样的:
{ x : 10, y : ${x} }
如果你孤立地解析“foo.conf” , 那么${x}
会等于10, 即路径中的x
的值。如果你include “foo.conf” 到另一个对象的键a
, 然而, 它必须被修复的${a.x}
而不是${x}
。
假如根配置重定义a.x
, 像这样:
{
a : { include "foo.conf" }
a : { x : 42 }
}
那么在 “foo.conf”中的${x}
, 已修复到 ${a.x}
, 会等于42
而不是10
。代入发生在解析整个配置之后。
然而, 很多情况下包含的文件可能打算引用应用程序的根配置。举例, 要从系统属性获得一个值或从参考配置。所以仅查找“修复”路径是不够的, 它还必须查找原始的路径。
Include语义: 丢失的文件
如果一个包含的文件不存在, include语句会静默地忽略 (就当做包含文件仅包含一个空对象)。
Include语义: 定位资源
从概念上讲, 在include语句中加引号的字符串标识一个文件或其它资源 “adjacent to” 一个开始解析,以及作为一个相同类型的解析。 “adjacent to”的意思是, 加上字符串本身, 必须为每个资源的类型单独指定。
支持includ的实现资源的种类可能会有所不同。
在Java 虚拟机上, 如果一个include语句不能标识任何 “adjacent to” 包含资源, 实现时可能希望回退到类路径资源。这允许配置在文件或URLs中查找,以访问类路径资源。
对于资源定位到Java类路径:
- 包含的资源通过调用
getResource()
查找,在同一个类加载器用于查找包含的资源。 - 如果包含的资源名称是绝对的(以‘/’开始),那么它应该被传递到
getResource()
并移除‘/’。 - 如果包含的资源的名称不是以‘/’开头,那么应该有包含资源的 “目录”。在传递到
getResource()
中之前把它附加上去, 如果包含资源不是绝对的(没有 ‘/’) 和没有“父目录” (只是一个单路径元素), 那么包含相对的资源名称会保留原样。 - 使用
getResource()
来获得一个URL然后定位包含文件名相对于这个URL是错误的, 因为一个类加载器不需要在它的URLs中的路径及它在getResource()
中处理的路径之间有一对一的映射关系。换言之, “adjacent to”运算应该由资源的名称而不是资源的URL完成。
对于文件系统中的普通文件:
- 如果包含文件是一个绝对路径,那么它应保持绝对的,以这样的方式加载。
- 如果包含文件是相对路径, 那么它应该相对定位到包含的文件的那个目录。当解析包含路径时,处理解析文件的当前工作目录必须不被使用。
- 如果文件没有找到, 回退到类路径资源。类路径资源前面不能添加任何包名, 它应该相对到 “根”; 意思是任何 “/” 应该被删除 (绝对和相对是一样的,因为它是相对于根的)。 “/” 为处理包含从其它类路径资源内的资源的一致性, 资源名称不能是相对于根的,而 “/” 允许指定相对于根。
对于URLs:
- 对于文件系统和Java资源, 如果包含的名称是一个URL (以协议开头), 它是合理的行为,会尝试加载URL而不是作为文件或资源的名称处理。
- 对于从URL加载一个文件, “adjacent to” 应该基于解析URL的路径组件, 替换最后一个路径元素为包含的名称。
- file: URLs 这个应该行为和作为一个普通文件名一样
持续时间的格式
受支持的持续时间的字符串单位是区分大小写的,并且必须是小写。完全支持这些字符串:
ns
,nanosecond
,nanoseconds
us
,microsecond
,microseconds
ms
,millisecond
,milliseconds
s
,second
,seconds
m
,minute
,minutes
h
,hour
,hours
d
,day
,days
以字节表示的大小格式
对于单字节, 完全支持这些字符串:
B
,b
,byte
,bytes
对于十次方的, 完全支持这些字符串:
kB
,kilobyte
,kilobytes
MB
,megabyte
,megabytes
GB
,gigabyte
,gigabytes
TB
,terabyte
,terabytes
PB
,petabyte
,petabytes
EB
,exabyte
,exabytes
ZB
,zettabyte
,zettabytes
YB
,yottabyte
,yottabytes
对于二次方的, 完全支持这些字符串:
K
,k
,Ki
,KiB
,kibibyte
,kibibytes
M
,m
,Mi
,MiB
,mebibyte
,mebibytes
G
,g
,Gi
,GiB
,gibibyte
,gibibytes
T
,t
,Ti
,TiB
,tebibyte
,tebibytes
P
,p
,Pi
,PiB
,pebibyte
,pebibytes
E
,e
,Ei
,EiB
,exbibyte
,exbibytes
Z
,z
,Zi
,ZiB
,zebibyte
,zebibytes
Y
,y
,Yi
,YiB
,yobibyte
,yobibytes
通过系统属性的常规覆盖
Java 系统属性重写在application.conf
和reference.conf
文件中找到的设置。这支持在命令行上指定配置选项。比如 play -Dkey=value run
注意 : Play forks JVM 以测试 - 因此在你可以使用他们测试之前,测试中使用命令行重写你必须添加 Keys.fork in Test := false
到build.sbt
中。