百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Elixir实战:11 组件工作 (1) OTP 应用程序

zhezhongyun 2025-01-05 21:29 59 浏览

本章涵盖

  • 创建 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 。

相关推荐

激光手术矫正视力对眼睛到底有没有伤害?

因为大家询问到很多关于“基质不能完全愈合”的问题,有必要在这里再详细解释一下。谢谢@珍惜年少时光提出的疑问:因为手头刚好在看组织学,其中提到:”角膜基质约占角膜的全厚度的90%,主要成分是胶原板层,...

OneCode核心概念解析——View(视图)

什么是视图?在前面的章节中介绍过,Page相关的概念,Page是用户交互的入口,具有Url唯一性。但Page还只是一个抽象的容器,而View则是一个具备了具体业务能力的特殊的Page,它可以是一个...

精品博文图文详解Xilinx ISE14.7 安装教程

在软件安装之前,得准备好软件安装包,可从Xilinx官网上下载:http://china.xilinx.com/support/download/index.html/content/xilinx/z...

卡片项目管理(Web)(卡片设计的流程)

简洁的HTML文档卡片管理,简单框架个人本地离线使用。将个人工具类的文档整理使用。优化方向:添加图片、瀑布式布局、颜色修改、毛玻璃效果等。<!DOCTYPEhtml><html...

GolangWeb框架Iris项目实战-JWT和中间件(Middleware)的使用EP07

前文再续,上一回我们完成了用户的登录逻辑,将之前用户管理模块中添加的用户账号进行账号和密码的校验,过程中使用图形验证码强制进行人机交互,防止账号的密码被暴力破解。本回我们需要为登录成功的用户生成Tok...

sitemap 网站地图是什么格式?有什么好处?

sitemap网站地图方便搜索引擎发现和爬取网页站点地图是一种xml文件,或者是txt,是将网站的所有网址列在这个文件中,为了方便搜索引擎发现并收录的。sitemap网站地图分两种:用于用户导...

如何在HarmonyOS NEXT中处理页面间的数据传递?

大家好,前两天的Mate70的发布,让人热血沸腾啊,不想错过,自学的小伙伴一起啊,今天分享的学习笔记是关于页面间数据伟递的问题,在HarmonyOSNEXT5.0中,页面间的数据传递可以有很多种...

从 Element UI 源码的构建流程来看前端 UI 库设计

作者:前端森林转发链接:https://mp.weixin.qq.com/s/ziDMLDJcvx07aM6xoEyWHQ引言由于业务需要,近期团队要搞一套自己的UI组件库,框架方面还是Vue。而业界...

jq+ajax+bootstrap改了一个动态分页的表格

最近在维护一个很古老的项目,里面是用jq的dataTable方法实现一个分页的表格,不过这些表格的分页是本地分页。现在想要的是点击分页去请求数据。经过多次的修改,以失败告终。分页的不准确,还会有这个错...

学习ES6- 入门Vue(大量源代码及笔记,带你起飞)

ES6学习网站:https://es6.ruanyifeng.com/箭头函数普通函数//普通函数this指向调用时所在的对象(可变)letfn=functionfn(a,b){...

青锋微服务架构之-Ant Design Pro 基本配置

青锋(msxy)-Gitee.com1、更换AntDesignPro的logo和名称需要修改文件所在位置:/config/defaultSetting.jsconstproSett...

大数据调度服务监控平台(大数据调度服务监控平台官网)

简介SmartKettle是针对上述企业的痛点,对kettle的使用做了一些包装、优化,使其在web端也能具备基础的kettle作业、转换的配置、调度、监控,能在很大一定程度上协助企业完成不同...

Flask博客实战 - 实现博客首页视图及样式

本套教程是一个Flask实战类教程,html/css/javascript等相关技术栈不会过多的去详细解释,那么就需要各位初学者尽可能的先去掌握这些基础知识,当然本套教程不需要你对其非常精通,但最起码...

Web自动化测试:模拟鼠标操作(ActionChains)

在日常的测试中,经常会遇到需要鼠标去操作的一些事情,比如说悬浮菜单、拖动验证码等,这一节我们来学习如何使用webdriver模拟鼠标的操作首页模拟鼠标的操作要首先引入ActionChains的包fro...

DCS F-16C 中文指南 16.9ILS仪表降落系统教程

10–ILS教程我们的ILS(仪表着陆进近)将到达Batumi巴统机场。ILS频率:110.30跑道航向:120磁航向/126真航向无线电塔频率:131.0001.设置雷达高度表开关打开(前)并...