web-dev-qa-db-fra.com

Appel de clojure depuis java

La plupart des meilleurs résultats de Google pour "Calling clojure from Java" sont obsolètes et recommandent d'utiliser clojure.lang.RT pour compiler le code source. Pourriez-vous nous aider avec une explication claire de la procédure à suivre pour appeler Clojure depuis Java en supposant que vous avez déjà créé un fichier JAR à partir du projet Clojure et que vous l'avez inclus dans le chemin d'accès aux classes?

160
Arthur Ulfeldt

Mise à jour: Depuis que cette réponse a été publiée, certains outils disponibles ont été modifiés. Après la réponse initiale, il existe une mise à jour incluant des informations sur la construction de l'exemple avec les outils actuels.

Ce n'est pas aussi simple que de compiler dans un bocal et d'appeler les méthodes internes. Il semble y avoir quelques astuces pour que tout fonctionne bien. Voici un exemple de fichier Clojure simple pouvant être compilé dans un fichier 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)))
)

Si vous l'exécutez, vous devriez voir quelque chose comme:

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

Et voici un programme Java qui appelle le -binomial fonction dans le 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));
    }
}

Sa sortie est:

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

Le premier morceau de magie utilise le :methods mot-clé dans le gen-class déclaration. Cela semble être nécessaire pour vous permettre d'accéder à la fonction Clojure, une méthode similaire aux méthodes statiques en Java.

La deuxième chose à faire est de créer une fonction wrapper pouvant être appelée par Java. Notez que la deuxième version de -binomial a un tiret devant.

Et bien sûr, le pot Clojure lui-même doit être sur le chemin de la classe. Cet exemple utilisait le fichier Clojure-1.1.0.

Mise à jour: Cette réponse a été testée à nouveau à l'aide des outils suivants:

  • Clojure 1.5.1
  • Leiningen 2.1.3
  • JDK 1.7.0 Mise à jour 25

La partie Clojure

Commencez par créer un projet et une structure de répertoires associée à l'aide de Leiningen:

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

Maintenant, passez au répertoire du projet.

C:\projects>cd com.domain.tiny

Dans le répertoire du projet, ouvrez le project.clj fichier et modifiez-le de manière à ce que le contenu soit celui indiqué ci-dessous.

(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)

Maintenant, assurez-vous que toutes les dépendances (Clojure) sont disponibles.

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

Vous verrez peut-être un message sur le téléchargement du pot Clojure à ce stade.

Maintenant, éditez le fichier Clojure C:\projects\com.domain.tiny\src\com\domain\tiny.clj tel qu'il contienne le programme Clojure indiqué dans la réponse d'origine. (Ce fichier a été créé lors de la création du projet par Leiningen.)

Une grande partie de la magie ici réside dans la déclaration d'espace de noms. Le :gen-class indique au système de créer une classe nommée com.domain.tiny avec une seule méthode statique appelée binomial, une fonction prenant deux arguments entiers et renvoyant un double. Il existe deux fonctions portant le même nom binomial, une fonction Clojure traditionnelle et -binomial et wrapper accessibles depuis Java. Notez le tiret dans le nom de la fonction -binomial. Le préfixe par défaut est un trait d'union, mais vous pouvez le changer si vous le souhaitez. Le -main _ function ne fait que quelques appels à la fonction binomiale pour s'assurer que nous obtenons les bons résultats. Pour ce faire, compilez la classe et exécutez le programme.

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

Vous devriez voir la sortie affichée dans la réponse originale.

Emballez-le dans un bocal et placez-le dans un endroit pratique. Copiez le pot Clojure là aussi.

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.

La Java

Leiningen a une tâche intégrée, lein-javac, cela devrait pouvoir vous aider avec la compilation Java. Malheureusement, il semble être cassé dans la version 2.1.3. Il ne trouve pas le JDK installé ni le référentiel Maven. Les chemins d'accès aux deux chemins ont des espaces incorporés sur mon système. Je suppose que c'est le problème. N'importe lequel Java IDE pourrait également gérer la compilation et le packaging Mais pour ce poste, nous allons à l’école et le faisons en ligne de commande.

Commencez par créer le fichier Main.Java avec le contenu indiqué dans la réponse d'origine.

Pour compiler Java partie

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

Créez maintenant un fichier avec des méta-informations à ajouter au fichier jar que vous souhaitez créer. Dans Manifest.txt, ajoutez le texte suivant

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

Emballez maintenant le tout dans un grand fichier jar, comprenant notre programme Clojure et le bocal 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

