最近完成了一個 elixir 項目, 打包發佈以後, 卻遇到了一些問題. config
文件中的配置信息, 都在編譯的時候固定了, 打包以後就沒法更改配置了. 爲了解決這個問題, 實現運行時的配置, 我查詢了一些資料, 整理成本文.編程
Application.get_env
Application, 即應用, 是 elixir/erlang 世界裏實現某些特定功能的單位. 好比在新建 Phoenix 項目的時候, 就會建立一個 lib/MyApp.ex
文件, 包含了這個應用的 start
等函數. elixir 提供了一個 Application.get_env(app, key, default \\ nil)
函數, 咱們能夠很方便地獲取到某個應用的某個環境變量的值. 我在想, 這些變量是存儲在哪裏的, 是查詢速度很快的ets裏嗎? 因而我查看了一下這個函數的實現:app
@spec get_env(app, key, value) :: value def get_env(app, key, default \\ nil) do :application.get_env(app, key, default) end
-spec get_env(Application, Par, Def) -> Val when Application :: atom(), Par :: atom(), Def :: term(), Val :: term(). get_env(Application, Key, Def) -> case get_env(Application, Key) of {ok, Val} -> Val; undefined -> Def end.
到這裏, 還只是在實現默認值的功能, 繼續看看:函數
-spec get_env(Application, Par) -> 'undefined' | {'ok', Val} when Application :: atom(), Par :: atom(), Val :: term(). get_env(Application, Key) -> application_controller:get_env(Application, Key).
忽然冒出了一個 application_controller
模塊, 它大概是核心人物了吧, 繼續看看:工具
get_env(AppName, Key) -> case ets:lookup(ac_tab, {env, AppName, Key}) of [{_, Val}] -> {ok, Val}; _ -> undefined end.
果真和咱們預想的同樣, 全部應用的環境變量都存儲在一個名爲 ac_tab
的 ets 表中, 以環境名, 應用名和變量的Key組成的三元組來進行查詢. 這就意味着, 在運行時修改這些環境變量的值是有可能的.測試
Mix.Config
那麼, elixir 是在何時往這個表裏寫入數據的呢. 在 Phoenix 項目中, 咱們通常把配置文件寫在 config
目錄下, 每一個配置文件都須要用到 use Mix.Config
. Mix 是elixir 內置的編譯工具, 也是 elixir 世界裏的管家, 項目的新建, 編譯, 測試等它都要插一手. 讓我來看看它都幹了什麼好事:atom
defmacro __using__(_) do quote do import Mix.Config, only: [config: 2, config: 3, import_config: 1] {:ok, agent} = Mix.Config.Agent.start_link var!(config_agent, Mix.Config) = agent end end
這裏普及一點元編程的內容, var!(var, context)
函數的做用就是在宏內使用調用者上下文的變量. 在調用 use Mix.Config
以後, 首先導入了 Mix.Config
模塊中的三個函數, 而後啓動了一個 Agent, 並將它的 pid 綁定到變量 config_agent
上. 這個Agent是幹嗎用的呢? 猜想一下, 應該是用來臨時存儲配置的, 最後再寫入到 ets.spa
接下來就要看下 config(app, opts)
和 config(app, key, opts)
函數了. 看到這裏, 我想它們的做用應該就是將配置信息寫入到 config_agent
中:code
defmacro config(app, opts) do quote do Mix.Config.Agent.merge var!(config_agent, Mix.Config), [{unquote(app), unquote(opts)}] end end defmacro config(app, key, opts) do quote do Mix.Config.Agent.merge var!(config_agent, Mix.Config), [{unquote(app), [{unquote(key), unquote(opts)}]}] end end
看一下, Mix.Config.Agent.merge(agent, new_config)
函數:進程
@spec merge(pid, config) :: config def merge(agent, new_config) do Agent.update(agent, &Mix.Config.merge(&1, new_config)) end
在 elixir 中, 若是一個進程的做用只是用來作簡單的數據存取, 那麼可使用 Agent. 雖然不少人更偏向於只使用 GenServer~. 這裏就不看 Mix.Config.merge/2
函數了, 它的做用就是將相同 app 的配置合併到一個列表.圖片
那麼, 調用完 config
函數, 配置信息都寫入了 agent, 那麼何時寫入 ets 呢? 是在編譯時嗎, 仍是運行時? 首先, 編譯時確定是有寫入的, 由於咱們常常會像這樣 @something Application.get_env(:my_app, :something)
將環境變量獲取到模塊屬性中, 而模塊屬性的值是在編譯時肯定的.
有很重要的一點咱們尚未肯定, 那就是 myapp/config
目錄下的這些.exs
文件究竟是何時執行的, elixir又是怎麼得到 config_agent 的pid 的. 我在 Mix.Config
模塊中發現了這個函數:
def read!(file, loaded_paths \\ []) do try do if file in loaded_paths do raise ArgumentError, message: "recursive load of #{file} detected" end {config, binding} = Code.eval_string File.read!(file), [{{:loaded_paths, Mix.Config}, [file | loaded_paths]}], [file: file, line: 1] config = case List.keyfind(binding, {:config_agent, Mix.Config}, 0) do {_, agent} -> get_config_and_stop_agent(agent) nil -> config end validate!(config) config rescue e in [LoadError] -> reraise(e, System.stacktrace) e -> reraise(LoadError, [file: file, error: e], System.stacktrace) end end defp get_config_and_stop_agent(agent) do config = Mix.Config.Agent.get(agent) Mix.Config.Agent.stop(agent) config end
咱們看到, 這裏使用 Code.eval_string
函數, 執行 .exs配置文件, 而且粗暴地提取了其中的 config_agent
變量, 也就是用於存放環境變量列表的 Agent 的pid. 而後從該 Agent 裏獲取 config , 並將其終結.
在Mix.Tasks.Loadconfig
模塊裏, 咱們看到這個函數:
defp load(file) do apps = Mix.Config.persist Mix.Config.read!(file) Mix.ProjectStack.configured_applications(apps) :ok end
其它的代碼這裏就不展現了, 簡而言之, 每次運行 mix ...
命令時, 都會先執行 mix loadconfig
來完成配置文件的載入工做.
sys.config
如今, 咱們已經搞清楚了 elixir 是如何讀取和保存環境變量的. 問題在於, 當咱們使用 distillery 等發佈工具將項目打包以後, 就沒法使用 Mix 了, 那要如何修改配置呢.
事實上, 打包後的 my_app.tar.gz 文件解壓縮後, 會附帶一個 var/sys.config
文件, 裏面有咱們在 config.exs
中的全部配置. 修改它, 並將其保存到上層目錄, 再運行項目, 就大功告成啦.
至於erlang是如何讀取 sys.config
文件, 以及如何以更簡單的方式修改該文件, 咱們下回分解.