[elixir! #0036]關於 elixir 應用的配置

圖片描述

緣起

最近完成了一個 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 文件, 以及如何以更簡單的方式修改該文件, 咱們下回分解.

相關文章
相關標籤/搜索