#376 JRuby Basics
- Download:
- source codeProject Files in Zip (41 KB)
- mp4Full Size H.264 Video (28.6 MB)
- m4vSmaller H.264 Video (12.8 MB)
- webmFull Size VP8 Video (14 MB)
- ogvFull Size Theora Video (23.3 MB)
JRuby es una implementación estable de Ruby sobre la Máquina Virtual de Java. JRuby es de mucho interés incluso aunque no seamos programadores de Java y en este episodio vamos a ver cómo empezar con JRuby y centrarnos en algunas áreas en las que difiere de la implementación estándar MRI.
Instalación de JRuby
Una forma de instalar JRuby es descargándolo del sitio web pero también lo podemos hacer con gestor de entornos de Ruby como RVM o rbenv. Vamos a usar rbenv para instalar la última versión, que en el momento de escribir este artículo es la 1.7.0.preview2.
$ rbenv install jruby-1.7.0-preview2
Nótese que si estamos usando OS X Mountain Lion es posible que nos solicite instalar una máquina virtual de Java. Y dependiendo también de nuestra configuración puede ser que tengamos que poner una j
delante de las órdenes normales de Ruby para indicar que queremos usar la versión de JRuby, lo que quiere decir que tendríamos que ejecutar jruby
y jirb
en lugar de ruby
o irb
.
$ rbenv shell jruby-1.7.0-preview2 $ ruby -v jruby 1.7.0.preview2 (1.9.3p203) 2012-08-07 4a6bb0a on Java HotSpot(TM) 64-Bit Server VM 1.6.0_35-b10-428-11M3811 [darwin-x86_64]
Comenzaremos nuestro repaso de JRuby experimentando en la consola. Podemos ejecutar las órdenes de Ruby igual que si estuviésemos ejecutando Ruby MRI 1.9.3, y probablemente no encontremos ninguna diferencia, pero un cambio grande es que ahora tenemos acceso al mundo de Java y todas sus librerías. La clave para ello es con la línea require "java"
, que otorga acceso a varias clases de Java como el HashMap. Crearemos un nuevo HashMap, añadiéndole un valor para luego recuperarlo.
>> require "java" => true >> h = java.util.HashMap.new => {} >> h.put(:foo, "bar") => nil >> h.get(:foo) => "bar"
JRuby incluye algunos métodos de conveniencia para las clases importantes de Java. Por ejemplo con nuestro HashMap
podemos acceder a los valores de forma similar a como lo haríamos en Ruby. Podemos incluso iterar sobre las claves con .each
y mucho más, aunque estemos usando una clase de Java.
>> h[:foo] => "bar"
Otro uso más interesante de JRuby el poder crear interfaces de usuario con Swing.
>> javax.swing.JOptionPane.showMessageDialog(nil, "Hello World!") => nil
Este código muestra una caja de diálogo, y podríamos crear interfaces completas de usuario de esta manera, lo que no sería muy útil para las aplicaciones Rails pero sí que nos puede venir bien para crear scripts administrativos. JRuby proporciona una forma alternativa para hacer esto más al estilo de Ruby, de esta manera:
>> Java::JavaxSwing::JOptionPane.show_message_dialog(nil, "Hello") => nil
Esto muestra una caja de diálogo similar a la que vimos anteriormente. A continuación veremos un ejemplo más completo de lo que se puede hacer con Swing.
require "java" java_import javax.swing.JFrame java_import javax.swing.JButton java_import javax.swing.JOptionPane class HelloWorld < JFrame def initialize super "Example" setSize(150, 100) setDefaultCloseOperation(JFrame::EXIT_ON_CLOSE) setLocationRelativeTo(nil) button = JButton.new("Say Hello") add(button) button.addActionListener do |e| JOptionPane.showMessageDialog(nil, "Hello World") end setVisible(true) end end HelloWorld.new
Al principio de todo hacemos require "java"
igual que hicimos en irb para tener acceso a Swing y luego usamos java_import
para importar algunas clases directamente a este script. Luego creamos una clase Ruby que hereda de la clase JFrame
de Java, que creará una ventana. En nuestra clase establecemos el tamaño y otras propiedades, tras lo cual creamos un botón y se lo añadimos a la ventana junto con una función para atender un evento que se activa cuando se hace clic en el botón que hemos creado. En Java usaríamos una función anónima para definirla, pero en JRuby usaremos un bloque. Tras esto hacemos que la ventana sea visible y, por último, creamos una nueva instancia de nuestra clase. Ejecutaremos este archivo con la orden ruby
y veremos que aparece una ventana pequeña con el botón que acabamos de crear. Al hacer clic aparece el siguiente mensaje de diálogo.
La concurrencia mejorada de JRuby
Tender puentes entre Ruby y Java es útil pero como desarrolladores Rails puede ser que no nos lo parezca. Lo que es más interesante en JRuby es que el soporte de threads mejora la ejecución concurrente de nuestro código. Podemos demostrar esto creando un nuevo script de Ruby que calcula números de Fibonacci.
def fib(n) n < 2 ? n : fib(n-1) + fib(n-2) end start = Time.now 1.upto(5).map do |n| Thread.new { puts "Thread #{n}: #{fib(32)}" } end.each(&:join) puts "Time: #{Time.now - start}"
Se trata de invocar cinco veces un método sencillo que calcula números de Fibonacci, pero en threads separados. Cuando el programa termina imprimimos el tiempo que ha tardado en ejecutarse. En JRuby tarda menos de un segundo en completar su ejecución, pero en la implementación MRI de Ruby tarda más de tres. El mótivo principal de esta lentitud es el Bloqueo Global de Intérprete. Matt Aimonetti escribió un artículo en 2011 donde explicaba este tema en detalle pero la conclusión es que este bloqueo impide que la secuencia de Fibonacci se ejecute concurrentemente en hilos separados, por lo que si tenemos una máquina con varios núcleos la diferencia de tiempo será aún más notable. Si no procesamos permanentemente código Ruby sino que estamos esperando la respuesta de una base de datos o la lectura de un archivo este bloqueo global no surte efecto, lo que podemos simular con una llamada a sleep
en nuestro código de Fibonacci.
def fib(n) n < 2 ? n : fib(n-1) + fib(n-2) end start = Time.now 1.upto(5).map do |n| Thread.new { puts "Thread #{n}: #{sleep(0.5)}" } end.each(&:join) puts "Time: #{Time.now - start}"
Si ejecutamos este código bajo MRI Ruby sólo tarda medio segundo en completarse porque el bloqueo no tiene efecto mientras el thread está inactivo.
Si estamos desarrollando una aplicación Rails multihilo y nos preguntamos cuál podría ser la ganancia de rendimiento usando JRuby la respuesta es que depende de la relación entre el tiempo que nuestra aplicación emplea en acceder a la base de datos y el tiempo que está ejecutando código Ruby, así como del número de núcleos del que disponga la máquina. Por supuesto hay muchas más cosas que debemos considerar cuando comparamos el rendimiento entre un caso y otro pero en general si queremos ejecutar una aplicación Rails multihilo en producción con MRI Ruby tendremos que establecer procesos separados ejecutando la aplicación para aprovechar al máximo la concurrencia. Sin embargo con JRuby es posible que podamos controlarlo todo en un único proceso.
Una aplicación JRuby on Rails
Vamos a terminar este episodio mostrando cómo hacer que una aplicación Rails sea compatible con JRuby. Primero vamos a conmutar a JRuby en la consola y luego instalaremos la gema Rails. Tras instalar todas las gemas podremos crear una nueva aplicación Rails que llamaremos blog
.
$ rbenv shell jruby-1.7.0-preview2 $ gem install rails $ rails new blog
El generador de Rails es bastante sofisticado y hará que nuestra aplicación sea compatible con JRuby al detectar que lo estamos usando. Las diferencias clave se pueden encontrar en el Gemfile
, la primera es que no usamos la gema sqlite3
para acceder a SQLite sino algo distinto.
gem 'activerecord-jdbcsqlite3-adapter'
JDBC es la interfaz estándar que usan las aplicaciones Java para comunicarse con la base de datos. La gema lo utiliza para que ActiveRecord pueda hablar con la base de datos de nuestra aplicación. Es importante entender que algunas gemas no son compatibles con JRuby porque dependen de extensiones en C, y la gema sqlite3
es una de ellas. Esta gema no se puede instalar en JRuby, produciéndose un fallo al instalar las extensiones nativa. Se puede intentar soslayar este problema pero lo mejor es buscar una gema alternativa compatible con JRuby. En el wiki de JRuby hay una página dedicada a las alternativas a las extensiones en C, lo que hace que sea un buen sitio para empezar la búsqueda. Otro recurso útil es la gema JRuby-Lint, que incluye la orden jrlint
que podemos ejecutar sobre un proyecto para ver si tiene incompatibilidades con JRuby. No lo comprueba todo, pero sí que busca extensiones en C y otros problemas potenciales.
De vuelta en el Gemfile
de nuestra aplicación vemos que aparece la gema jruby-openssl
que emula la librería OpenSSL de Ruby. La otra gema específica de JRuby es therubyrhino
que se usa para ejecutar JavaScript. Esta son las gemas principales que tendremos que añadir si estamos convirtiendo una aplicación Rails ya existente para que use JRuby, tras lo cual deberíamos poder seguir trabajando con nuestra aplicación Rails, para demostrarlo vamos a generar el andamiaje necesario para un modelo sencillo llamado Article
.
$ rails g scaffold article name $ rake db:migrate $ rails s
Esto genera las páginas habituales de andamiaje con las que podremos crear, modificar y listar artículos igual que con cualquier otra aplicación que utilice la implementación MRI de Ruby.
La carga de la aplicación completa Rails en JRuby puede tardar un poco. En general JRuby tardará mucho más en cargar el código inicialmente pero es bastante rápido ejecutándolo una vez porque ha logrado cargarlo por completo. Si tenemos problema con esto podemos intentar hacer que la aplicación sea compatible tanto con MRI como con JRuby de forma que podamos usar MRI en desarrollo (que es cuando estaremos continuamente reiniciando la aplicación). Esto puede conseguirse con la opción platform
del Gemfile
de forma que sólo se carguen ciertas gemas dependiendo de la implementación de Ruby.
source 'https://rubygems.org' gem 'rails', '3.2.8' platform :jruby do gem 'activerecord-jdbcsqlite3-adapter' gem 'jruby-openssl' gem 'therubyrhino' end plaform :ruby do gem 'sqlite3' end # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.2.3' gem 'coffee-rails', '~> 3.2.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails'
Si pasamos a usar Ruby MRI 1.9.3 y ejecutamos bundle
para que se instalen las gemas veremos que la aplicación Rails también se ejecuta correctamente. Por supuesto es importante tener una batería de tests completa para que podamos garantizar que la aplicación funciona bajo JRuby y MRI.