clojure 中的惯用配置管理?

Posted

技术标签:

【中文标题】clojure 中的惯用配置管理?【英文标题】:Idiomatic config management in clojure? 【发布时间】:2013-07-17 04:44:45 【问题描述】:

在 clojure 中处理应用程序配置的惯用方式是什么?

目前我使用的是这个环境:

;; config.clj
:k1 "v1"
 :k2 2

;; core.clj
(defn config []
  (let [content (slurp "config.clj")]
    (binding [*read-eval* false]
      (read-string content))))

(defn -main []
  (let [config (config)]
    ...))

这有很多缺点:

config.clj 的路径可能无法始终正确解析 没有明确的方法来为使用的库/框架构建配置部分 无法全局访问 (@app/config)(当然,这可以看作是一种很好的函数式样式方式,但会使跨源文件访问配置变得乏味。

storm 等更大的开源项目似乎使用 YAML 而不是 Clojure,并通过有点丑陋的 hack 使配置在全球范围内可访问:(eval ``(def ~(symbol new-name) (. Config ~(symbol name)))).

【问题讨论】:

考虑使用clojure.edn/read-string而不是绑定*read-eval* 关于分辨率问题,请使用(slurp (io/resource "config.clj)) 这也有助于当您的代码成为 jar 中嵌入配置的库时 【参考方案1】:

首先使用 clojure.edn,尤其是 clojure.edn/read。比如

(use '(clojure.java [io :as io]))
(defn from-edn
  [fname]    
  (with-open [rdr (-> (io/resource fname)
                      io/reader
                      java.io.PushbackReader.)]
    (clojure.edn/read rdr)))

关于 config.edn 使用 io/resource 的路径只是解决这个问题的一种方法。由于您可能希望在运行时保存更改后的 config.edn,因此您可能需要依赖以下事实,即文件读取器和写入器的路径使用非限定文件名(如

(io/reader "where-am-i.edn")

默认为

(System/getProperty "user.dir")

考虑到您可能希望在运行时更改配置,您可以实现这样的模式 (粗略)

;; myapp.userconfig
(def default-config :k1 "v1"
                     :k2 2)
(def save-config (partial spit "config.edn"))
(def load-config #(from-edn "config.edn")) ;; see from-edn above

(let [cfg-state (atom (load-config))]
  (add-watch cfg-state :cfg-state-watch
    (fn [_ _ _ new-state]
      (save-config new-state)))
  (def get-userconfig #(deref cfg-state))
  (def alter-userconfig! (partial swap! cfg-state))
  (def reset-userconfig! #(reset! cfg-state default-config)))

基本上,这段代码包装了一个非全局原子,并提供了对它的设置和获取访问。你可以读取它的当前状态并像原子一样改变它。喜欢(alter-userconfig! assoc :k2 3)。对于全局测试,您可以重置!用户配置并将各种用户配置注入您的应用程序(alter-userconfig! (constantly :k1 300, :k2 212))

需要userconfig的函数可以这样写 (defn do-sth [cfg arg1 arg2 arg3] ...) 并使用各种配置进行测试,例如 default-userconfig、testconfig1、2、3... 像在用户面板中操作用户配置的函数将使用 get/alter..!功能。

同样,上面的 let 在 userconfig 上封装了一个 watch,它会在每次 userconfig 更改时自动更新 .edn 文件。如果你不想这样做,你可以添加一个 save-userconfig!将原子内容吐入 config.edn 的函数。但是,您可能想要创建一种向原子添加更多手表的方法(例如在更改自定义字体大小后重新渲染 GUI),我认为这会打破上述模式的模式。

相反,如果您正在处理更大的应用程序,更好的方法是为 userconfig 定义一个协议(具有类似 let 块中的功能)并使用文件、数据库、原子的各种构造函数来实现它(或使用 reify 或 defrecord 进行测试/不同使用场景所需的任何东西。这个实例可以在应用程序中传递,每个状态操作/io 函数都应该使用它而不是任何全局的。

【讨论】:

【参考方案2】:

我什至不会将配置映射作为资源保存在单独的文件中(对于每个环境)。 Confijulate(https://github.com/bbbates/confijulate,是的 - 这是一个个人项目)允许您在单个命名空间中为每个环境定义所有配置,并通过系统属性在它们之间切换。但是,如果您需要在不重建的情况下即时更改值,Confijulate 也允许您这样做。

【讨论】:

【参考方案3】:

在过去的一个月里,我做了很多这样的工作。对于不能接受传递配置的情况,我们在原子中使用了全局配置映射。在应用程序启动的早期,配置变量是 swap!ed 和加载的配置,之后它就被单独放置了。这在实践中有效,因为它在应用程序的生命周期内实际上是不可变的。不过,这种方法可能不适用于图书馆。

我不确定您所说的“没有明确的方法来构造已用库/框架的配置部分”是什么意思。您希望库可以访问配置吗?无论如何,我创建了一个配置加载器管道,该管道被赋予在启动时设置配置的函数。这允许我根据库和源分离配置。

【讨论】:

This approach may not work well for libraries, though - 或测试(这里的经验之声......) 它实际上适用于测试。在内部,配置一旦被设置就被视为不可变的。在那一点上,一切本质上都是一个恒定的全局。当我们进行测试时,我们只需加载一个带有模拟值的配置。不过,我们的配置主要是 URL、凭据和不变的事物名称。我还没有看到我们使用 config 来微调逻辑之类的任何实例。但是,如果您打算在应用程序运行时更改配置,那么您是对的。在那种情况下,测试很复杂。 其他线程无法以这种方式访问​​配置映射。绑定是线程本地的。 我想我的意思是使用原子和swap! 配置映射。

以上是关于clojure 中的惯用配置管理?的主要内容,如果未能解决你的问题,请参考以下文章

[Clojure] 包管理器leiningen配置国内镜像仓库

Clojure 中的惯用错误处理

通过 Clojure 中的集合进行递归的惯用方式

从clojure中的普通lisp替换(null x)函数的惯用方法

惯用的 Clojure 函数别名

Clojure 中的惯用语:(drop 1 str)还是(rest str)?