7 应用程序
本节应与内核中的 app(4) 和 application(3) 手册页一起阅读。
7.1 应用程序概念
当你编写了实现某些特定功能的代码时,你可能希望将代码变成一个应用程序,即可以作为一个单元启动和停止的组件,并且也可以在其他系统中重复使用。
为此,请创建一个应用程序回调模块,并描述如何启动和停止应用程序。
然后,需要一个应用程序规范,它被放在一个应用程序资源文件中。除其他事项外,此文件指定应用程序包含哪些模块以及回调模块的名称。
如果你使用 systools(Erlang/OTP 用于打包代码的工具,请参见 发布),则每个应用程序的代码将放置在遵循预定义目录结构的单独目录中。
7.2 应用程序回调模块
如何启动和停止应用程序的代码(即监督树),由两个回调函数描述
start(StartType, StartArgs) -> {ok, Pid} | {ok, Pid, State} stop(State)
- start 在启动应用程序时被调用,用于通过启动顶级监督者来创建监督树。它应该返回顶级监督者的 pid 和一个可选的项 State,该项默认为 []。此项将按原样传递给 stop。
- StartType 通常是原子 normal。它只有在接管或故障转移的情况下才具有其他值,请参见 分布式应用程序。
- StartArgs 由 应用程序资源文件 中的键 mod 定义。
- stop/1 在应用程序停止后被调用,用于执行任何必要的清理工作。应用程序的实际停止(即监督树的关闭)将如 启动和停止应用程序 中所述自动处理。
来自 监督者行为 的监督树的应用程序回调模块示例
-module(ch_app). -behaviour(application). -export([start/2, stop/1]). start(_Type, _Args) -> ch_sup:start_link(). stop(_State) -> ok.
无法启动或停止的库应用程序不需要任何应用程序回调模块。
7.3 应用程序资源文件
要定义应用程序,请创建一个应用程序规范,并将其放在一个应用程序资源文件中,或简称为 .app 文件
{application, Application, [Opt1,...,OptN]}.
- Application(一个原子)是应用程序的名称。该文件必须命名为 Application.app。
- 每个 Opt 都是一个元组 {Key,Value},它定义了应用程序的某个属性。所有键都是可选的。对于任何省略的键,都使用默认值。
库应用程序 libapp 的最小 .app 文件的内容如下所示
{application, libapp, []}.
类似 ch_app 的监督树应用程序的最小 .app 文件 ch_app.app 的内容如下所示
{application, ch_app, [{mod, {ch_app,[]}}]}.
键 mod 定义了应用程序的回调模块和启动参数,在本例中分别为 ch_app 和 []。这意味着在启动应用程序时,将调用以下内容
ch_app:start(normal, [])
在停止应用程序时,将调用以下内容。
ch_app:stop([])
当使用 systools(Erlang/OTP 用于打包代码的工具,请参见第 发布 节)时,还需要指定键 description、vsn、modules、registered 和 applications
{application, ch_app, [{description, "Channel allocator"}, {vsn, "1"}, {modules, [ch_app, ch_sup, ch3]}, {registered, [ch3]}, {applications, [kernel, stdlib, sasl]}, {mod, {ch_app,[]}} ]}.
- description - 简短描述,一个字符串。默认为 ""。
- vsn - 版本号,一个字符串。默认为 ""。
- modules - 此应用程序引入的所有模块。 systools 在生成引导脚本和 tar 文件时使用此列表。一个模块必须只在一个应用程序中定义。默认为 []。
- registered - 应用程序中所有注册进程的名称。 systools 使用此列表来检测应用程序之间的名称冲突。默认为 []。
- applications - 在启动此应用程序之前必须启动的所有应用程序。 systools 使用此列表来生成正确的引导脚本。默认为 []。请注意,所有应用程序都至少依赖于 Kernel 和 STDLIB。
有关应用程序资源文件语法和内容的详细信息,请参见内核中的 app 手册页。
7.4 目录结构
当使用 systools 打包代码时,每个应用程序的代码都将放置在单独的目录 lib/Application-Vsn 中,其中 Vsn 是版本号。
即使没有使用 systools,了解这一点也很有用,因为 Erlang/OTP 是根据 OTP 原则打包的,因此附带特定的目录结构。代码服务器(请参见内核中的 code(3) 手册页)如果存在多个版本的应用程序,则会自动使用版本号最高的目录中的代码。
开发环境的目录结构指南
只要发布的目录结构符合以下描述,任何开发目录结构都可以,但建议在开发环境中也使用相同的目录结构。应省略应用程序目录名称中的版本号,因为这是发布步骤的产物。
一些子目录是必需的。一些子目录是可选的,这意味着只有当应用程序本身需要它时才应使用它。最后,一些子目录是推荐的,这意味着建议使用它并按此处所述使用它。例如,为了使应用程序成为一个合格的 OTP 应用程序,鼓励应用程序同时拥有文档和测试。
─ ${application} ├── doc │ ├── internal │ ├── examples │ └── src ├── include ├── priv ├── src │ └── ${application}.app.src └── test
- src - 必需。包含 Erlang 源代码、.app 文件的源代码以及应用程序本身使用的内部包含文件。 src 内部的其他子目录可以用作命名空间来组织源文件。这些目录的深度永远不应超过一层。
- priv - 可选。用于应用程序特定文件。
- include - 可选。用于必须从其他应用程序访问的公共包含文件。
- doc - 推荐。任何源文档都应放置在此处的子目录中。
- doc/internal - 推荐。任何描述有关此应用程序的实现细节的文档(不打算发布)都应放置在此处。
- doc/examples - 推荐。有关如何使用此应用程序的示例的源代码应放置在此处。建议从该目录中的公共文档为示例提供源代码。
- doc/src - 推荐。所有文档的源文件,例如 Markdown、AsciiDoc 或 XML 文件,都应放置在此处。
- test - 推荐。所有与测试相关的文件,例如测试套件和测试规范,都应放置在此处。
开发环境中可能需要其他目录。 例如,如果使用除 Erlang 以外的语言的源代码(例如,用于 NIF 的 C 代码),则该代码应放在单独的目录中。 按照惯例,建议在这些目录前面加上语言名称,例如 c_src 用于 C,java_src 用于 Java 或 go_src 用于 Go。 具有 _src 后缀的目录表示它是应用程序的一部分,并且是编译步骤。 最终的构建工件应针对 priv/lib 或 priv/bin 目录。
priv 目录包含应用程序在运行时需要的资产。 可执行文件应驻留在 priv/bin 中,动态链接库应驻留在 priv/lib 中。 其他资产可以自由地驻留在 priv 目录中,但建议以结构化的方式进行。
从其他语言生成的 Erlang 代码的源文件,例如 ASN.1 或 Mibs,应放置在目录中,在顶层或在 src 中,名称与源语言相同,例如 asn1 和 mibs。 构建工件应放置在它们各自的语言目录中,例如 Erlang 代码的 src 或 Java 代码的 java_src。
用于发布的 .app 文件可能驻留在开发环境中的 ebin 目录中,但鼓励将其作为构建步骤的工件。 按照惯例,使用 .app.src 文件,它驻留在 src 目录中。 此文件与 .app 文件几乎相同,但在构建步骤中某些字段可能会被替换,例如应用程序版本。
目录名称不应大写。
鼓励省略空目录。
已发布系统的目录结构
已发布的应用程序必须遵循一定的结构。
─ ${application}-${version} ├── bin ├── doc │ ├── html │ ├── man[1-9] │ ├── pdf │ ├── internal │ └── examples ├── ebin │ └── ${application}.app ├── include ├── priv │ ├── lib │ └── bin └── src
- src - 可选。 包含应用程序本身使用的 Erlang 源代码和内部包含文件。 此目录在已发布的应用程序中不再需要。
- ebin - 必需。 包含 Erlang 对象代码,beam 文件。 .app 文件也必须放在这里。
- priv - 可选。 用于应用程序特定文件。 code:priv_dir/1 用于访问此目录。
- priv/lib - 建议。 应用程序使用的任何共享对象文件(例如 NIF 或链接驱动程序)应放在这里。
- priv/bin - 建议。 应用程序使用的任何可执行文件(例如端口程序)应放在这里。
- include - 可选。用于必须从其他应用程序访问的公共包含文件。
- bin - 可选。 应用程序生成的任何可执行文件(例如 escript 或 shell 脚本)应放在这里。
- doc - 可选。 任何已发布的文档都应放在此处的子目录中。
- doc/man1 - 建议。 应用程序可执行文件的帮助页面。
- doc/man3 - 建议。 模块 API 的帮助页面。
- doc/man6 - 建议。 应用程序概述的帮助页面。
- doc/html - 可选。 整个应用程序的 HTML 页面。
- doc/pdf - 可选。 整个应用程序的 PDF 文档。
src 目录可能对调试目的有用,但不是必需的。 include 目录仅在应用程序具有公共包含文件时才发布。
建议以这种方式发布的唯一文档是帮助页面。 HTML 和 PDF 通常会以其他方式分发。
鼓励省略空目录。
7.5 应用程序控制器
启动 Erlang 运行时系统时,内核应用程序会启动许多进程。 其中一个进程是注册为 application_controller 的 **应用程序控制器** 进程。
应用程序控制器协调所有应用程序操作。 它通过模块 application 中的函数进行交互,请参阅内核中的 application(3) 手册页。 特别是,应用程序可以加载、卸载、启动和停止。
7.6 加载和卸载应用程序
在启动应用程序之前,必须 **加载** 它。 应用程序控制器从 .app 文件中读取并存储信息
1> application:load(ch_app). ok 2> application:loaded_applications(). [{kernel,"ERTS CXC 138 10","2.8.1.3"}, {stdlib,"ERTS CXC 138 10","1.11.4.3"}, {ch_app,"Channel allocator","1"}]
已停止或从未启动的应用程序可以卸载。 关于应用程序的信息将从应用程序控制器的内部数据库中删除。
3> application:unload(ch_app). ok 4> application:loaded_applications(). [{kernel,"ERTS CXC 138 10","2.8.1.3"}, {stdlib,"ERTS CXC 138 10","1.11.4.3"}]
加载/卸载应用程序不会加载/卸载应用程序使用的代码。 代码加载以通常的方式进行。
7.7 启动和停止应用程序
通过调用以下命令启动应用程序
5> application:start(ch_app). ok 6> application:which_applications(). [{kernel,"ERTS CXC 138 10","2.8.1.3"}, {stdlib,"ERTS CXC 138 10","1.11.4.3"}, {ch_app,"Channel allocator","1"}]
如果应用程序尚未加载,应用程序控制器将首先使用 application:load/1 加载它。 它检查 applications 键的值,以确保在此应用程序之前要启动的所有应用程序都在运行。
然后,应用程序控制器为该应用程序创建一个 **应用程序主控**。 应用程序主控成为应用程序中所有进程的组领导者。 不过,I/O 会转发到之前的组领导者,这只是识别属于应用程序的进程的一种方式。 例如,用于从任何进程中找到自己,或者反过来,在它终止时杀死它们。
应用程序主控通过调用应用程序回调函数 start/2(在模块中,并使用由 .app 文件中的 mod 键定义的启动参数)来启动应用程序。
应用程序被停止,但不会卸载,通过调用以下命令
7> application:stop(ch_app).
ok
应用程序主控通过告知顶层监管者关闭来停止应用程序。 顶层监管者告知其所有子进程关闭,依此类推;整个树按照相反的启动顺序终止。 然后,应用程序主控调用模块中定义的应用程序回调函数 stop/1,该模块由 mod 键定义。
7.8 配置应用程序
可以使用 **配置参数** 配置应用程序。 这些参数是由 .app 文件中的键 env 指定的 {Par,Val} 元组列表
{application, ch_app, [{description, "Channel allocator"}, {vsn, "1"}, {modules, [ch_app, ch_sup, ch3]}, {registered, [ch3]}, {applications, [kernel, stdlib, sasl]}, {mod, {ch_app,[]}}, {env, [{file, "/usr/local/log"}]} ]}.
Par 必须是原子。 Val 是任何项。 应用程序可以通过调用 application:get_env(App, Par) 或许多类似函数来检索配置参数的值,请参阅内核中的 application(3) 手册页。
示例
% erl Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0] Eshell V5.2.3.6 (abort with ^G) 1> application:start(ch_app). ok 2> application:get_env(ch_app, file). {ok,"/usr/local/log"}
.app 文件中的值可以被 **系统配置文件** 中的值覆盖。 此文件包含相关应用程序的配置参数
[{Application1, [{Par11,Val11},...]}, ..., {ApplicationN, [{ParN1,ValN1},...]}].
系统配置应命名为 Name.config,Erlang 应使用命令行参数 -config Name 启动。 有关详细信息,请参阅内核中的 config(4) 手册页。
示例
使用以下内容创建一个文件 test.config
[{ch_app, [{file, "testlog"}]}].
file 的值将覆盖在 .app 文件中定义的 file 的值
% erl -config test Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0] Eshell V5.2.3.6 (abort with ^G) 1> application:start(ch_app). ok 2> application:get_env(ch_app, file). {ok,"testlog"}
如果使用 发布处理,则应使用一个系统配置文件,该文件应命名为 sys.config。
.app 文件中的值和系统配置文件中的值可以直接从命令行覆盖
% erl -ApplName Par1 Val1 ... ParN ValN
示例
% erl -ch_app file '"testlog"' Erlang (BEAM) emulator version 5.2.3.6 [hipe] [threads:0] Eshell V5.2.3.6 (abort with ^G) 1> application:start(ch_app). ok 2> application:get_env(ch_app, file). {ok,"testlog"}
7.9 应用程序启动类型
启动应用程序时定义 **启动类型**
application:start(Application, Type)
application:start(Application) 等同于调用 application:start(Application, temporary)。 该类型也可以是 permanent 或 transient
- 如果永久性应用程序终止,所有其他应用程序和运行时系统也会终止。
- 如果瞬态应用程序以原因 normal 终止,这将被报告,但不会终止其他应用程序。 如果瞬态应用程序异常终止,即以除 normal 之外的任何其他原因终止,所有其他应用程序和运行时系统也会终止。
- 如果临时应用程序终止,这将被报告,但不会终止其他应用程序。
始终可以通过调用 application:stop/1 显式停止应用程序。 无论模式如何,都不会影响其他应用程序。
瞬态模式几乎没有实际用处,因为监管树终止时,原因将设置为 shutdown,而不是 normal。