【问题标题】:Clojure: how to tell if code is running in the REPL or a JAR?Clojure:如何判断代码是在 REPL 还是 JAR 中运行?
【发布时间】:2021-04-16 00:08:28
【问题描述】:

我正在用 Clojure 编写一个名为 OneCLI 的 CLI 框架。该框架的主要核心部分是一个名为go! 的函数,它“为您”解析命令行、环境变量和配置文件,并根据这些输入中提供的内容运行几个不同的用户提供的函数之一。

通常,go! 是从用户调用 Clojure 程序的 -main 函数调用的。例如,我在另一个名为zic 的“uberjar”风格的应用程序中使用我自己的库。函数go! 调用System/exit 作为其运行的一部分,向其传递来自用户提供函数结果的退出代码。这在“生产”中效果很好,但这也意味着我无法从 REPL 运行 zic.cli/-main 函数,因为每当我这样做时,它都会调用 System/exit 并且 REPL 退出。

在你问之前,在树莓派上开发时从 REPL 运行它可以避免运行 lein uberjar/1 分 30 秒运行 clj -X:depstar uberjar :jar ... 所需的昂贵的 45 秒。

我的问题是:作为 Clojure 标准库的一部分,我可以检查一些 var 或 value,告诉我的 OneCLI 代码是从 REPL 运行还是从 JAR 运行? p>

这样的变量可以让我在 OneCLI 中检测到我们正在从 REPL 运行,从而避免调用 System/exit。

【问题讨论】:

  • 对于那些只需要一种快速而肮脏的方式来判断 当前代码(不适用于我上面的问题)是否在 REPL 中的未来人:只需检查*file* var,在 REPL 内部工作时未设置(或设置为某个默认值)。
  • 这对我不起作用 - *file* 在我的 REPL 中设置为 /tmp/some-long-filename(带有 lein repl 2.9.5 的 linux,clojure 1.10.1)

标签: clojure read-eval-print-loop leiningen deps-edn


【解决方案1】:

每个 Java JAR 文件都必须有文件 META-INF/MANIFEST.MF 添加。如果它不存在,则不能在(正常)JAR 文件中运行。虽然您可以通过在类路径中放置一个伪造文件(例如,在 ./resources 中)来欺骗此检测器,但这是检测正常 JAR 文件的可靠方法。


问题:

依赖项 JAR 文件有时很草率,并且会用它们自己的 META-INF/MANIFEST.MF 文件污染类路径,因此任何随机的 META-INF/MANIFEST.MF 的存在都不足以在存在“噪声”文件的情况下确定答案。因此,您需要检查您自己的特定META-INF/MANIFEST.MF 文件是否存在。如果您知道 ArtifactId 和 GroupId 的 Maven 值,这很容易做到。

在一个 Leiningen 项目中,project.clj 的第一行看起来像

