大多数关于“从 java 调用 clojure”的热门谷歌点击都已过时,建议使用clojure.lang.RT
它来编译源代码。假设您已经从 Clojure 项目构建了一个 jar 并将其包含在类路径中,您能否帮助清楚地解释如何从 Java 调用 Clojure?
9 回答
更新:自从发布此答案以来,一些可用的工具已经改变。在原始答案之后,有一个更新,包括有关如何使用当前工具构建示例的信息。
它并不像编译到 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 程序,它调用-binomial
.tiny.jar
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
第一个魔术是在语句中使用:methods
关键字。gen-class
这似乎是让您访问 Clojure 函数(类似于 Java 中的静态方法)所必需的。
第二件事是创建一个可以被 Java 调用的包装函数。请注意,第二个版本的-binomial
前面有一个破折号。
当然,Clojure jar 本身必须在类路径上。此示例使用 Clojure-1.1.0 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
告诉系统创建一个名为 的类,该类使用一个名为的com.domain.tiny
静态方法binomial
,该函数接受两个整数参数并返回一个双精度值。有两个名称相似的函数binomial
,一个是传统的 Clojure 函数,一个是-binomial
可从 Java 访问的包装器。注意函数名称中的连字符-binomial
。默认前缀是连字符,但如果需要,可以将其更改为其他前缀。该-main
函数只是对二项式函数进行了几次调用,以确保我们得到正确的结果。为此,编译类并运行程序。
C:\projects\com.domain.tiny>lein run
您应该看到原始答案中显示的输出。
现在把它装在一个罐子里,放在方便的地方。也将 Clojure jar 复制到那里。
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 存储库。两者的路径在我的系统上都有嵌入空间。我认为这就是问题所在。任何 Java IDE 也可以处理编译和打包。但是对于这篇文章,我们将使用老式的方法并在命令行中进行操作。
首先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 双精度。
如前所述,Java IDE 可能会处理杂乱的编译参数和打包。
从 Clojure 1.6.0 开始,有一种新的首选方式来加载和调用 Clojure 函数。这种方法现在比直接调用 RT 更受欢迎(并取代了这里的许多其他答案)。javadoc 在这里- 主要入口点是clojure.java.api.Clojure
.
要查找和调用 Clojure 函数:
IFn plus = Clojure.var("clojure.core", "+");
plus.invoke(1, 2);
中的函数clojure.core
会自动加载。其他命名空间可以通过 require 加载:
IFn require = Clojure.var("clojure.core", "require");
require.invoke(Clojure.read("clojure.set"));
IFn
s 可以传递给高阶函数,例如下面的例子传递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
s 都是指函数。然而,少数是指非功能数据值。要访问这些,请使用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")
编辑这个答案写于 2010 年,当时有效。有关更现代的解决方案,请参阅 Alex Miller 的答案。
从 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);
}
}
编辑:我差不多三年前写了这个答案。在 Clojure 1.6 中,有一个适当的 API 专门用于从 Java 调用 Clojure。请亚历克斯米勒的回答以获取最新信息。
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 上获取该项目的所有代码。
这适用于 Clojure 1.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)"));
}
}
如果用例是在 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] ...)))
在 JVM 之上也可以与其他语言一起使用的其他技术是为要调用的函数声明一个接口,然后使用“代理”函数创建实现它们的实例。
您还可以使用 AOT 编译来创建代表您的 clojure 代码的类文件。阅读 Clojure API 文档中有关编译、gen-class 和朋友的文档以了解有关如何执行此操作的详细信息,但本质上您将创建一个为每个方法调用调用 clojure 函数的类。
另一种选择是使用新的 defprotocol 和 deftype 功能,这也需要 AOT 编译但提供更好的性能。我还不知道如何做到这一点的细节,但邮件列表上的一个问题可能会解决问题。