Application
是用于处理应用和定义应用回调的模块。
应用是 Erlang/OTP 中打包软件的惯用方式。为了理解这个概念,它类似于其他编程语言中常见的“库”的概念,但又有一些额外的特性。
应用是一个实现特定功能的组件,具有标准化的目录结构、配置和生命周期。应用程序可以被加载、启动和停止 。每个应用程序还有自己的环境,它提供了一个统一的API来配置每个应用。
开发人员通常与应用环境及其回调模块打交道。因此,我们首先讨论它们,然后再深入应用资源文件和生命周期的细节。
应用环境
每个应用都有自己的环境。环境是一个关键字列表。请注意,此环境与操作系统环境无关。
默认情况下,应用的环境是一个空列表。在Mix项目的 mix.exs
文件中,你可以通过在 application/0
函数中设置 :env
键来配置它:
def application do[env: [db_host: "localhost"]]
end
现在,在你的应用中,你可以使用诸如 fetch_env!/2
及其变体函数来读取此环境:
defmodule MyApp.DBClient dodef start_link() doSomeLib.DBClient.start_link(host: db_host())enddefp db_host doApplication.fetch_env!(:my_app, :db_host)end
end
在Mix项目中,应用及其依赖的环境可以通过 config/config.exs
和 config/runtime.exs
文件覆盖。前者用于构建时,在编译之前加载,后者用于运行时,在应用启动之前加载。例如,使用你的应用的人可以覆盖 :db_host
环境变量如下:
import Config
config :my_app, :db_host, "db.local"
有关更多信息,请参见 Mix
模块中的“配置”部分。你还可以使用 put_env/3
和 delete_env/2
等函数动态更改应用环境。
库的环境
配置文件config/config.exs
和config/runtime.exs
很少被库使用。库通常在其mix.exs
的application/0
函数中定义环境。配置文件更多地被应用用来配置它们的库。
读取其他应用的环境
每个应用只应该负责自己的环境。不要使用此模块中的函数直接访问或修改其他应用的环境。当你更改应用环境时,Elixir的构建工具只会重新编译属于该应用的文件。所以如果你读取了另一个应用的环境,就有可能依赖于过时的配置,因为你的代码不会随着它的变化被重新编译。
编译时环境
在前面的示例中,我们在运行时读取应用环境:
defmodule MyApp.DBClient dodef start_link() doSomeLib.DBClient.start_link(host: db_host())enddefp db_host doApplication.fetch_env!(:my_app, :db_host)end
end
换句话说,应用 :my_app
的环境 :db_host
只有在 MyApp.DBClient
实际启动时才会被读取。虽然在运行时读取应用环境是首选的方法,但在某些罕见的情况下,你可能会在编译阶段访问应用环境。然而,如果你尝试在函数外部访问 Application.fetch_env!/2
函数 :
defmodule MyApp.DBClient do@db_host Application.fetch_env!(:my_app, :db_host)def start_link() doSomeLib.DBClient.start_link(host: @db_host)end
end
你可能会看到警告和错误:
warning: Application.fetch_env!/2 is discouraged in the module body,
use Application.compile_env/3 insteadiex:3: MyApp.DBClient** (ArgumentError) could not fetch application environment :db_host
for application :my_app because the application was not loaded nor
configured
这是因为在定义模块时,应用环境尚不可用。幸运的是,警告告诉了我们应用使用 Application.compile_env/3
来解决这个问题:
defmodule MyApp.DBClient do@db_host Application.compile_env(:my_app, :db_host, "db.local")def start_link() doSomeLib.DBClient.start_link(host: @db_host)end
end
这里的不同之处在于 compile_env
需要提供默认值作为参数,而不是使用 mix.exs
中的 def application
函数。此外,通过使用 compile_env/3
,像Mix这样的工具会存储在编译期间使用的值,并在系统启动时将其与运行时的值进行比较,如果它们不同,会抛出错误。
总之,应避免使用编译时环境。只要有可能,运行时读取应用环境都应该是首选。
应用回调模块
应用可以被加载、启动和停止。通常,像Mix这样的构建工具会为你启动应用及其所有依赖项,但你也可以手动执行
{:ok, _} = Application.ensure_all_started(:some_app)
当应用启动时,开发人员可以配置一个回调模块来执行自己的代码。开发人员也是使用此回调来启动应用的监督树。
首先,你需要在 mix.exs
文件的 application/0
定义中添加一个 :mod
键。它需要一个元组,包含应用回调模块和启动参数(通常是空列表):
def application do[mod: {MyApp, []}]
end
提供给 :mod
的 MyApp
模块需要实现 Application
行为。我们可以通过在该模块中使用 use Application
并实现 start/2
回调来完成,例如:
defmodule MyApp douse Applicationdef start(_type, _args) dochildren = []Supervisor.start_link(children, strategy: :one_for_one)end
end
当你
use Application
时,Application
模块会设置@behaviour Application
并定义一个可重写的stop/1
函数,这是 Erlang/OTP 所需要的。
start/2
回调必须派生并链接一个监督者,并返回 {:ok, pid}
或 {:ok, pid, state}
,其中 pid
是监督者的PID, state
是可选的应用状态。 args
是传递给 :mod
选项的元组的第二个元素。
传递给 start/2
的 type
参数通常是 :normal
,除非是在配置了应用接管和故障转移的分布式设置中。分布式应用程序超出了此文档的范围。
当应用关闭时,其 stop/1
回调会在监督树被运行时停止后被调用。这个回调允许应用进行最后的清理工作。它的参数是 start/2
返回的状态,如果有的话,或者 []
。 stop/1
的返回值会被忽略。
use Application
为模块定义了一个默认的 stop/1
实现,它会忽略其参数并返回 :ok
,但它可以被重写。
应用回调模块还可以实现可选回调 prep_stop/1
。如果存在, prep_stop/1
会在监督树终止之前被调用。其参数是 start/2
返回的状态,如果有的话,或者 []
,其返回值会传递给 stop/1
。
应用资源文件
在上述部分中,我们在 mix.exs
文件的 application/0
部分配置了一个应用。最终,Mix会使用此配置创建一个名为 APP_NAME.app
的应用资源文件。例如,OTP应用 ex_unit
的应用资源文件就叫做 ex_unit.app
。
你可以在 Mix.Tasks.Compile.App
的文档中了解更多关于生成应用资源文件的信息,也可以通过运行 mix help compile.app
来获取帮助。
应用生命周期
加载应用
应用被加载,意思就是运行时找到并处理它的资源文件:
Application.load(:ex_unit)
#=> :ok
当应用被加载时,资源文件中指定的环境会与配置文件中的合并。
加载应用不会加载它的模块,只是加载应用资源文件。
实际上,你很少需要手动加载应用,因为这是启动过程的一部分。
启动应用
应用也会被启动:
Application.start(:ex_unit)
#=> :ok
一旦应用编译完成,运行系统就是启动当前的应用及其依赖项的问题。与其他语言不同,Elixir 没有负责启动系统的 main
函数。相反,你会启动一个或多个应用,每个应用都有自己的初始化和终止逻辑。
当应用启动时, Application.load/1
会自动被调用,如果没有调用过的话。然后,它会检查资源文件中 applications
键列出的依赖项是否已经启动。只要有一个依赖项未启动都会报错。像 ensure_all_started/1
这样的函数会为你启动应用及其所有依赖项。
如果应用没有配置回调模块,启动在这里就完成了。否则,会调用其 start/2
回调。此函数返回的顶级监督者的PID由运行时存储以备后用,返回的应用状态也会被保存。
停止应用
天下没有不散的筵席,启动的应用最终也会被停止:
Application.stop(:ex_unit)
#=> :ok
如果没有定义 stop
回调,停止应用实际上没有什么操作,除了一些系统跟踪。
停止一个有回调模块的应用有三个步骤:
- 调用可选的
prep_stop/1
回调,如果有的话。 - 终止顶级监督者。
- 调用
stop/1
回调。
传递给回调的参数与 start/2
可选返回的状态有关,如上文关于回调模块的部分所述。
需要特别强调的是,第二步是一个阻塞步骤。终止监督者会触发递归终止子进程,有序关闭所有后代进程。 stop/1
回调仅在终止整个监督树之后被调用。
通过调用 System.stop/1
可以干净的关闭系统 。它会以与启动相反的顺序关闭每个应用。
默认情况下,来自操作系统的 SIGTERM
会自动转换为 System.stop/0
。你还可以通过 :os.set_signal/2
函数对操作系统信号进行更多的显式控制。
工具
Mix 构建工具自动化了大多数应用管理任务。例如, mix test
在你的测试运行之前自动启动应用依赖和应用本身。 mix run --no-halt
启动当前工程,可以用来启动一个长期运行的系统。参见 mix help run
。
开发人员还可以使用 mix release
构建发布包。发布包能够将你的所有源代码以及 Erlang VM 打包到一个目录中。发布包还可以让你控制每个应用如何启动以及启动顺序。它还提供了一个更流畅的机制,用于启动和停止系统、调试、日志记录以及系统监控。
最后,Elixir 还提供了诸如 escripts 和归档工具,用来打包你的应用。这些通常用于工具必须在开发人员之间共享时,而不是作为部署选项。有关更多详细信息,请参阅 mix help archive.build
和 mix help escript.build
。