Clojure:如何判断代码是 REPL 还是 JAR 中的 运行?
Clojure: how to tell if code is running in the REPL or a JAR?
我正在用 Clojure 编写一个名为 OneCLI 的 CLI 框架。这个框架的主要核心部分是一个名为 go!
的函数,它“为你”解析命令行、环境变量和配置文件,运行s 是几个不同的用户提供的函数之一,基于什么是在这些输入中提供。
通常,go!
是从用户调用 Clojure 程序的 -main
函数中调用的。例如,我在另一个名为 zic 的“uberjar”风格的应用程序中使用了自己的库。函数 go!
调用 System/exit
作为其 运行 的一部分,向它传递一个退出代码,该退出代码来自用户提供的函数的结果。这在“生产”中非常有效,但这也意味着我不能 运行 来自 REPL 的 zic.cli/-main
函数,因为每当我调用 System/exit
并且 REPL 退出时。
在你问之前,运行在 raspberry pi 上开发时从 REPL 中获取它避免了 运行 lein uberjar
/1 分钟 30 所花费的昂贵的 45 秒秒 运行 clj -X:depstar uberjar :jar ...
.
我的问题是:作为 Clojure 标准库的一部分,我是否可以检查一些 var 或值来告诉我的 OneCLI 代码它是来自 REPL 的 运行ning 还是 运行ning 来自 JAR?
这样的变量将使我能够在 OneCLI 中检测到我们 运行ning 来自 REPL,这样它就可以避免调用 System/exit
.
每个 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"
组 ID demo-grp
和工件 ID demo-art
。如果您的文件如下所示:
(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 的。
P.S。对于 Leiningen JAR 文件
当使用 lein
构建 JAR 文件时,它总是将 project.clj
文件的副本放置在以下位置:
META-INF/leiningen/demo-grp/demo-art/project.clj
因此您也可以使用此文件的 presence/absence 作为检测器。
更新
好的,看起来 MANIFEST.MF 文件高度依赖于您的构建工具。参见
- https://docs.oracle.com/javase/tutorial/deployment/jar/defman.html
- https://www.baeldung.com/java-jar-manifest
因此,您的选择似乎是:
- 对于
lein
,可以使用上面的技巧。
- 您可以使用其他答案中
*1
的 REPL 技巧。
- 您始终可以让构建工具在清单中包含一个自定义键值对,然后对其进行检测。
更新 #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
我不知道如何检测您是否 运行正在使用 REPL。我快速浏览了 Clojure's launching code (clojure.main),但我没有看到任何钩子来检测你是否在 REPL 中,相比之下 运行 通过 clojure -m
。
如果您正在使用 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 工具可能希望快速启动),但是 none 我从事的项目完全使用 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
文件,但在 运行 宁“生产”时将其保留。这样做的好处是更明确,但代价是开发人员必须更明确。
与其尝试使用一种函数神奇地检测出您 运行 来自哪个环境,不如使用两种行为不同的函数非常简单。
- 将共享行为提取到不属于
-main
的函数。称它为 run
或其他名称。
- 让
-main
调用该函数,然后调用 System/exit
- 当您希望从 repl 使用该程序时,请调用
run
而不是 -main
。它会正常完成,而不是调用 System/exit
.
这是一个有趣的问题,因为将 JVM 关闭放入库通常是可怕的,但另一方面,“真正的应用程序”涉及很多样板,可以很好地共享...例如隐藏 jar 的在正确的时间启动 gif,或者(重新)打开 Windows 终端,如果应用需要 stdio。
您的 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
关闭一切,你说。那么你需要两个-main
s。在不同的命名空间中创建第二个 -main
。让 jar 的 Main-Class -main
除了 (1) 委托给第二个 main 和 (2) 最后关闭 JVM 之外什么都不做。当您在 REPL 中时,调用第二个 -main
,它不会破坏 JVM。您可以将每个 -main
中的大部分提取到一个库中。如果你使用“完整框架”,你甚至可以让框架拥有 uberjarring 进程和 Main-Class.
我正在用 Clojure 编写一个名为 OneCLI 的 CLI 框架。这个框架的主要核心部分是一个名为 go!
的函数,它“为你”解析命令行、环境变量和配置文件,运行s 是几个不同的用户提供的函数之一,基于什么是在这些输入中提供。
通常,go!
是从用户调用 Clojure 程序的 -main
函数中调用的。例如,我在另一个名为 zic 的“uberjar”风格的应用程序中使用了自己的库。函数 go!
调用 System/exit
作为其 运行 的一部分,向它传递一个退出代码,该退出代码来自用户提供的函数的结果。这在“生产”中非常有效,但这也意味着我不能 运行 来自 REPL 的 zic.cli/-main
函数,因为每当我调用 System/exit
并且 REPL 退出时。
在你问之前,运行在 raspberry pi 上开发时从 REPL 中获取它避免了 运行 lein uberjar
/1 分钟 30 所花费的昂贵的 45 秒秒 运行 clj -X:depstar uberjar :jar ...
.
我的问题是:作为 Clojure 标准库的一部分,我是否可以检查一些 var 或值来告诉我的 OneCLI 代码它是来自 REPL 的 运行ning 还是 运行ning 来自 JAR?
这样的变量将使我能够在 OneCLI 中检测到我们 运行ning 来自 REPL,这样它就可以避免调用 System/exit
.
每个 Java JAR 文件必须有文件 META-INF/MANIFEST.MF
添加。如果它不存在,则您不能 运行 在(普通)JAR 文件中。虽然您可以通过将伪造文件放在类路径中(例如 ./resources
中)来欺骗此检测器,但这是检测普通 JAR 文件的可靠方法。
问题:
依赖 JAR 文件有时会很草率,并且会用它们自己的 META-INF/MANIFEST.MF
文件污染类路径,因此任何随机 META-INF/MANIFEST.MF
的存在都不足以确定存在“噪声”的答案" 文件。因此,您需要检查您自己的特定 META-INF/MANIFEST.MF
文件是否存在。如果您知道 ArtifactId
和 GroupId
.
在 Leiningen 项目中,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
。您的特定 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 的。
P.S。对于 Leiningen JAR 文件
当使用 lein
构建 JAR 文件时,它总是将 project.clj
文件的副本放置在以下位置:
META-INF/leiningen/demo-grp/demo-art/project.clj
因此您也可以使用此文件的 presence/absence 作为检测器。
更新
好的,看起来 MANIFEST.MF 文件高度依赖于您的构建工具。参见
- https://docs.oracle.com/javase/tutorial/deployment/jar/defman.html
- https://www.baeldung.com/java-jar-manifest
因此,您的选择似乎是:
- 对于
lein
,可以使用上面的技巧。 - 您可以使用其他答案中
*1
的 REPL 技巧。 - 您始终可以让构建工具在清单中包含一个自定义键值对,然后对其进行检测。
更新 #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
我不知道如何检测您是否 运行正在使用 REPL。我快速浏览了 Clojure's launching code (clojure.main),但我没有看到任何钩子来检测你是否在 REPL 中,相比之下 运行 通过 clojure -m
。
如果您正在使用 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 工具可能希望快速启动),但是 none 我从事的项目完全使用 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
文件,但在 运行 宁“生产”时将其保留。这样做的好处是更明确,但代价是开发人员必须更明确。
与其尝试使用一种函数神奇地检测出您 运行 来自哪个环境,不如使用两种行为不同的函数非常简单。
- 将共享行为提取到不属于
-main
的函数。称它为run
或其他名称。 - 让
-main
调用该函数,然后调用System/exit
- 当您希望从 repl 使用该程序时,请调用
run
而不是-main
。它会正常完成,而不是调用System/exit
.
这是一个有趣的问题,因为将 JVM 关闭放入库通常是可怕的,但另一方面,“真正的应用程序”涉及很多样板,可以很好地共享...例如隐藏 jar 的在正确的时间启动 gif,或者(重新)打开 Windows 终端,如果应用需要 stdio。
您的 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
关闭一切,你说。那么你需要两个-main
s。在不同的命名空间中创建第二个 -main
。让 jar 的 Main-Class -main
除了 (1) 委托给第二个 main 和 (2) 最后关闭 JVM 之外什么都不做。当您在 REPL 中时,调用第二个 -main
,它不会破坏 JVM。您可以将每个 -main
中的大部分提取到一个库中。如果你使用“完整框架”,你甚至可以让框架拥有 uberjarring 进程和 Main-Class.