从Java调用Clojure

176

大多数与“从Java调用Clojure”相关的谷歌搜索结果已经过时,建议使用clojure.lang.RT来编译源代码。请您提供一个清晰的解释,假设您已经构建了一个Clojure项目的JAR包,并将其包含在类路径中,如何从Java中调用Clojure?


8
我不认为每次编译源代码都是"过时的"。这是一种设计决策。现在我正在这样做,因为这使得将Clojure代码集成到旧的Java Netbeans项目中变得非常简单。只需将Clojure作为库添加,添加Clojure源文件,设置调用,即可立即支持Clojure,而无需进行多个编译/链接步骤!这样做的代价是每次启动应用程序会延迟几分之一秒。 - Brian Knoblauch
请参阅Clojure 1.6.0的最新版本 - A. Webb
2
请查看Clojure 1.8.0的最新版本——Clojure现在具有编译器直接链接功能。 - T.W.R. Cole
9个回答

179

更新:自此回答发布以来,某些可用工具已发生了变化。在原始回答之后,将包括有关如何使用当前工具构建示例的信息。

并不像编译成jar文件然后调用内部方法那么简单。但是似乎有一些技巧可以使它们都起作用。这里有一个示例,演示如何将一个简单的Clojure文件编译为jar文件:

(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关键字。这似乎是必需的,让你可以像Java中的静态方法一样访问Clojure函数。

第二个要做的事情是创建一个包装函数,可以被Java调用。注意,-binomial的第二个版本前面有一个破折号。

当然,Clojure jar本身必须在类路径上。本例中使用了Clojure-1.1.0 jar。

更新:此答案已经使用以下工具进行了重新测试:

  • Clojure 1.5.1
  • Leiningen 2.1.3
  • JDK 1.7.0 Update 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函数,另一个是Java可访问的-binomial函数和包装器。请注意函数名称-binomial中的连字符。默认前缀是连字符,但如果需要,可以将其更改为其他内容。 -main函数只是对二项式函数进行了几次调用,以确保我们获得了正确的结果。要做到这一点,请编译类并运行程序。

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

你应该能够看到原始答案中显示的输出。

现在将其打包成一个jar文件并放在方便的地方。同时也复制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 double。

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


4
  1. 我可以将您的示例用作“ns”宏的例子放在http://clojuredocs.org上吗?
  2. “#^”在“:methods [# ^ {:static true} [二项式[int int] double]]”前面是什么意思(我是新手)?
- Belun
5
@Belun,当然你可以将它用作示例-我很荣幸。 "#^ {: static true}"将一些元数据附加到函数上,表示binomial是静态函数。在这种情况下,它是必需的,因为在Java方面,我们从main中调用该函数 - 一个静态函数。如果binomial不是静态的,则在Java侧编译main函数将生成有关“非静态方法binomial(int,int)无法从静态上下文引用”的错误消息。在Object Mentor网站上还有其他示例。 - clartaq
4
这篇文章中没有提到一个非常重要的事情——若要将Clojure文件编译成Java类,你需要执行以下操作:(compile 'com.domain.tiny)。 - Domchi
你好,你是如何对Clojure源代码进行AOT编译的?这就是我卡住的地方。 - Matthew Boston
@MatthewBoston 在回答写作时,我使用的是Enclojure,它是NetBeans IDE的插件。现在,我可能会使用Leiningen,但尚未尝试或测试过它。 - clartaq

138

从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可以被传递给高阶函数,例如下面的示例将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") 

1
使用这种方法,脚本无法使用引号'()和require事物。有解决方案吗? - Renato
2
我认为只有几个特殊形式不存在于变量中。一个解决方法是通过 Clojure.read("'(1 2 3") 访问它。不过,提供 Clojure.quote() 或使其作为变量工作,将其作为增强请求进行归档是合理的。 - Alex Miller
1
@Renato 不需要引用任何东西,因为Clojure的求值规则根本不会对任何东西进行求值。如果您想要一个包含数字1-3的列表,则不要编写'(1 2 3),而是编写类似于Clojure.var("clojure.core", "list").invoke(1,2,3)的内容。答案已经有了如何使用require的示例:它只是像其他任何变量一样的变量。 - amalloy

35

编辑:这个答案是在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);
  }
}

1
如果您想要访问命名空间中定义的变量(例如(def my-var 10)),请使用RT.var("namespace", "my-var").deref() - Ivan Koblik
没有在开头添加 RT.load("clojure/core"); 就不能正常工作,这是为什么奇怪的行为? - hsestupin
这个方法可行,和我之前使用的类似;不确定是否是最新技术。我可能在使用Clojure 1.4或1.6,不太确定。 - Yan King Yin

12

编辑:我在近三年前写了这篇答案。在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源文件(或编译文件)在类路径中,这段代码就能正常工作。


1
这个建议已经过时了,从Clojure 1.6开始,请使用clojure.java.api.Clojure。 - Alex Miller
当时也不是一个坏答案,但我本来想将https://dev59.com/ZHI95IYBdhLWcg3wtwf4#23555959作为Clojure 1.6的权威答案。明天再试一次... - A. Webb
Alex的回答确实值得获得奖励!如果需要,我可以帮忙转移奖励,请告知。 - raek
@raek,我不介意你从我的过度咖啡因的触发手指中获得了一点奖励。希望再次在Clojure标签下见到你。 - A. Webb

10

我同意clartaq的回答,但我认为初学者也可以使用以下内容:

  • 逐步说明如何实际运行此代码
  • 针对Clojure 1.3和leiningen的最新版本提供的信息
  • 一个包含主函数的Clojure jar,以便可以作为独立的应用程序运行或链接为库。

所以我在这篇博客文章中涵盖了所有这些内容。

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上的此项目获取所有代码。


你为什么使用AOT?这不会导致程序不再具有平台独立性吗? - Edward

3
这适用于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)"));
    }
}

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] ...)))

0

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


-1
你还可以使用AOT编译来创建代表你的Clojure代码的类文件。阅读关于编译、gen-class和Clojure API文档中的其他相关内容,了解如何进行此操作的详细信息。基本上,你将创建一个类,在每个方法调用时调用Clojure函数。
另一种选择是使用新的defprotocol和deftype功能,这也需要AOT编译,但提供更好的性能。我还不知道如何具体操作,但在邮件列表上提问可能会有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接