从爪哇呼叫 Clojure

大多数 google 上关于“从 java 调用 clojure”的热门搜索结果都已经过时,建议使用 clojure.lang.RT来编译源代码。如果您已经从 Clojure 项目中构建了一个 jar 并将其包含在类路径中,那么您可以帮助解释如何从 Java 调用 Clojure 吗?

49352 次浏览

这个答案写于2010年,当时是有效的。

从 Java 调用什么样的代码?如果使用 gen-class 生成了类,那么只需调用它。如果要从脚本调用函数,请参考 下面的例子

如果你想在 Java 中从字符串中计算代码,那么你可以使用下面的代码:

import clojure.lang.RT;
import clojure.lang.Var;
import clojure.lang.Compiler;
import java.io.StringReader;


public class Foo {
public static void main(String[] args) throws Exception {
// Load the Clojure script -- as a side effect this initializes the runtime.
String str = "(ns user) (defn foo [a b]   (str a \" \" b))";


//RT.loadResourceScript("foo.clj");
Compiler.load(new StringReader(str));


// Get a reference to the foo function.
Var foo = RT.var("user", "foo");


// Call it!
Object result = foo.invoke("Hi", "there");
System.out.println(result);
}
}

还可以使用 AOT 编译来创建表示 clojure 代码的类文件。阅读 Clojure API 文档中关于编译、 gen-class 和朋友的文档,了解如何做到这一点的详细信息,但实质上,您将创建一个类,该类为每个方法调用调用 Clojure 函数。

另一种选择是使用新的 defprotocol 和 deftype 功能,这也需要 AOT 编译,但提供更好的性能。我还不知道具体该怎么做,但是邮件列表上的一个问题可能会起作用。

更新 : 自从这个答案发布以来,一些可用的工具已经发生了变化。在原始答案之后,会有一个更新,其中包括如何使用当前工具构建示例的信息。

它不像编译成 jar 和调用内部方法那么简单。不过,似乎确实有一些小技巧可以让这一切奏效。下面是一个可以编译成 jar 的简单 Clojure 文件示例:

