Вызов clojure из Java
Большинство популярных хитов Google для "Call Clojure из Java" устарели и рекомендуют использовать clojure.lang.RT
скомпилировать исходный код. Не могли бы вы помочь с четким объяснением того, как вызывать Clojure из Java, при условии, что вы уже создали jar из проекта Clojure и включили его в путь к классам?
9 ответов
Обновление: с тех пор, как этот ответ был опубликован, некоторые из доступных инструментов изменились. После первоначального ответа есть обновление, включающее информацию о том, как построить пример с использованием текущих инструментов.
Это не так просто, как компилирование в 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-программа, которая вызывает -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.
Обновление: этот ответ был повторно протестирован с использованием следующих инструментов:
- 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.
Теперь отредактируйте файл Clojure C:\projects\com.domain.tiny\src\com\domain\tiny.clj
такой, что он содержит программу Clojure, показанную в исходном ответе. (Этот файл был создан, когда Лейнинген создал проект.)
Большая часть волшебства здесь находится в объявлении пространства имен. :gen-class
говорит системе создать класс с именем com.domain.tiny
с одним статическим методом под названием binomial
, функция, принимающая два целочисленных аргумента и возвращающая двойное число. Есть две одноименные функции 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 часть
Лейнинген имеет встроенную задачу, 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
Теперь создайте файл с некоторой метаинформацией для добавления в банку, которую мы хотим построить. В Manifest.txt
, добавьте следующий текст
Class-Path: lib\com.domain.tiny-0.1.0-SNAPSHOT.jar lib\clojure-1.5.1.jar
Main-Class: Main
Теперь упакуйте все это в один большой файл jar, включая нашу программу Clojure и jar Clojure.
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, вероятно, позаботится о беспорядочных аргументах компиляции и упаковке.
Начиная с 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]"));
Наиболее IFn
s в Clojure относятся к функциям. Некоторые, однако, ссылаются на нефункциональные значения данных. Чтобы получить доступ к ним, используйте 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 году, и работал в то время. Смотрите ответ Алекса Миллера для более современного решения.
Какой код вызывается из 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 именно для вызова Clojure из Java. Пожалуйста, ответ Алекса Миллера для получения актуальной информации: /questions/18286626/vyizov-clojure-iz-java/18286654#18286654
Оригинальный ответ с 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)
И на Яве:
// 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 находятся в пути к классам.
Я согласен с ответом клартака, но я чувствовал, что новички также могут использовать:
- пошаговая информация о том, как на самом деле получить это работает
- информация, которая актуальна для Clojure 1.3 и последних версий leiningen.
- банка Clojure, который также включает в себя основную функцию, поэтому он может быть запущен автономно или связан как библиотека.
Итак, я рассказал обо всем этом в этом блоге.
Код 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)"));
}
}
Если сценарий использования должен включать JAR, созданный с помощью Clojure, в приложении Java, я обнаружил, что было бы полезно иметь отдельное пространство имен для интерфейса между двумя мирами:
(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, - это объявить интерфейс для функций, которые вы хотите вызвать, и затем использовать функцию "proxy" для создания экземпляра, который их реализует.
Вы также можете использовать компиляцию AOT для создания файлов классов, представляющих ваш код clojure. Прочтите документацию о компиляции, классе gen и друзей в документации API Clojure, чтобы узнать, как это сделать, но по сути вы создадите класс, который будет вызывать функции clojure для каждого вызова метода.
Другой альтернативой является использование новых функций defprotocol и deftype, которые также потребуют компиляции AOT, но обеспечат более высокую производительность. Я пока не знаю деталей, как это сделать, но вопрос в списке рассылки, вероятно, поможет.