Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

JRuby: Java e Ruby insieme

Introduzione pratica alla piattaforma JRuby, per utilizzare classi Java con Ruby e viceversa
Introduzione pratica alla piattaforma JRuby, per utilizzare classi Java con Ruby e viceversa
Link copiato negli appunti

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

Figura 1. Selezionare la piattaforma JRuby
Selezionare la piattaforma JRuby su NetBeans

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.

Figura 2. LiveGraph
LiveGraph

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.


Ti consigliamo anche