(ns com.domain.tiny
(:gen-class
:name com.domain.tiny
:methods [#^{:static true} [binomial [int int] double]]))


(defn binomial
"Calculate the binomial coefficient."
[n k]
(let [a (inc n)]
(loop [b 1
c 1]
(if (> b k)
c
(recur (inc b) (* (/ (- a b) b) c))))))


(defn -binomial
"A Java-callable wrapper around the 'binomial' function."
[n k]
(binomial n k))


(defn -main []
(println (str "(binomial 5 3): " (binomial 5 3)))
(println (str "(binomial 10042 111): " (binomial 10042 111)))
)

如果你运行它,你应该看到这样的东西:

(binomial 5 3): 10
(binomial 10042 111): 49068389575068144946633777...

这是一个 Java 程序,它调用 tiny.jar中的 -binomial函数。

import com.domain.tiny;


public class Main {


public static void main(String[] args) {
System.out.println("(binomial 5 3): " + tiny.binomial(5, 3));
System.out.println("(binomial 10042, 111): " + tiny.binomial(10042, 111));
}
}

它的输出是:

(binomial 5 3): 10.0
(binomial 10042, 111): 4.9068389575068143E263

神奇的第一步是在 gen-class语句中使用 :methods关键字。这似乎是访问 Clojure 函数所必需的,类似于 Java 中的静态方法。

第二件事是创建一个可以被 Java 调用的包装函式。请注意,-binomial的第二个版本在它的前面有一个破折号。

当然,Clojure jar 本身必须位于类路径上。

更新 : 使用以下工具重新测试了这个答案:

  • Clojure 1.5.1
  • 雷宁根2.1.3
  • JDK 1.7.0更新25

Clojure 部分

首先使用 Leiningen 创建一个项目和相关的目录结构:

C:\projects>lein new com.domain.tiny

现在,切换到项目目录。

C:\projects>cd com.domain.tiny

在项目目录中,打开 project.clj文件并编辑它,使其内容如下所示。

(defproject com.domain.tiny "0.1.0-SNAPSHOT"
:description "An example of stand alone Clojure-Java interop"
:url "http://clarkonium.net/2013/06/java-clojure-interop-an-update/"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.5.1"]]
:aot :all
:main com.domain.tiny)

现在,确保所有的依赖项(Clojure)都可用。

C:\projects\com.domain.tiny>lein deps

此时,您可能会看到一条关于下载 Clojure jar 的消息。

现在编辑 Clojure 文件 C:\projects\com.domain.tiny\src\com\domain\tiny.clj,使其包含原始答案中显示的 Clojure 程序。(这个文件是在 Leiningen 创建项目时创建的。)

这里的许多神奇之处在于名称空间声明。:gen-class告诉系统用一个名为 binomial的静态方法创建一个名为 com.domain.tiny的类,这个函数接受两个整数参数并返回一个 double。有两个类似命名的函数 binomial,一个传统的 Clojure 函数,以及可以从 Java 访问的 -binomial和包装器。注意函数名 -binomial中的连字符。默认前缀是连字符,但如果需要,可以将其更改为其他内容。-main函数只是对二项式函数进行几次调用,以确保我们得到的结果是正确的。为此,编译该类并运行该程序。

C:\projects\com.domain.tiny>lein run

您应该会看到原始答案中显示的输出。

现在把它打包在一个罐子里,放在一个方便的地方。

C:\projects\com.domain.tiny>lein jar
Created C:\projects\com.domain.tiny\target\com.domain.tiny-0.1.0-SNAPSHOT.jar
C:\projects\com.domain.tiny>mkdir \target\lib


C:\projects\com.domain.tiny>copy target\com.domain.tiny-0.1.0-SNAPSHOT.jar target\lib\
1 file(s) copied.


C:\projects\com.domain.tiny>copy "C:<path to clojure jar>\clojure-1.5.1.jar" target\lib\
1 file(s) copied.

Java 部分

Leiningen 有一个内置任务 lein-javac,它应该能够帮助进行 Java 编译。不幸的是,它似乎在2.1.3版本中被破坏了。它找不到安装的 JDK,也找不到 Maven 存储库。这两个路径在我的系统上都有嵌入空间。我想这就是问题所在。任何 JavaIDE 都可以处理编译和打包。但是对于这个岗位,我们要用老办法,在命令行中完成。

首先创建文件 Main.java,其内容显示在原始答案中。

编译 java 部分

javac -g -cp target\com.domain.tiny-0.1.0-SNAPSHOT.jar -d target\src\com\domain\Main.java

现在创建一个包含一些元信息的文件,以添加到我们要构建的 jar 中。在 Manifest.txt中,添加以下文本

Class-Path: lib\com.domain.tiny-0.1.0-SNAPSHOT.jar lib\clojure-1.5.1.jar
Main-Class: Main

现在将它们打包成一个大的 jar 文件,包括 Clojure 程序和 Clojure jar。

C:\projects\com.domain.tiny\target>jar cfm Interop.jar Manifest.txt Main.class lib\com.domain.tiny-0.1.0-SNAPSHOT.jar lib\clojure-1.5.1.jar

运行程序:

C:\projects\com.domain.tiny\target>java -jar Interop.jar
(binomial 5 3): 10.0
(binomial 10042, 111): 4.9068389575068143E263

输出与 Clojure 单独生成的输出基本相同,但是结果已经转换为 Java double。

如前所述,JavaIDE 可能会处理混乱的编译参数和打包。

在 JVM 之上使用其他语言的其他技术是,为想要调用的函数声明一个接口,然后使用“代理”函数创建实现它们的实例。

编辑: 大约三年前,我写下了这个答案。在 Clojure 1.6中,有一个恰当的 API 用于从 Java 调用 Clojure。请 Alex Miller 的回答的最新信息。

2011年的原始答案:

在我看来,最简单的方法(如果不生成带 AOT 编译的类)是使用 clojure.lang.RT 访问 clojure 中的函数。有了它,你可以模仿在 Clojure 中的操作(不需要以特殊的方式编译东西) :

;; Example usage of the "bar-fn" function from the "foo.ns" namespace from Clojure
(require 'foo.ns)
(foo.ns/bar-fn 1 2 3)

在 Java 中:

// Example usage of the "bar-fn" function from the "foo.ns" namespace from Java
import clojure.lang.RT;
import clojure.lang.Symbol;
...
RT.var("clojure.core", "require").invoke(Symbol.intern("foo.ns"));
RT.var("foo.ns", "bar-fn").invoke(1, 2, 3);

它在 Java 中有点冗长,但是我希望它清楚地表明这些代码片段是等价的。

只要 Clojure 和 Clojure 代码的源文件(或已编译的文件)位于类路径上,这就应该可以工作。

我同意 Clartaq 的回答,但我觉得初学者也可以使用:

  • 有关如何实际运行此程序的详细信息
  • 最新的 Clojure 1.3和 leiningen 的最新版本的信息。
  • 一个 Clojure jar,它还包含一个 main 函数,因此它可以作为库运行独立的 或者链接。

所以我用 这篇博文把这些都讲了一遍。

Clojure 代码如下:

(ns ThingOne.core
(:gen-class
:methods [#^{:static true} [foo [int] void]]))


(defn -foo [i] (println "Hello from Clojure. My input was " i))


(defn -main [] (println "Hello from Clojure -main." ))

Leiningen 1.7.1项目设置如下:

(defproject ThingOne "1.0.0-SNAPSHOT"
:description "Hello, Clojure"
:dependencies [[org.clojure/clojure "1.3.0"]]
:aot [ThingOne.core]
:main ThingOne.core)

Java 代码如下:

import ThingOne.*;


class HelloJava {
public static void main(String[] args) {
System.out.println("Hello from Java!");
core.foo (12345);
}
}

或者你也可以从 Github 上的这个项目获得所有的代码。

这适用于 Clojure1.5.0:

public class CljTest {
public static Object evalClj(String a) {
return clojure.lang.Compiler.load(new java.io.StringReader(a));
}


public static void main(String[] args) {
new clojure.lang.RT(); // needed since 1.5.0
System.out.println(evalClj("(+ 1 2)"));
}
}

从 Clojure 1.6.0开始,有了一种新的首选方法来加载和调用 Clojure 函数。现在,这种方法比直接调用 RT 更可取(并且取代了这里的许多其他应答)。Javadoc 是 给你-主入口点是 clojure.java.api.Clojure

查找并调用 Clojure 函数:

IFn plus = Clojure.var("clojure.core", "+");
plus.invoke(1, 2);

clojure.core中的函数是自动加载的。其他名称空间可以通过要求加载:

IFn require = Clojure.var("clojure.core", "require");
require.invoke(Clojure.read("clojure.set"));

IFn可以传递给高阶函数,例如下面的例子将 plus传递给 read:

IFn map = Clojure.var("clojure.core", "map");
IFn inc = Clojure.var("clojure.core", "inc");
map.invoke(inc, Clojure.read("[1 2 3]"));

Clojure 中的大多数 IFn都引用函数。但是,有一些是指非函数数据值。要访问它们,请使用 deref而不是 fn:

IFn printLength = Clojure.var("clojure.core", "*print-length*");
IFn deref = Clojure.var("clojure.core", "deref");
deref.invoke(printLength);

有时候(如果使用 Clojure 运行时的其他部分) ,您可能需要确保 Clojure 运行时正确初始化-调用 Clojure 类上的方法就足够了。如果您不需要在 Clojure 上调用一个方法,那么简单地导致类加载就足够了(过去有一个类似的建议来加载 RT 类; 现在这是首选) :

Class.forName("clojure.java.api.Clojure")

如果用例是在 Java 应用程序中包含一个用 Clojure 构建的 JAR,我发现为两个世界之间的接口提供一个单独的名称空间是有益的:

(ns example-app.interop
(:require [example-app.core :as core])


;; This example covers two-way communication: the Clojure library
;; relies on the wrapping Java app for some functionality (through
;; an interface that the Clojure library provides and the Java app
;; implements) and the Java app calls the Clojure library to perform
;; work. The latter case is covered by a class provided by the Clojure lib.
;;
;; This namespace should be AOT compiled.


;; The interface that the java app can implement
(gen-interface
:name com.example.WeatherForecast
:methods [[getTemperature [] Double]])


;; The class that the java app instantiates
(gen-class
:name com.example.HighTemperatureMailer
:state state
:init init
;; Dependency injection - take an instance of the previously defined
;; interface as a constructor argument
:constructors {[com.example.WeatherForecast] []}
:methods [[sendMails [] void]])


(defn -init [weather-forecast]
[[] {:weather-forecast weather-forecast}])


;; The actual work is done in the core namespace
(defn -sendMails
[this]
(core/send-mails (.state this)))

核心命名空间可以使用注入的实例来完成其任务:

(ns example-app.core)


(defn send-mails
[{:keys [weather-forecast]}]
(let [temp (.getTemperature weather-forecast)] ...))

为了测试的目的,接口可以被削弱:

(example-app.core/send-mails
(reify com.example.WeatherForecast (getTemperature [this] ...)))