【问题标题】:Java -> Clojure -> JavaJava -> Clojure -> Java
【发布时间】:2013-02-20 19:04:49
【问题描述】:

我正在尝试使用 Clojure 作为宿主 Java 程序的脚本语言。这个想法是最终用户将能够编写将调用特定于域的 Java API 的 Clojure 脚本代码。在运行时,宿主 Java 程序将评估最终用户的 Clojure 脚本(依次调用域 API)。所以我从一个非常简单的原型开始探索地形。

package a.problem.domain;

public class Domain {

    public Domain() { }

    public String defaultMsg() {
        return "default";
    }

    public String passBackMsg(String s) {
        return s;
    }
}

宿主 Java 程序

(为简单起见,对最终用户的 Clojure 脚本进行了硬编码)

String script = "(do                                    "+
                "  (import '(a.problem.domain Domain))  "+
                "  (.defaultMsg (Domain.))              "+
                ")                                      ";
System.out.println(RT.var("clojure.core", "eval").invoke(RT.var("clojure.core","read-string").invoke(script)));

(代码 sn-p 取自here

到目前为止一切顺利。

但是我找不到调用第二种方法(需要参数的方法)的方法。相反,我求助于在运行时动态生成 Clojure 脚本,并用表示调用域方法 passBackMsg 的结果的文字替换占位符。显然,这是不能令人满意的并且不会走得太远(如果我想将 java.sql.Connection 传递给我的 Clojure 脚本怎么办?)。

那么,如何从宿主 Java 程序调用 passBackMsg 方法?

当我尝试以下操作时:

String script = "(ns foo)                                   "+ 
                "(import '(a.problem.domain Domain))        "+
                "(defn numberToString [s]  (                "+
                "  (.passBackMsg (Domain.) s)               "+
                "))                                         ";
RT.var("clojure.core", "eval").invoke(RT.var("clojure.core","read-string").invoke(script)); // line-A
System.out.println(RT.var("foo", "numberToString").invoke(33)); // line-B

我明白了:

java.lang.IllegalStateException: Can't change/establish root binding of: *ns* with set

... 在 A 行。当我尝试不使用 ns 并使用:

RT.var("user", "numberToString").invoke(33)

(“用户”是一个疯狂的猜测,因为我没有看到没有命名空间参数的 var 方法)

我得到一个:

java.lang.IllegalStateException: Attempting to call unbound fn: #'user/numberToString"

... 在 B 行。

【问题讨论】:

  • 难道你不能通过一个script 字符串然后invoke 它就像你调用read-string 一样,通过解引用它的变量来defn 一个函数?
  • 我试过了,但我无法让它工作。首先它会接受 defn 并且必须使用 (let (fn )) 代替。不是阻塞问题,但很奇怪。然后我遇到了其他问题。我不想复制失败的代码以免使帖子太长,但在您的消息之后我可能会添加它。
  • 我的一个项目中有这个工作:clojure.lang.RT.load(filename); clojure.lang.RT.var(mainNamespace, "main").invoke( stringArg ); 从文件加载不应该有根本的区别。
  • 更新 defn:事实证明,使用 defn 毕竟不是问题,并且已经更新了显示的脚本,因此,“无法更改/建立根绑定:ns with set" 仍然存在。
  • 您绝对需要一个do 来围绕所有内容,因为read-string 只读取一个表单。此外,read-string 不会评估它,它只是返回读入的内容。这与我使用load 的技术形成对比。

标签: java clojure


【解决方案1】:

试试这个:

String script = "(do                                    "+
                "  (import '(a.problem.domain Domain))  "+
                "  (fn [s]                " +
                "   (.passBackMsg (Domain.) s)               "+
                "))                                         ";

IFn fn = (IFn)RT.var("clojure.core", "eval").invoke(RT.var("clojure.core","read-string").invoke(script));

fn.invoke("hello");

更新: 下面的示例代码工作正常:

package hello_clj;

import clojure.lang.RT;
import clojure.lang.IFn;

public class Main {

    public String passBackMsg(String s) {
        return s;
    }

    public static void main(String[] args) {
        String script = "(do (import 'hello_clj.Main) (fn [s] " + 
                        "(.passBackMsg (Main.) s) ))";

        IFn fn = (IFn)RT.var("clojure.core", "eval").invoke(RT.var("clojure.core","read-string").invoke(script));
        System.out.print(fn.invoke("Hello"));
    }

}

【讨论】:

  • 没有import 会更可靠,因为它可能不会在与评估相同的形式内生效。你试过了吗?
  • java.lang.ClassCastException: java.lang.String 不能转换为 clojure.lang.IFn(在 A 行抛出)
  • 使用运行示例代码更新了答案。旧代码在 fn 周围有额外的参数,这就是问题所在。