从Java调用Clojure


165

对于“从Java调用Clojure”而言,大多数Google热门歌曲都已过时,建议您使用它clojure.lang.RT来编译源代码。假设您已经从Clojure项目构建了一个jar并将其包含在类路径中,那么您能否帮助您清楚地说明如何从Java调用Clojure?


8
我不知道每次编译源代码都是“过时的”。这是设计决定。我现在正在这样做,因为它可以将Clojure代码集成到旧版Java Netbeans项目中。将Clojure添加为库,添加Clojure源文件,设置调用,并立即获得Clojure支持,而无需进行多个编译/链接步骤!每个应用启动的时间只有一秒钟的延迟。
Brian Knoblauch


2
有关Clojure 1.8.0的最新信息,请参见– Clojure现在具有编译器直接链接。
TWR Cole

Answers:


167

更新:自发布此答案以来,某些可用工具已更改。原始答案之后,进行了更新,其中包含有关如何使用当前工具构建示例的信息。

它不像编译成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

魔术的第一步是:methodsgen-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,该函数接受两个整数参数并返回一个double值。有两个相似命名的函数binomial,一个传统的Clojure函数和一个-binomial可从Java访问的包装器。注意函数名称中的连字符-binomial。默认前缀是连字符,但是如果需要,可以将其更改为其他名称。该-main函数仅对二项式函数进行了两次调用,以确保我们得到正确的结果。为此,请编译类并运行程序。

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

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

现在将其包装在罐子中,并放在方便的地方。也将Clojure罐子复制到那里。

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”宏的示例放在clojuredocs.org上吗?2.“:methods [#^ {:static true} [binomial [int int] double]]]”前面的#^是什么(我是新手)?
Belun

5
@Belun,请确保您可以使用它为例-我很受宠若惊。“#^ {:static true}”将一些元数据附加到该函数,指示二项式是静态函数。在这种情况下是必需的,因为在Java方面,我们从main调用函数-静态函数。如果二项式不是静态的,则在Java端编译主要函数将产生一条错误消息,内容为“无法从静态上下文引用非静态方法binomial(int,int)”。在“对象向导”站点上还有其他示例。
clartaq 2010年

4
这里没有提到一件重要的事情-将Clojure文件编译为Java类,您需要:(编译'com.domain.tiny)
Domchi

您如何AOT编译Clojure源代码?这就是我卡住的地方。
马修·波士顿

@MatthewBoston在编写答案时,我使用了Enclojure,这是NetBeans IDE的插件。现在,我可能会使用Leiningen,但尚未尝试过或测试过它。
clartaq 2012年

119

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

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

IFnClojure中的大多数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") 

1
使用这种方法,脚本不能使用引号'()并需要其他内容。有解决方案吗?
雷纳托

2
我认为只有两种特殊形式不作为var存在。一种解决方法是通过访问它Clojure.read("'(1 2 3")。尽管提供Clojure.quote()或使其作为var工作,但将其提交为增强请求是合理的。
亚历克斯·米勒

1
@Renato无需引用任何内容,因为Clojure的评估规则始终未评估任何内容。如果您想要一个包含数字1-3的列表,则'(1 2 3)可以编写类似的文字,而不是编写Clojure.var("clojure.core", "list").invoke(1,2,3)。答案已经有一个使用示例require:它就像其他任何变量一样。
amalloy

34

编辑这个答案写于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'd var(即(def my-var 10)),请稍稍扩展一下,请使用this RT.var("namespace", "my-var").deref()
伊万·科布利克

不先添加就对我不起作用RT.load("clojure/core");。有什么奇怪的行为?
hsestupin 2012年

这有效,并且与我一直在使用的类似;不知道这是否是最新技术。我不确定使用Clojure 1.4或1.6。
严景贤

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。
亚历克斯·米勒

当时的答案还不错,但是我想把stackoverflow.com/a/23555959/1756702奖励为Clojure 1.6的权威答案。明天会再试...
A. Webb

亚历克斯的回答确实值得赏赐!如果需要,请告诉我是否可以协助转移赏金。
raek

@raek我不介意我因咖啡因而过高的扳机手指给了你一点好处。希望再次在Clojure标签附近见到您。
A. Webb 2014年

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?会不会导致程序不再与平台无关?
爱德华

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


-1

您还可以使用AOT编译来创建代表Clojure代码的类文件。阅读Clojure API文档中有关编译,gen-class和朋友的文档,以获取有关如何执行此操作的详细信息,但从本质上讲,您将创建一个类,该类为每次方法调用均调用clojure函数。

另一种选择是使用新的defprotocol和deftype功能,这也将需要AOT编译,但提供更好的性能。我尚不知道如何执行此操作的详细信息,但是邮件列表上的问题可能可以解决问题。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.