Pour exécuter le programme:

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

La sortie est essentiellement identique à celle produite par Clojure uniquement, mais le résultat a été converti en un Java double.

Comme mentionné, a Java IDE s'occupera probablement des arguments de compilation malpropres et de l'emballage).

156
clartaq

Depuis Clojure 1.6.0, il existe une nouvelle méthode préférée pour charger et appeler des fonctions Clojure. Cette méthode est maintenant préférée pour appeler RT directement (et remplace beaucoup d'autres réponses ici). Le javadoc est ici - le point d'entrée principal est clojure.Java.api.Clojure.

Pour rechercher et appeler une fonction Clojure:

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

Fonctions dans clojure.core sont automatiquement chargés. D'autres espaces de noms peuvent être chargés via require:

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

IFns peuvent être passés à des fonctions d'ordre supérieur, par exemple. l'exemple ci-dessous passe plus à read:

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

La plupart des IFns de Clojure font référence à des fonctions. Quelques-uns, cependant, font référence à des valeurs de données non fonctionnelles. Pour y accéder, utilisez deref au lieu de fn:

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

Parfois (si vous utilisez une autre partie du runtime Clojure), vous devrez peut-être vous assurer que le runtime Clojure est correctement initialisé - l'appel d'une méthode sur la classe Clojure est suffisant à cet effet. Si vous n'avez pas besoin d'appeler une méthode sur Clojure, il suffit alors de charger la classe, cela suffit (dans le passé, une recommandation similaire recommandait de charger la classe RT; cette méthode est désormais recommandée ):

Class.forName("clojure.Java.api.Clojure") 
116
Alex Miller

[~ # ~] modifier [~ # ~] Cette réponse a été écrite en 2010 et fonctionnait à cette époque. Voir la réponse d'Alex Miller pour une solution plus moderne.

Quel type de code appelle de Java? Si vous avez une classe générée avec gen-class, appelez-la simplement. Si vous souhaitez appeler une fonction à partir d'un script, recherchez exemple suivant .

Si vous souhaitez évaluer le code d'une chaîne, en Java, vous pouvez utiliser le code suivant:

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);
  }
}
34
Alex Ott

EDIT: J'ai écrit cette réponse il y a presque trois ans. Dans Clojure 1.6, il existe une API appropriée pour appeler Clojure à partir de Java. S'il vous plaît réponse d'Alex Miller pour des informations à jour.

Réponse originale de 2011:

À mon avis, le moyen le plus simple (si vous ne générez pas de classe avec une compilation AOT) consiste à utiliser clojure.lang.RT pour accéder aux fonctions dans clojure. Avec cela, vous pouvez imiter ce que vous auriez fait dans Clojure (inutile de compiler les choses de manière particulière):

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

Et en 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);

C'est un peu plus détaillé en Java, mais j'espère qu'il est clair que les morceaux de code sont équivalents.

Cela devrait fonctionner tant que Clojure et les fichiers source (ou fichiers compilés) de votre code Clojure sont sur le chemin de classe.

12
raek

Je suis d'accord avec la réponse de clartaq, mais je pensais que les débutants pourraient aussi utiliser:

  • informations étape par étape sur la façon de réellement faire fonctionner cette machine
  • informations actuelles pour Clojure 1.3 et les versions récentes de leiningen.
  • un pot Clojure qui inclut également une fonction principale, il peut donc être exécuté de manière autonome ou lié en tant que bibliothèque.

J'ai donc couvert tout cela dans cet article de blog .

Le code Clojure ressemble à ceci:

(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." ))

La configuration du projet leiningen 1.7.1 se présente comme suit:

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

Le code Java ressemble à ceci:

import ThingOne.*;

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

Ou vous pouvez aussi obtenir tout le code de ce projet sur github .

10
sandover

Cela fonctionne avec 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)"));
    }
}
3
KIM Taegyoon

Si le cas d'utilisation consiste à inclure un fichier JAR construit avec Clojure dans une application Java, j'ai constaté qu'il était bénéfique de disposer d'un espace de nom distinct pour l'interface entre les deux mondes:

(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)))

Le core namespace peut utiliser l'instance injectée pour accomplir ses tâches:

(ns example-app.core)

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

À des fins de test, l'interface peut être stubée:

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

Une autre technique qui fonctionne également avec d'autres langages au-dessus de la machine virtuelle Java consiste à déclarer une interface pour les fonctions que vous souhaitez appeler, puis à utiliser la fonction 'proxy' pour créer une instance qui les implémente.

0
Petr Gladkikh