Elixir实战:11 组件工作 (1) OTP 应用程序
zhezhongyun 2025-01-05 21:29 87 浏览
本章涵盖
- 创建 OTP 应用程序
- 处理依赖关系
- 构建一个网络服务器
- 配置应用程序
是时候将我们的注意力转向生产可发布的系统,这些系统可以被部署。为了实现这个目标,您需要了解 OTP 应用程序,它们可以让您将系统组织成可重用的组件。应用程序是构建 Elixir 或 Erlang 生产系统和库的标准方式。依赖它们带来了各种好处,例如依赖管理;简化系统启动;以及构建独立的、可部署的发布版本的能力。
在本章中,您将学习如何创建应用程序并处理依赖关系。在此过程中,您将把待办事项系统转变为一个正式的 OTP 应用程序,并使用来自 Erlang 和 Elixir 生态系统的一些第三方库,为您现有的系统暴露一个 HTTP 接口。前方还有很多工作,所以让我们开始进行 OTP 应用程序的创建。
11.1 OTP 应用程序
OTP 应用程序是由多个模块组成的组件,并且可以依赖其他应用程序。这使得通过单个函数调用启动整个系统成为可能。正如你将要看到的,将代码转换为应用程序是相当简单的。你当前版本的待办事项系统已经是一个 OTP 应用程序,但还有一些小细节可以改进。你很快就会看到这一点;首先,让我们看看 OTP 应用程序由什么组成。
11.1.1 使用混合工具创建应用程序
应用程序是一个特定于 OTP 的构造。定义应用程序的资源称为应用程序资源文件——一个用 Erlang 术语编写的纯文本文件,描述了该应用程序。(不用担心;您不需要直接编写这个。您将依赖 mix 工具为您完成这项工作)。该文件包含几条信息,例如:
- 应用名称和版本,以及描述
- 应用模块列表
- 应用依赖项列表(这些必须是应用程序本身)
- 可选的应用回调模块
依赖于 mix 工具简化和自动化了生成应用程序资源文件的部分工作。例如,应用程序资源文件必须包含所有应用程序模块的列表。当您使用 mix 时,此列表会根据源代码中定义的模块自动为您生成。
某些信息,例如应用名称、版本和描述,当然必须由您提供。然后, mix 工具可以使用这些数据和您的源代码在编译项目时生成相应的资源文件。
让我们看看这个在实践中的应用。去一个临时文件夹,运行 mix new hello_world --sup 。这个命令创建了一个 hello_world 文件夹,里面有最小的 Mix 项目骨架。参数 --sup 使得 mix 工具生成应用回调模块,并从中启动空的(无子进程的)监督者。
您现在可以切换到 hello_world 文件夹,并使用熟悉的 iex -S mix 启动系统。表面上看,似乎没有什么特别的事情发生。但是 mix 会自动启动您的应用程序,您可以通过调用 Application.started_applications/0 来验证:
iex(1)> Application.started_applications()
[
{:iex, ~c"iex", ~c"1.15.0"},
{:hello_world, ~c"hello_world", ~c"0.1.0"}, ?
{:logger, ~c"logger", ~c"1.15.0"},
{:mix, ~c"mix", ~c"1.15.0"},
{:elixir, ~c"elixir", ~c"1.15.0"},
{:compiler, ~c"ERTS CXC 138 10", ~c"8.3"},
{:stdlib, ~c"ERTS CXC 138 10", ~c"5.0"},
{:kernel, ~c"ERTS CXC 138 10", ~c"9.0"}
]应用程序正在运行。
如您所见, hello_world 应用程序正在运行,同时还有一些其他应用程序,例如 Elixir 的 mix 、 iex 和 elixir ,以及 Erlang 的 stdlib 和 kernel 。
您很快就会看到这的好处,但首先,让我们看看应用程序是如何描述的。您指定应用程序的主要位置是在 mix.exs 文件中。以下是生成文件的完整内容(注释已被删除):
defmodule HelloWorld.MixProject do
use Mix.Project
def project do ?
[ ?
app: :hello_world, ?
version: "0.1.0", ?
elixir: "~> 1.15", ?
start_permanent: Mix.env() == :prod, ?
deps: deps() ?
] ?
end ?
def application do ?
[ ?
extra_applications: [:logger], ?
mod: {HelloWorld.Application, []} ?
] ?
end ?
defp deps do ?
[] ?
end ?
end? 描述项目
? 描述应用程序
? 列出依赖项
第一个有趣的事情发生在 project/0 函数中,在这里你描述了 Mix 项目。 app: :hello_world 为你的应用程序命名。应用程序名称只能是一个原子,你可以使用这个原子在运行时启动和停止应用程序。
该应用程序在 application/0 函数中描述。在这里,您指定一些选项,这些选项最终将进入应用程序资源文件。在这种情况下,描述包括您依赖的其他 Erlang 和 Elixir 应用程序的列表,以及将用于启动应用程序的模块。默认情况下,Elixir 的 :logger 应用程序被列出(https://hexdocs.pm/logger/Logger.xhtml)。
最后, deps 函数返回第三方依赖项的列表——您希望在项目中使用的其他库。默认情况下,此列表为空。您将在本章稍后看到如何使用依赖项。
11.1.2 应用程序行为
应用程序描述的关键部分是 mod: {HelloWorld.Application, []} ,由 application/0 在 mix.exs 中提供。该部分指定将用于启动应用程序的模块。当应用程序启动时,将调用函数 HelloWorld.Application.start/2 。
显然,您需要实现 HelloWorld.Application 模块。这是由 mix 工具为您完成的,所以让我们看看它的样子:
defmodule HelloWorld.Application do
use Application ?
def start(_type, _args) do ?
children = [] ?
opts = [strategy: :one_for_one, name: HelloWorld.Supervisor] ?
Supervisor.start_link(children, opts) ?
end
end? 使用应用程序模块
? 回调函数
? 启动顶级监督者
应用程序是一个由 Application 模块(https://hexdocs.pm/elixir/Application.xhtml)驱动的 OTP 行为,它是 Erlang 的 :application 模块(https://www.erlang.org/doc/man/application.xhtml)的一层封装。要能够与 Application 一起工作,您必须实现自己的回调模块并定义一些回调函数。
至少,您的回调模块必须包含 start/2 函数。传递的参数是应用程序启动类型(您通常会忽略)和一个任意参数(在 mix.exs 中的 mod 键下指定的术语)。有关详细信息,请参阅官方文档 (https://hexdocs.pm/elixir/Application.xhtml#c:start/2)。
start/2 回调的任务是启动您系统的顶级进程,这通常应该是一个监督者。如果出现问题,函数将以 {:ok, pid} 或 {:error, reason} 的形式返回其结果。
11.1.3 启动应用程序
要在运行的 BEAM 实例中启动应用程序,可以调用 Application .start/1 。此函数首先查找应用程序资源文件(由 mix 生成)并解释其内容。然后,它验证您所依赖的所有应用程序是否已启动。最后,通过调用回调模块的 start/2 函数来启动应用程序。 Application.ensure_all_started/2 函数也可用,它递归启动所有尚未启动的依赖项。
通常,您不需要调用这些函数,因为 mix 会自动启动项目实现的应用程序。调用 iex -S mix 会自动启动应用程序及其依赖项。
请注意,您无法启动单个应用程序的多个实例。尝试启动已在运行的应用程序将返回错误:
$ iex -S mix
iex(1)> Application.start(:hello_world)
{:error, {:already_started, :hello_world}}
您可以使用 Application.stop/1 停止应用程序:
iex(2)> Application.stop(:hello_world)
[info] Application hello_world exited: :stopped停止应用程序会终止其顶级进程。如果该进程是一个监督者,它将在停止自身之前停止其子进程。这就是为什么在监督树中组织进程很重要,如第 9 章所述。这样可以确保应用程序以受控的方式停止,避免留下悬挂的进程。
Application.stop/1 仅停止指定的应用程序,保留依赖项(其他应用程序)运行。要以受控方式停止整个系统,您可以调用 System.stop/0 。此功能将关闭所有 OTP 应用程序,然后关闭 BEAM 实例。 Application.stop/1 和 System.stop/0 都以礼貌的方式工作。监督树中的每个进程都可以在其 terminate/2 回调中执行一些最终清理,如第 9.1.6 节所述。
11.1.4 库应用程序
您不需要在 mix.exs 中提供 application/0 函数的 mod: ... 选项:
defmodule HelloWorld.Application do
...
def application do
[]
end
...
end在这种情况下,没有应用回调模块,这反过来意味着没有顶级进程可以启动。这仍然是一个合适的 OTP 应用程序。您甚至可以启动和停止它。
这样的应用程序有什么目的?该技术用于库应用程序——不需要创建自己监督树的组件。顾名思义,这些通常是库,例如 JSON 或 CSV 解析器。Erlang 自己的 STDLIB 应用程序(https://erlang.org/doc/apps/stdlib/index.xhtml)是一个纯库应用程序,因为它暴露了各种实用模块,但不需要管理自己的监督树。
库应用程序很有用,因为您可以将它们列为运行时依赖项。这在您开始组装可部署版本时起着重要作用,正如您将在第 13 章中看到的。
11.1.5 实现应用程序回调
凭借这些知识,您可以将您的待办事项系统转变为一个正式的应用程序。如前所述,它已经是一个 OTP 应用程序,尽管是一个库,它并没有为应用程序行为实现回调模块。实际上,Mix 项目与 OTP 应用程序之间通常存在 1:1 的关系。一个 Mix 项目实现恰好一个 OTP 应用程序。一个例外是所谓的伞形项目,它可以包含多个 OTP 应用程序。
考虑到待办系统在一个监督树下运行一组自己的进程,实现应用程序回调模块是有意义的。一旦您这样做,系统可以在您运行 iex -S mix 时自动启动。
您需要做的第一件事是编辑 mix.exs 文件。回想一下,您在第 7 章中使用 mix 工具创建了初始项目。因此,您已经有了这个文件,只需添加一些信息即可。mix.exs 的完整代码在以下列表中提供。
清单 11.1 指定应用程序参数 (todo_app/mix.exs)
defmodule Todo.MixProject do
use Mix.Project
def project do
[
app: :todo,
version: "0.1.0",
elixir: "~> 1.15",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
def application do
[
extra_applications: [:logger],
mod: {Todo.Application, []} ?
]
end
defp deps do
[]
end
end? 指定应用回调模块
对 mix.exs 的唯一更改是在 application/0 函数中,指定了回调模块。
接下来,您需要实现回调模块。代码如下。
清单 11.2 实现应用模块 (todo_app/lib/todo/application.ex)
defmodule Todo.Application do
use Application
def start(_, _) do
Todo.System.start_link()
end
end如前所述,启动应用程序就像启动顶级监督者一样简单。鉴于您已经将系统结构设置为位于 Todo.System 监督者之下,这就是将您的系统转变为完整 OTP 应用程序所需的一切。
让我们看看它的实际效果:
$ iex -S mix
Starting database server ?
Starting database worker ?
Starting database worker ?
Starting database worker ?
Starting to-do cache ?系统自动启动。
通过实现 OTP 应用程序回调,您已使系统能够自动启动。
值得注意的是,在运行测试时您也会获得相同的好处。当您调用 mix test 时,系统中的所有基本进程都会启动。在第 7 章中,当您添加了一些测试时,您必须手动启动缓存进程:
defmodule Todo.CacheTest do
use ExUnit.Case
test "server_process" do
{:ok, cache} = Todo.Cache.start() ?
bob_pid = Todo.Cache.server_process(cache, "bob")
assert bob_pid != Todo.Cache.server_process("alice")
assert bob_pid == Todo.Cache.server_process("bob")
end
...
end手动启动缓存
代码自那时起经历了许多变革,但在每个版本中,都有前述模式的某种变体。在 Todo.CacheTest 内,您需要手动启动支持进程,例如缓存。随着系统转变为一个真正的 OTP 应用,这种情况不再存在,如下列表所示。
清单 11.3 测试 server_process (todo_app/test/todo_cache_test.exs)
defmodule Todo.CacheTest do
use ExUnit.Case
test "server_process" do
bob_pid = Todo.Cache.server_process("bob") ?
assert bob_pid != Todo.Cache.server_process("alice")
assert bob_pid == Todo.Cache.server_process("bob")
end
...
end? 无需手动启动缓存即可工作
11.1.6 应用程序文件夹结构
让我们简要讨论一下您编译的应用程序的文件夹结构。由于 mix 工具,您通常不需要担心这个,但有时了解编译系统的文件夹结构是有用的。
混合环境
在查看应用程序结构之前,您应该了解一些关于 Mix 环境的知识。Mix 环境是一种编译时选项,可以用来影响编译代码的形状。
混合项目使用三个环境:开发(dev)、测试(test)和生产(prod)。这三个环境在编译代码时会产生细微的差异。例如,在为开发环境编译的版本中(dev 环境),您可能希望运行一些额外的调试日志,而在为生产环境编译的版本中(prod 环境),您不希望包含这样的日志。在为测试环境编译的版本中(test 环境),您希望进一步减少日志量,并使用不同的数据库,以防止测试污染您的开发数据库。
您可以根据需要引入自己的 Mix 环境,但这通常不必要,甚至可以说从未需要。这里提到的三种环境应该足以涵盖所有可能的场景。
对于大多数 mix 任务,默认环境是 dev,表示您正在处理开发。这个规则的一个例外是测试任务。当您调用 mix test 时,Mix 环境会自动设置为 test 。
您可以通过设置 MIX_ENV 操作系统环境变量来指定 Mix 环境。根据惯例,在为生产环境构建时,您应该使用 prod 环境。要为 prod 编译代码,您可以调用 MIX_ENV=prod mix compile 。要编译并启动 prod 版本,您可以调用 MIX_ENV=prod iex -S mix 。
生产环境常常是一个令人困惑的来源。许多团队将生产环境与生产机器混为一谈。生产版本确实运行在生产机器上,但它也会在其他机器上运行,例如预发布环境。最重要的是,应该能够并且简单地在本地开发机器上启动为生产编译的版本。你不需要频繁这样做,但偶尔分析与已部署的生产版本尽可能接近的系统行为是有用的。这正是生产编译版本的意义。因此,建议支持在本地运行生产编译版本。从项目开始时就致力于这个目标,因为在代码库显著增长后再去做通常会困难得多。
编译代码结构
一旦您编译项目,编译后的二进制文件将位于_build/project_env 文件夹中,其中 project_env 是编译期间生效的 Mix 环境。
因为 dev 是默认环境,如果你运行 mix compile 或 iex -S mix ,你会在 _build/dev 文件夹中得到二进制文件。OTP 本身推荐以下文件夹约定:
lib/
App1/
ebin/
priv/
App2/
ebin/
priv/
...在这里, App1 和 App2 代表应用程序名称(例如 todo )。ebin 文件夹包含编译后的二进制文件(.beam 文件和应用程序资源文件),而 priv 文件夹包含特定于应用程序的私有文件(图像、编译的 C 二进制文件等)。这不是一个强制性的结构,但它是大多数 Elixir 和 Erlang 项目中使用的约定。一些工具可能依赖于此结构,因此最好遵循这一约定。
幸运的是,您不需要自己维护这个结构,因为 mix 会自动完成。编译后的 Mix 项目的最终文件夹结构如下:
YourProjectFolder
_build
dev
lib
App1
ebin
priv
App2
...除了您的应用程序,lib 文件夹还包含应用程序依赖项,标准应用程序除外,这些标准应用程序包含在 Elixir 和 Erlang 中,位于 Elixir 和 Erlang 安装的文件夹中,并可以通过加载路径访问。
如前所述,应用程序资源文件位于 lib/YourApp/ebin,文件名为 YourApp.app。对于待办事项系统,该文件位于 _build/dev/lib/todo/ebin/(相对于根项目文件夹)。当您尝试启动应用程序时,通用应用程序行为会在加载路径中查找资源文件(与搜索编译二进制文件的路径相同)。
这结束了我们对应用基础的讨论。现在你已经掌握了一些理论,让我们来看看如何处理依赖关系。
可部署系统
应用程序在构建可部署系统中发挥着重要作用。您将在第 13 章中了解这一点,我们将讨论 OTP 发布。简而言之,基本思想是组装一个最小的自包含系统,仅包含所需的应用程序和 Erlang 运行时。为了实现这一点,您必须将代码转换为 OTP 应用程序,因为只有这样您才能指定对其他应用程序的依赖关系。这最终用于将所有所需的应用程序打包成一个单一的可部署发布。
因此,Elixir 和 Erlang 中的任何可重用内容都应该位于一个应用程序中。这对于 Elixir 和 Erlang 也是如此:例如 elixir 应用程序,它将 Elixir 标准库与 iex 和 mix 打包在一起,这些都是作为单独的应用程序实现的。在 Erlang 中也发生了同样的事情,它被划分为许多应用程序(https://www.erlang.org/doc/applications.xhtml),例如 kernel 和 stdlib 。
相关推荐
- Python入门学习记录之一:变量_python怎么用变量
-
写这个,主要是对自己学习python知识的一个总结,也是加深自己的印象。变量(英文:variable),也叫标识符。在python中,变量的命名规则有以下三点:>变量名只能包含字母、数字和下划线...
- python变量命名规则——来自小白的总结
-
python是一个动态编译类编程语言,所以程序在运行前不需要如C语言的先行编译动作,因此也只有在程序运行过程中才能发现程序的问题。基于此,python的变量就有一定的命名规范。python作为当前热门...
- Python入门学习教程:第 2 章 变量与数据类型
-
2.1什么是变量?在编程中,变量就像一个存放数据的容器,它可以存储各种信息,并且这些信息可以被读取和修改。想象一下,变量就如同我们生活中的盒子,你可以把东西放进去,也可以随时拿出来看看,甚至可以换成...
- 绘制学术论文中的“三线表”具体指导
-
在科研过程中,大家用到最多的可能就是“三线表”。“三线表”,一般主要由三条横线构成,当然在变量名栏里也可以拆分单元格,出现更多的线。更重要的是,“三线表”也是一种数据记录规范,以“三线表”形式记录的数...
- Python基础语法知识--变量和数据类型
-
学习Python中的变量和数据类型至关重要,因为它们构成了Python编程的基石。以下是帮助您了解Python中的变量和数据类型的分步指南:1.变量:变量在Python中用于存储数据值。它们充...
- 一文搞懂 Python 中的所有标点符号
-
反引号`无任何作用。传说Python3中它被移除是因为和单引号字符'太相似。波浪号~(按位取反符号)~被称为取反或补码运算符。它放在我们想要取反的对象前面。如果放在一个整数n...
- Python变量类型和运算符_python中变量的含义
-
别再被小名词坑哭了:Python新手常犯的那些隐蔽错误,我用同事的真实bug拆给你看我记得有一次和同事张姐一起追查一个看似随机崩溃的脚本,最后发现罪魁祸首竟然是她把变量命名成了list。说实话...
- 从零开始:深入剖析 Spring Boot3 中配置文件的加载顺序
-
在当今的互联网软件开发领域,SpringBoot无疑是最为热门和广泛应用的框架之一。它以其强大的功能、便捷的开发体验,极大地提升了开发效率,成为众多开发者构建Web应用程序的首选。而在Spr...
- Python中下划线 ‘_’ 的用法,你知道几种
-
Python中下划线()是一个有特殊含义和用途的符号,它可以用来表示以下几种情况:1在解释器中,下划线(_)表示上一个表达式的值,可以用来进行快速计算或测试。例如:>>>2+...
- 解锁Shell编程:变量_shell $变量
-
引言:开启Shell编程大门Shell作为用户与Linux内核之间的桥梁,为我们提供了强大的命令行交互方式。它不仅能执行简单的文件操作、进程管理,还能通过编写脚本实现复杂的自动化任务。无论是...
- 一文学会Python的变量命名规则!_python的变量命名有哪些要求
-
目录1.变量的命名原则3.内置函数尽量不要做变量4.删除变量和垃圾回收机制5.结语1.变量的命名原则①由英文字母、_(下划线)、或中文开头②变量名称只能由英文字母、数字、下画线或中文字所组成。③英文字...
- 更可靠的Rust-语法篇-区分语句/表达式,略览if/loop/while/for
-
src/main.rs://函数定义fnadd(a:i32,b:i32)->i32{a+b//末尾表达式}fnmain(){leta:i3...
- C++第五课:变量的命名规则_c++中变量的命名规则
-
变量的命名不是想怎么起就怎么起的,而是有一套固定的规则的。具体规则:1.名字要合法:变量名必须是由字母、数字或下划线组成。例如:a,a1,a_1。2.开头不能是数字。例如:可以a1,但不能起1a。3....
- Rust编程-核心篇-不安全编程_rust安全性
-
Unsafe的必要性Rust的所有权系统和类型系统为我们提供了强大的安全保障,但在某些情况下,我们需要突破这些限制来:与C代码交互实现底层系统编程优化性能关键代码实现某些编译器无法验证的安全操作Rus...
- 探秘 Python 内存管理:背后的神奇机制
-
在编程的世界里,内存管理就如同幕后的精密操控者,确保程序的高效运行。Python作为一种广泛使用的编程语言,其内存管理机制既巧妙又复杂,为开发者们提供了便利的同时,也展现了强大的底层控制能力。一、P...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- opacity 属性 (32)
- transition 属性 (33)
- 1-1. 变量声明 (31)
