【问题标题】:How can I use clojure as scripting language for a Java program?如何使用 clojure 作为 Java 程序的脚本语言?
【发布时间】:2020-06-04 20:22:51
【问题描述】:

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

我可以让它工作,通过使用“RT.readString”,通过查询 ```Var EVAL=RT.var("clojure.core","eval")´´´ 和访问“eval”函数使用 EVAL 计算之前 RT.readString 返回的结果。

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

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

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

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

【问题讨论】:

    标签: clojure-java-interop


    【解决方案1】:

    这是一个复杂的问题,Clojure 可以真正发挥作用。让我们总结一下所有要点。你想要:

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

    1。从 Java 世界评估 Clojure 代码

    如您所见,在此用例中使用 Java 世界中的 RT 很快就会显示出局限性。 创建一个像这样的 Clojure 命名空间更简单:

    (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。提供上下文

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

    (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

    【讨论】:

      猜你喜欢
      • 2012-11-24
      • 2011-09-08
      • 2013-04-13
      • 2016-04-15
      • 1970-01-01
      • 2019-12-25
      • 2021-08-31
      • 1970-01-01
      • 2014-03-13
      相关资源
      最近更新 更多