Clojure中的惯用配置管理?

18

在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))))。


2
考虑使用 clojure.edn/read-string,而不是绑定 *read-eval* - Jeremy
1
关于分辨率问题,使用(slurp (io/resource "config.clj))。当您的代码成为一个带有嵌入式配置的JAR包时,这也会有所帮助。 - noisesmith
3个回答

12

首先使用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)))

关于使用 io/resource 处理 config.edn 的路径只是其中一种方法。由于您可能希望在运行时保存已更改的 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)))

基本上,此代码包装了一个非全局的原子,并提供对其进行设置和获取访问的功能。您可以像使用 atom 一样读取其当前状态并更改它,比如使用 (alter-userconfig! assoc :k2 3)。对于全局测试,您可以重置 userconfig 并将各种 userconfig 注入到应用程序中,例如 (alter-userconfig! (constantly {:k1 300, :k2 212}))

需要 userconfig 的函数可以编写为 (defn do-sth [cfg arg1 arg2 arg3] ...) 并使用像 default-userconfig、testconfig1、2、3 等不同配置进行测试。 操作 userconfig 的函数(就像在用户面板中一样)将使用 get/alter..! 函数。

另外,上述 let 包装了一个 watch,自动更新 .edn 文件,每次更改 userconfig 时都会更新它。如果您不想这样做,可以添加一个 save-userconfig! 函数,将原子内容转存到 config.edn 中。但是,您可能希望创建一种方法来向原子添加更多 watch(例如在自定义字体大小更改后重新渲染 GUI),这在我看来会打破上述模式。

相反,如果你正在处理一个较大的应用程序,更好的方法是定义一个协议(具有类似 let 块中的函数),用文件、数据库、原子(或您需要测试/不同用例的任何内容)的各种构造函数来实现它,利用 reify 或 defrecord。该实例可以在应用程序中传递,并且每个状态操作/io 函数都应使用它,而不是使用任何全局变量。


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

1
我在过去的一个月里为工作做了相当多的这方面的工作。对于那些传递配置不可接受的情况,我们使用了一个原子中的全局配置映射。在应用程序启动早期,配置变量与加载的配置进行了交换,之后就不再改变。这种方法实际上是不可变的,因此在应用程序的生命周期内可以有效地运行。但是,这种方法可能不适用于库。
“没有明确的方法来为使用的库/框架结构化配置部分”的意思我不太确定。您是否希望库能够访问配置?无论如何,我创建了一个配置加载器的管道,将其提供给设置启动配置的函数。这使我可以根据库和来源分离配置。

1
这种方法可能不适用于库,尽管对于测试来说效果还不错(从经验角度来看...) - Alex
在测试方面,它实际上运行良好。在内部,一旦设置了配置,它就被视为不可变的。此时,所有内容基本上都是常量全局变量。当我们进行测试时,只需加载具有模拟值的配置即可。虽然我们的配置大多是URL、凭据和不变的事物名称。我还没有看到任何使用配置来微调逻辑等的情况。但如果您打算在应用程序运行时更改配置,那么您是正确的。在这种情况下,测试会变得复杂。 - Jeremy
在这种方式下,配置映射将无法从其他线程访问。绑定是线程本地的。 - Didier A.
我认为我想要说的是使用一个原子并用swap!将配置映射插入。 - Jeremy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接