Può capitare che nel bel mezzo di un porting di un'applicazione da Java a Ruby ci si renda conto della mancanza del binding di una libreria. La soluzione breve è rinunciare, quella geek è iniziare a scriversi il porting della libreria, quella pratica è continuare a utilizzare la libreria Java con JRuby.
In questo articolo vedremo dunque come utilizzare delle classi Java all'interno di applicazioni Ruby usando l'implementazione JRuby. Il modo più semplice per provarlo è quello di scaricare una versione recente di NetBeans. L'IDE della Sun utilizza di default, per il supporto al linguaggio Ruby, proprio JRuby.
Classi Java in Ruby
Iniziamo con un semplice esempio che mostra come utilizzare delle classi Java, come ad esempio la classe Random
, direttamente da un applicazione Ruby. Da Netbeans creiamo un nuovo progetto Ruby e scegliamo JRuby come "Ruby platform".
Fatto questo possiamo iniziare a scrivere il codice includendo il modulo Java
include Java
e istanziando un oggetto della classe java.util.Random
rnd = java.util.Random.new
Possiamo ora utilizzare i metodi di Random per generare dei numeri casuali, ad esempio:
puts rnd.nextInt puts rnd.nextLong puts rnd.nextFloat puts rnd.nextDouble puts rnd.nextGaussian
In alternativa è anche possibile utilizzare la direttiva import
import java.util.Random rnd = Random.new
oppure utilizzare include_class
che permette anche di indicare degli alias, ad esempio
include_class 'java.util.Random' do |pkg,name| "JRandom" end
in quest'ultimo caso un oggetto di tipo java.util.Random
va creato utilizzando il nome JRandom
rnd = JRandom.new
Gli alias sono utili soprattutto quando c'è conflitto tra i nomi delle classi Ruby e quelle Java. Un ulteriore meccanismo consiste nel creare un modulo ad-hoc e utilizzare include_package
module JavaUtil include_package 'java.util' end rnd = JavaUtil::Random.new
Nell'esempio il modulo JavaUtil
contiene tutto il package java.util
e non solo la classe Random
. Una piccola curiosità, i nomi Java sono convertiti automaticamente anche in stile Ruby, ad esempio è possibile riscrivere il primo esempio in questo modo:
include Java rnd = java.util.Random.new puts rnd.next_int puts rnd.next_long puts rnd.next_float puts rnd.next_double puts rnd.next_gaussian
Anche i path vengono convertiti in stile Ruby, ad esempio i due percorsi seguenti sono equivalenti:
java.util.Random # stile Java Java::JavaUtil::Random # stile Ruby
ovvero vengono rimossi i punti, viene usato lo stile CamelCase per i nomi e il tutto viene messo nel package Java. Un esempio più esplicativo è quello relativo alla classe ParserFactory:
org.xml.sax.helpers.ParserFactory # stile Java Java::OrgXmlSaxHelpers::ParserFactory # stile Ruby
Esistono anche dei metodi di utilità come java_class
e java_kind_of?
che rispettivamente restituiscono il nome della classe Java di un oggetto e verificano il tipo di un oggetto. Ad esempio il codice
include Java rnd = java.util.Random.new puts rnd.class puts rnd.java_class puts rnd.java_kind_of?(Java::JavaUtil::Random) puts rnd.java_kind_of?(java.util.Random)
fornirà in output il nome della classe Ruby di rnd
, il nome della classe Java e confermerà il tipo:
Java::JavaUtil::Random java.util.Random true true
Estendere una classe Java in JRuby
È possibile anche aggiungere dei nuovi metodi alle classi Java importate. Tornando all'esempio appena visto aggiungiamo un metodo nextPositiveInt
alla classe Random
.
include Java import java.util.Random class Random def nextPositiveInt self.nextInt.abs end end rnd = Random.new puts rnd.nextPositiveInt
In questo modo gli oggetti di tipo Random
avranno anche l'inutile metodo nextPositiveInt
che come si evince dal nome restituirà un numero intero casuale sempre positivo.
Le eccezioni
Le eccezioni generate da Java possono essere intercettate da JRuby attraverso l'eccezione NativeException
. Questo permette di gestire tutte le eccezioni direttamente da Ruby, un semplice esempio è il seguente:
include Java begin java.lang.Class.forName("ClasseInesistente") rescue NativeException => e puts "Native exception: #{e.cause}" end
L'output sarà del tipo:
Native exception: java.lang.ClassNotFoundException: ClasseInesistente
Il tipo di eccezione viene ottenuto attraverso il metodo cause
, per tutti i dettagli è utile il metodo backtrace
. Includendo le singole classi Java che implementano le eccezioni è possibile definire l'eccezione specifica che si vuole catturare. L'esempio visto prima diventa dunque
include Java include_class 'java.lang.ClassNotFoundException' begin java.lang.Class.forName("ClasseInesistente") rescue ClassNotFoundException => e puts "ClassNotFoundException: #{e.message}" end
Questo è sicuramente i modo più naturale di gestire le eccezioni Java.
Un esempio reale
Vediamo ora un esempio reale per meglio capire le potenzialità di JRuby. Supponiamo di dover visualizzare dei dati raccolti in tempo reale da un'applicazione Ruby e di voler utilizzare la libreria open source LiveGraph che però fornisce solo delle librerie Java.
L'unica soluzione percorribile a questo punto è quella di portare la nostra applicazione in JRuby e utilizzare quindi direttamente le librerie Java.
Dopo aver impostato JRuby come piattaforma e incluso nel JRuby Classpath le librerie necessarie scaricabili dal sito di LiveGraph possiamo includere le classi necessarie attraverso include_class
require 'java' include_class 'org.LiveGraph.dataFile.write.DataStreamWriter' include_class 'org.LiveGraph.dataFile.common.PipeClosedByReaderException' include_class 'org.LiveGraph.LiveGraph' include_class 'com.softnetConsult.utils.sys.SystemTools' include_class 'java.lang.System'
A questo punto non ci resta che scrivere il codice necessario a visualizzare i dati raccolti
class LiveGraphDemo def initialize lg = LiveGraph.application lg.execStandalone out = lg.updateInvoker.startMemoryStreamMode if (out.nil?) puts "Could not switch LiveGraph into memory stream mode." lg.disposeGUIAndExit end out.setSeparator(";") out.addDataSeries("Sensor 1") out.addDataSeries("Sensor 2") collectData writeData out.close lg.disposeGUIAndExit end end
Nell'esempio i metodi collectData
e writeData
si occuperanno di collezionare i dati direttamente dai sensori e di passarli a LiveGraph per la visualizzazione utilizzando out.setDataValue
e out.writeDataSet
.
Oltre a includerle nel JRuby classpath è possibile utilizzare le classi contenute in un file JAR direttamente dall'applicazione utilizzando require
, ad esempio supponendo che le nostre librerie siano contenute nella directory java_lib
basta scrivere
require 'java_lib/LiveGraph.2.0.beta01.Complete.jar' require 'java_lib/SoftNetConsultUtils.2.01.slim.jar'
e poi includere le singole classi come visto prima.
In questo caso abbiamo usato un path relativo ma è possibile utilizzare anche i path assoluti come ad esempio
require '/usr/lib/ooo3/basis3.0/program/classes/agenda.jar'
Includendo in questo modo i file JAR, JRuby li aggiungerà al classpath dinamicamente.
Ruby da Java
Concludiamo dando uno sguardo alla soluzione al problema inverso: eseguire codice Ruby da un'applicazione Java. Ci sono diversi meccanismi che permettono questo comportamento, vediamo quello più immediato: inserire direttamente JRuby nell'applicazione Java.
Per farlo basta usare JavaEmbedUtils che fornisce i metodi per creare un'istanza del runtime JRuby.
import java.util.ArrayList; import org.jruby.Ruby; import org.jruby.javasupport.JavaEmbedUtils; public class RubyInJava { public static void main(String[] ARGV) { Ruby runtime = JavaEmbedUtils.initialize(new ArrayList()); runtime.evalScriptlet("puts 'Hello World'"); } }
JavaEmbedUtils.initialize()
, che si occupa di inizializzare e creare un'istanza di JRuby, prende come argomento una lista di path da aggiungere al Ruby load path, nel nostro caso non aggiungiamo niente passandogli una lista vuota. Il codice Ruby viene eseguito attraverso il metodo evalScriptlet()
.
Altro esempio. Riprendiamo l'estensione fatta alla classe Random e portiamola in Java:
import java.util.ArrayList; import org.jruby.Ruby; import org.jruby.javasupport.JavaEmbedUtils; public class RubyInJava { public static void main(String[] ARGV) { Ruby runtime = JavaEmbedUtils.initialize(new ArrayList()); String script = "require 'java'n" + "import java.util.Randomn" + "class Randomn" + " def nextPositiveIntn"+ " self.nextInt.absn"+ " endn"+ "endn" + "rnd = Random.newn" + "puts rnd.nextPositiveIntn"; runtime.evalScriptlet(script); JavaEmbedUtils.terminate(runtime); } }
Non abbiamo fatto altro che modificare una classe Java utilizzando codice Ruby in un'applicazione Java. Anche se la descrizione è da mal di mare, è un meccanismo che può risultare utile in alcuni casi. Si noti che il metodo aggiunto sarà visibile solo nel contesto JRuby.
Chiudiamo con una nota. Il problema: ogni chiamata a JavaEmbedUtils.initialize() crea una nuova istanza del runtime. La soluzione: impostare la proprietà jruby.runtime.threadlocal
a true
System.setProperty("jruby.runtime.threadlocal", "true");
in questo modo è possibile riutilizzare lo stesso runtime all'interno di un unico thread. Per accedere all'istanza del runtime va utilizzato Ruby.newInstance()
che restituisce un oggetto di tipo org.jruby.Ruby
.
Conclusioni
Anche se la soluzione di mischiare due linguaggi è poco elegante, in molti casi pratici è l'unica soluzione percorribile. Dal punto di vista degli sviluppatori Ruby è un modo inoltre per aggirare un limite storico del linguaggio: la mancanza di librerie stabili e mature.