Clojure:如何判断代码是在 REPL 还是 JAR 中运行



我正在用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 标准库的一部分,我是否可以检查一些变量或值,它告诉我的 OneCLI 代码是从 REPL 运行还是从 JAR 运行?

这样的变量将使我在OneCLI中能够检测到我们正在从REPL运行,以便它可以避免调用System/exit

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

  • 共享行为提取到不属于-main的函数中。叫它run什么的。
  • -main调用该函数,然后调用System/exit
  • 当您希望从 repl 使用该程序时,请调用run而不是-main。它将正常完成,而不是调用System/exit.

我不知道如何检测您是否在 REPL 上运行。我快速浏览了Clojure的启动代码(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.ednproject.clj文件,但在"生产中"运行时将其省略。这样做的好处是更明确,但代价是开发人员必须更明确。

这是一个有趣的问题,因为将 JVM 关闭放入库中通常是可怕的,但另一方面,"真正的应用程序"涉及许多样板文件,分享起来很棒......例如在正确的时间隐藏 Jar 的启动 GIF,或者(重新)打开 Windows 终端(如果应用程序需要 stdio)。

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

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

但我并不总是希望-main关闭一切,你说。 然后你需要两个-main。 在不同的命名空间中进行第二次-main。 让 jar 的主类-main什么都不做,只是 (1) 委托给第二个主类,(2) 最后关闭 JVM。 当您在 REPL 中时,调用第二个-main,即不会破坏 JVM 的那个。 您可以将每个-main的大部分分解到库中。 如果你使用"完整框架",你甚至可以让框架拥有超级干扰进程和主类。

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

<小时 />

问题:

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

在莱宁根项目中,第一行project.clj看起来像

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

组 ID 为demo-grp,项目 ID 为demo-art。 如果您的文件如下所示:

(defproject demo "0.1.0-SNAPSHOT"

则组 ID 和工件 ID 都将demo。 您的特定清单。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 字符串设置函数以检测特定项目清单的存在。中频:

(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

我不知道这是从哪里来的,以进入类路径。


P.S. 用于莱宁根 JAR 文件

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

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

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

<小时 />

更新

好的,它看起来像清单。MF 文件高度依赖于您的构建工具。 看

  • https://docs.oracle.com/javase/tutorial/deployment/jar/defman.html
  • https://www.baeldung.com/java-jar-manifest

因此,您的选择似乎是:

  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

相关内容

最新更新