(defproject demo-grp/demo-art "0.1.0-SNAPSHOT"

对于 demo-grp 的组 ID 和 demo-art 的工件 ID。如果您的文件如下所示:

(defproject demo "0.1.0-SNAPSHOT"

那么组 ID 和工件 ID 都是 demo。您特定的 MANIFEST.MF 看起来像

> cat META-INF/MANIFEST.MF 
Manifest-Version: 1.0
Created-By: Leiningen 2.9.1
Built-By: alan
Build-Jdk: 15
Leiningen-Project-ArtifactId: demo-art
Leiningen-Project-GroupId: demo-grp
Leiningen-Project-Version: 0.1.0-SNAPSHOT
Main-Class: demo.core

使用 to ID 字符串设置一个函数来检测您的特定项目 MANIFEST.MF 的存在:

(ns demo.core
  (:require [clojure.java.io :as io])
  (:gen-class))

(def ArtifactId "demo-art")
(def GroupId "demo-grp")

(defn jar-file? []
  (let [re-ArtifactId (re-pattern (str ".*ArtifactId.*" ArtifactId))
        re-GroupId    (re-pattern (str ".*GroupId.*" GroupId))
        manifest      (slurp (io/resource "META-INF/MANIFEST.MF"))
        f1            (re-find re-ArtifactId manifest)
        f2            (re-find re-GroupId manifest)
        found?        (boolean (and f1 f2))]
    found?))

(defn -main []
  (println "main - enter")
  (println "Detected JAR file: " (jar-file?))
  )

您现在可以测试代码了:

~/expr/demo > lein clean ; lein run
main - enter
Detected JAR file:  false

~/expr/demo > lein clean ; lein uberjar
Compiling demo.core
Created /home/alan/expr/demo/target/uberjar/demo-art-0.1.0-SNAPSHOT.jar
Created /home/alan/expr/demo/target/uberjar/demo-art-0.1.0-SNAPSHOT-standalone.jar

~/expr/demo > java -jar /home/alan/expr/demo/target/uberjar/demo-art-0.1.0-SNAPSHOT-standalone.jar 
main - enter
Detected JAR file:  true

“噪音”JAR 文件示例:如果我们执行lein clean; lein run,并在我们的主程序中添加一行

(println (slurp (io/resource "META-INF/MANIFEST.MF")))

我们出去:

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: jenkins
Created-By: Apache Maven 3.2.5
Build-Jdk: 1.8.0_111

我不知道这是从哪里进入 CLASSPATH 的。


附注对于 Leiningen JAR 文件

当使用lein 构建JAR 文件时,它总是将project.clj 文件的副本放在以下位置:

META-INF/leiningen/demo-grp/demo-art/project.clj

因此您也可以将此文件的存在/不存在用作检测器。


更新

好的,MANIFEST.MF 文件似乎高度依赖于您的构建工具。见

因此,您的选择似乎是:

  1. 对于lein,您可以使用上述技术。
  2. 您可以使用另一个答案中*1 的 REPL 技巧。
  3. 您始终可以让构建工具在清单中包含自定义键值对,然后检测它。

更新 #2

另一个答案,也许更简单,是使用lein-environ 插件和environ 库(您需要两者)来检测环境(假设您使用lein 创建您的REPL)。你的project.clj 应该是这样的:

  :dependencies [
                 [clojure.java-time "0.3.2"]
                 [environ "1.2.0"]
                 [org.clojure/clojure "1.10.2-alpha1"]
                 [prismatic/schema "1.1.12"]
                 [tupelo "21.01.05"]
                 ]
  :plugins [[com.jakemccrary/lein-test-refresh "0.24.1"]
            [lein-ancient "0.6.15"]
            [lein-codox "0.10.7"]
            [lein-environ "1.2.0"]
            ]

你需要一个profiles.clj:

{:dev  {:env {:env-mode "dev"}}
 :test {:env {:env-mode "test"}}
 :prod {:env {:env-mode "prod"}}}

还有一个命名空间demo.config,比如:

(ns demo.config
  (:require
    [environ.core :as environ]
  ))

(def ^:dynamic *env-mode* (environ/env :env-mode))
(println "  *env-mode* => " *env-mode*)

然后你会得到如下结果:

*env-mode* =>  dev      ; for `lein run`
*env-mode* =>  test     ; for `lein test`
*env-mode* =>  nil      ; from `java -jar ...`

您需要输入:

lein with-profile :prod run

生产

*env-mode* =>  prod

【讨论】:

  • 看起来有点老套。
  • 它非常 hacky,并且无法与许多 JAR 打包程序一起使用——它恰好可以与 Leiningen 一起使用,但它依赖于 Leiningen 未记录的怪癖。
  • @AlanThompson 我还发现了另一种方法:创建没有字段的记录,例如(defrecord Reference []) 并使用 java 反射方式来检测我们是否在 JAR 文件中:mkyong.com/java/java-get-the-name-or-path-of-a-running-jar-file 您可以在记录类本身的该链接中调用非静态方式。
  • 我喜欢 manifest 想法中的自定义键值对作为更通用的 JAR 检测解决方案。
  • 是的。这正是Leiningen-Project-ArtifactId 的含义:一个自定义的lein 键/值对(尽管它与Maven 坐标有关)。 lein JAR 文件也有自定义的META-INF/leiningen/demo-grp/demo-art/project.clj 文件。
【解决方案2】:

我不知道如何检测您是否在 REPL 上运行。我快速浏览了Clojure's launching code (clojure.main),但与通过clojure -m 运行的东西相比,我没有看到任何钩子来检测你是否处于REPL 中。

如果您使用 AOT(就像您在 zic 中一样),那么您可以检查是否有任何“REPL”变量(*1、*2、*3 和 *e)被绑定.

;; returns true in a REPL and `clojure -m`, and
;; returns false in an AOT jar file run with java -jar
(bound? #'*1) 

这解决了您提出的问题,但我不喜欢这种猜测程序员意图的“神奇”机制。它可能适用于您的用例(考虑到我认为 AOT 可以节省启动时间,并且 CLI 工具可能希望快速启动),但我从事的所有项目都没有使用 AOT。

在clojure -m 案例中解决您的问题的另一个选择是要求开发人员明确选择退出“完成时退出”行为。一种方法是使用属性。

(defn maybe-exit [exit-code]
  (cond
    (= (System/getProperty "onecli.oncompletion") "remain") (System/exit exit-code)
    (= exit-code 0) nil
    :else (throw (ex-info "Command completed unsuccessfully" {:exit-code exit-code}))))

使用此代码,您可以在开发环境中添加

:jvm-opts ["-Donecli.oncompletion=remain"]

到您的 deps.edn 或 project.clj 文件,但在“生产”运行时将其保留。这样做的好处是更明确,但代价是开发人员必须更明确。

【讨论】:

  • 使用属性的想法很有趣。顺便说一句,我确实发现clojure.core 中的*file* 变量对于代码当前位于REPL 中的代码未设置,对于试图找出它是否在REPL 中的代码(尽管这对于从 REPL 调用但在 JAR 中提供的库没有帮助,就像我的情况一样)。
  • 当我使用lein repl、使用cider-jack-in 和使用clojure 时,似乎绑定了*file*。你是怎么让它解绑的?
  • 我认为它是绑定的,但没有设置为实际的文件名,更像是“DEFAULT_PATH”之类的。
【解决方案3】:

与其试图让一个函数神奇地检测您正在运行的环境,不如只拥有两个行为不同的函数非常简单。

  • 将共享行为提取到不属于-main 的函数中。称它为run 或其他任何名称。
  • 让-main 调用该函数,然后调用System/exit
  • 如果您希望从 repl 中使用该程序,请调用 run 而不是 -main。它会正常结束,不会调用System/exit。

【讨论】:

  • 我同意这是一个好方法,但 go! 函数在 OneCLI 库中,-main 函数在 zic 项目中声明。该解决方案涉及更改库和应用程序之间的接口。目前 OneCLI 没有得到太多采用,所以这可能没问题,但这是一个更实质性的变化。
  • 解耦运行和错误处理也可以让测试更容易。
  • 好消息是,因为System/exit 是这个星球上最具破坏性的东西,所以很容易使该界面兼容更改。首先更改应用程序:go! 返回后它所做的任何事情都是无害的,因为go! 删除了全域。然后您可以更改go! 以不再调用System/exit,并且应用程序中的死代码将成为您的新清理代码。
  • @mange 我可以简单地在 OneCLI 库中使用修改后的行为创建一个新函数,然后从 go! 调用该函数,所以我实际上没有库的接口,我可以添加更多功能,同时仍然遵循建议。
【解决方案4】:

这是一个有趣的问题,因为将 JVM 关闭放入库中通常很糟糕,但另一方面,“真正的应用程序”涉及大量样板文件,非常适合分享……例如隐藏 jar 的启动 gif在正确的时间,或者如果应用需要 stdio,则(重新)打开一个 Windows 终端。

您的 uberjar 将包含 clojure.main,因此在您的 uberjar (java -cp my-whole-app.jar clojure.main) 中运行 REPL 是完全可能的(并且很有用)。因此,“检测”类路径上的线索可能无济于事。

相反,在您的 jar 清单声明为 Main-Class 的命名空间中的 -main 中管理 JVM 关闭工作。也就是说:如果您以java -jar my-whole-app.jar 运行它,那么它应该会正确关闭所有内容。

但你说,我并不总是希望-main 关闭一切。然后你需要两个-mains。在不同的命名空间中创建第二个-main。让 jar 的 Main-Class -main 除了 (1) 委托给第二个 main 和 (2) 最后关闭 JVM 之外什么都不做。当您在 REPL 中时,调用第二个 -main,它不会破坏 JVM。您可以将每个-main 的大部分内容分解到一个库中。如果你使用“完整框架”,你甚至可以让框架拥有 uberjarring 进程和 Main-Class。

【讨论】:

    猜你喜欢
    • 2020-03-31
    • 2020-06-18
    • 2010-10-15
    • 2020-09-22
    • 2015-03-28
    • 2023-03-03
    • 2011-12-14
    • 1970-01-01
    • 2023-03-07
    相关资源
    最近更新 更多