gpt4 book ai didi

clojure-java-interop - 如何使用 clojure 作为 Java 程序的脚本语言?

转载 作者:行者123 更新时间:2023-12-05 03:55:12 27 4
gpt4 key购买 nike

对于用Java编写的服务器程序,我需要为(待指定的)查询语言添加解释器。用户应该能够将自己编写的查询“程序”发送到该服务器,并接收返回的结果(基本上只是一个字符串列表)。查询的语言还没有指定,所以我想到在这里使用 clojure 作为脚本语言——这样用户就可以将小程序发送到服务器,服务器会对它们进行评估,如果结果类型正确,就将它们发回。

我可以让它工作,通过使用“RT.readString”,通过查询```Var EVAL=RT.var("clojure.core","eval")'''和之前使用EVAL对RT.readString返回的结果进行求值。

为了更好地使用它,我需要启用一些 Java 导入,这些导入应该始终启用。从逻辑上讲,这些导入应该只运行一次——我怎样才能做到这一点?我无法让它工作 - 当我尝试首先运行导入规范时,使用这些导入的查询字符串不起作用。

是否有可能实现这个目标 - 首先运行一次性初始化程序片段,并让以下脚本使用它?我搜索了网络,但我发现“从 Java 调用 Clojure”的示例都有不同的倾向 - 它们都集中于从 Java 执行特定的 Clojure 程序,而不是允许在 Clojure 中执行任意程序。

此外,我研究了如何将 Clojure 变量设置为特定的 Java 对象 - 我仍然不知道如何实现这一点。基本上,我希望能够将某些 Java 对象“放入”Clojure 解释器,并让以下代码使用它(理想情况下,这将是一个线程局部变量——Clojure 支持它,AFAIK)。但是如何呢?

这(使用 Clojure 编写另一个 Java 程序的“脚本”)可能吗?是否可以限制可以调用的代码?我不想开始使用自定义 ClassLoader 类和 SecurityManager 实例,但似乎如果我想阻止某些调用,这是我唯一的选择。这是正确的吗?

最佳答案

这是一个 Clojure 大放异彩的复杂问题。让我们总结一下所有要点。你想要:

  1. 评估来自用户输入的一些 Clojure 代码,
  2. 确保它是安全的,
  3. 此代码需要在您提供的上下文中运行,
  4. 您需要捕获所有输出。

1。从 Java 世界评估 Clojure 代码

如您所见,将来自 Java 世界的 RT 用于此用例很快就会显示出局限性。创建像这样的 Clojure namespace 更简单:

(ns interpreter.core)

;; This is unsafe and dangerous, we are going to make it safer in the next steps.
(defn unsafe-eval [code]
(eval (read-string code)))

并用这个最小的例子从 java 中调用它:

package interpreter;

import clojure.java.api.Clojure;
import clojure.lang.IFn;

class Runner{
public static void main(String[] args){
// Load the `require` function
IFn require = Clojure.var("clojure.core", "require");

// require the clojure namespace
require.invoke(Clojure.read("interpreter.core"));

// load the `unsafe-eval` function we crafted above
IFn unsafe_eval = Clojure.var("interpreter.core", "unsafe-eval");

// execute it
System.out.println("Result: " +unsafe_eval.invoke("(+ 1 2)"));

// => `Result: 3` get printed.
}
}

您现在可以评估来自 Java 世界的任何 Clojure 代码。

2。确保此代码是安全的

保护读取阶段

this thread 中所述,从不受信任的来源读取 Clojure 代码的安全方法是避免使用 clojure.core/read-string,而是使用 clojure.edn/read-string,这是专为此目的而设计。

保护代码

您显然不希望您的最终用户能够从您的解释器访问整个 JVM 世界。相反,您希望他们能够使用一组您可以控制的预定义操作。

由于 Clojure 代码只是数据,您可以遍历已解析的代码并根据规范/模式或更简单地使用函数对其进行验证:

(ns interpreter.core
(:require [clojure.edn :as edn]
[clojure.walk :as walk]))

;; Users are only allowed to perform operations listed in this set.
(def allowed-operations '#{+ -})

;; Users are also allowed to use lists and numbers
(defn allowed? [x]
(or (list? x) ;; Clojure code is mostly made of lists
(number? x)
(contains? allowed-operations x)))

(defn validate! [parsed-code]
(walk/postwalk (fn [x] (if (allowed? x)
x
(throw (ex-info "Unknown identifier" {:value x}))))
parsed-code))

;; This is safe as long as `allowed-operations` do not list anything sensitive
(defn eval-script [code]
(-> (edn/read-string code) ;; read safely
(validate!) ;; stop on forbidden operations or literals
(eval) ;; run
))

谨慎选择安全操作和文字取决于您。

3。提供上下文

从字符串解析时,非命名空间符号将引用当前命名空间。然后,您可以在解释器命名空间中提供函数、值或任何其他绑定(bind),或提供别名。

(ns interpreter.tools)

(defn cos [x]
(java.lang.Math/cos x))

(defn version []
"Interpreter v0.1")

相应地调整 interperter.core:

(ns interpreter.core
(:require [clojure.edn :as edn]
[clojure.walk :as walk]
[interpreter.tools :as tools]));; import your custom operations

;; Add them to the allowed operations set
(def allowed-operations '#{+ - tools/cos tools/version})

现在 tools/costools/version 函数在您的解释器中可用。

4。捕获所有输出

最好只提供纯函数作为可用操作,但我们并不总是能控制现实世界,尤其是依赖项中发生的事情。为了确保捕获 STDOUT,您可以这样重写 eval-script:

(ns interperter.tools)

(defn print-version[]
(println (version)))
(ns interpreter.core
(:require [clojure.edn :as edn]
[clojure.walk :as walk]
[interpreter.tools :as tools])
(:import java.io.StringWriter))

(def allowed-operations '#{+ - do tools/cos tools/version tools/print-version})

(defn eval-script [code]
(let [out (new java.io.StringWriter)]
(binding [*out* out] ;; redirect System.out to the `out` StringWriter
{:result (-> (edn/read-string code) ;; read safely
(validate!) ;; stop on forbidden operations or literals
(eval) ;; run
)
:out (str out)})))

让我们从 Clojure 中尝试一下:

(eval-script "(do (tools/print-version) (tools/cos 1)))")
;; => {:result 0.5403023058681398, :out "Interpreter v0.1\n"}

让我们从 Java 中尝试一下:

java -cp `lein cp` interpreter.Runner "(do (tools/print-version) (tools/cos 1)))"

=> java.lang.RuntimeException: No such namespace: tools

修复方法如下:

  1. interpreter.core 中创建函数 load-tools!
(defn load-tools! []
(require '[interpreter.tools :as tools]))
  1. 在评估脚本之前从 Java 调用一次:
IFn load_tools = Clojure.var("interpreter.core", "load-tools!");
load_tools.invoke();
IFn eval_script = Clojure.var("interpreter.core", "eval-script");
System.out.println("Result: " +eval_script.invoke(args[0]));

让我们再试一次:

java -cp `lein cp` interpreter.Runner "(do (tools/print-version) (tools/cos 1))"

=> {:result 0.5403023058681398, :out "Interpreter v0.1\n"}

这是完整的代码:https://github.com/ggeoffrey/interpreter-demo

关于clojure-java-interop - 如何使用 clojure 作为 Java 程序的脚本语言?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60321130/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com