JRuby, „w 100% czysta implementacja Javy na bazie języka Ruby”, jak określają go twórcy, dostarcza zalety obydwu tych języków. W artykule tym skupię się jednak tylko na jednym ciekawym aspekcie JRubiego, czyli wykorzystania go bezpośrednio w aplikacjach stricte javowych poprzez Script Engine.
Często programiści zostawiają w tworzonych aplikacjach różnego rodzaju furtki, by w razie awarii, jakiegoś nietypowego zachowania, mogli dojść do przyczyn. Czasem też dane w programie ewoluują, i okazuje się, że brakuje nam informacji by wykonać daną operację, np. stworzyć zaległy raport czy fakturę.
Java dostarcza środków do sprawnej realizacji tego typu zadań – silnik skryptowy. Dzięki niemu, możemy bez ingerencji w kod aplikacji czy magicznych sztuczek na poziomie interfejsu dostać się do interesujących nas danych i zmodyfikować je. I to wszystko bez potrzeby restartowania serwera czy przebudowywania kodu zależnie od problemu. Wystarczy umieścić w aplikacji Script Engine i interfejs do jego obsługi. Istnieje cały projekt zajmujący się dostarczaniem bibliotek do obsługi języków skryptowych w Javie – zainteresowanym polecam stronę https://scripting.dev.java.net/
Java dostarcza dwa rodzaje API do obsługi skryptów. Są to:
- JSR 223 Scripting API
- Bean Scripting Framework (BSF) API
Java 1.6 standardowo dostarcza engine do obsługi javaskryptu, jeżeli chcemy wykorzystywać JSR 223 do JRubiego, musimy dodać odpowiednie biblioteki do naszej aplikacji:
- jruby-bin-1.0.tar.gz – do pobrania z jruby.codehouse.org
- jsr223-engines.tar.gz – do pobrania z scripting.dev.java.net
Przyjrzyjmy się jak działa Script Engine na przykładzie*
import javax.script.*;
public class EvalScript {
public static void main(String[] args) throws Exception {
ScriptEngineManager factory = new ScriptEngineManager()
// Create a JRuby engine. ScriptEngine engine = factory.getEngineByName("jruby");
// Evaluate JRuby code from string. try { engine.eval("puts('Hello')"); } catch (ScriptException exception) { exception.printStackTrace(); }
} }
Powyższa klasa tworzy engine JRuby i próbuje wykonać skrypt napisany w tym języku. Ten prosty przykład wypisuje tylko powitanie, jednak poprzez jego parametryzację możemy w łatwy sposób uzyskać obsługę skomplikowanych skryptów.
Poza ewaluacją skryptu Script Engine pozwala nam na wywoływanie konkretnych metod zdefiniowanych w skrypcie, w zależności od potrzeb.
//klasa Javy **
public class RunMethods {
public static void main(String[] args) throws ScriptException, FileNotFoundException {
ScriptEngineManager factory = new ScriptEngineManager();
// Create a JRuby engine.
ScriptEngine engine = factory.getEngineByName("jruby");
//process a ruby file
engine.eval(new BufferedReader(new FileReader("myruby.rb")));
//call a method defined in the ruby source
engine.put("number", 6); //[1]
long fact = (Long) engine.eval("fact($number)"); //[2]
System.out.println("fact: " + fact);
engine.eval("$myglobalvar = fact($number)"); //[3] long myglob = (Long) engine.getBindings(ScriptContext.ENGINE_SCOPE).get("myglobalvar");//[4]
System.out.println("myglob: " + myglob);
} }
// myruby.rb **
def fact(n)
if n==0
return 1
else
return n*fact(n-1)
end
end
Klasa wczytuje plik myruby.rb, który definiuje metodę fact obliczającą silnię dla podanego parametru. Po ewaluacji, engine pozwala nam ustawić lokalne zmienne [1] i wykorzystać je do wywołania metody zdefiniowanej w pliku [2]. Dodatkowo zmienne możemy ustawiać wywołując kolejne ewaluacje [3]. Engine.getBindings pozwala nam odczytać ustawione w skrypcie zmienne [4].
Wszystko fajnie, ale co z modyfikowaniem danych i obiektów Javy? Tu wkraczamy bardziej w samą składnię JRubiego niż istotę Script Engine jednak pozwolę sobie na prosty przykład
//example1.rb***
require 'java' #[1]
import java.lang.System #[2]
frame = javax.swing.JFrame.new("Window") #[3]
label = javax.swing.JLabel.new("The Time" + System::currentTimeMillis) #[4]
frame.getContentPane.add(label) #[5]
frame.setDefaultCloseOperation(javax.swing.JFrame::EXIT_ON_CLOSE)
frame.pack
frame.setVisible(true)
Wczytując i wykonując powyższy skrypt powinniśmy dostać okienko pokazujące nam informację o aktualnym czasie. Linia [1] jest najistotniejsza w powyższym pliku. To ona pozwala nam na dostęp do standardowych klas Javy. Od tego momentu możemy używać klas Javy poprzez podanie pełnych ścieżek i nazw klas. Aby nie podawać ciągle pakietu klasy możemy użyć słowa import [2]. Linia [3] tworzy nam nowego JFrame’a, [4] JLabel z informacją o czasie. Operator :: używany jest do dostępu do statycznych metod i pól klasy. Do niestatecznych metod dostęp uzyskujemy przez standardową kropkę [5]. Aby uniknąć wielu importów klas, można importować całe pakiety dyrektywą include_package a klasy z poza projektu dostępne są po dodaniu pliku jar do skryptu (dyrektywa require). Tym sposobem, możemy w skrypcie dodać brakujące dane, stworzyć potrzebny obiekt i usunąć zbędne dane (np generowanie zaległej faktury po zmianie oferowanych produktów).
Załóżmy, że mamy działającą aplikację na serwerze, z zaimplementowanym Script Engine w postaci formularza z polem tekstowym, który służy do wpisania skryptu. Submit formularza wywołuje metodę ewaluującą wpisany tekst.
Naszą nietypową sytuacją jest konieczność wykonania operacji na fakturze, jej wydrukowanie i usunięcie
// skrypt wpisany do pola tekstowego
require 'java'
import pl.atena.MyInvoice
import pl.atena.InvoiceRepository
root = InvoiceRepository.instance
invoice = MyInvoice.new
# manipulate invoice data
root.getInvoices.add(invoice)
root.printInvoice(invoice)
root.removeInvoice(invoice)
//funkcja obsługująca submit formularza
public void doSubmit(String script) {
try {
ScriptEngine engine = factory.getEngineByName("jruby");
engine.eval(script);
} catch (ScriptException exception) {
exception.printStackTrace();
}
}
// klasa obsługi faktur
class InvoiceRepository {
InvoiceRepository getInstance() {…}
List<Invoice> getInvoices() {….}
void printInvoice(Invoice invoice) {….}
boolean removeInvoice(Invoice invoice {…}
}
Sytuacji wyjątkowych może być całe mnóstwo, a wszystkie je możemy obsłużyć w analogiczny sposób – bez konieczności modyfikacji nawet kawałka kodu naszej aplikacji. Jeżeli dodamy do tego, że aplikacja nie dostarcza bezpośrednio GUI do obsługi funkcji removeInvoice, ujawnia się nam prawdziwa potęga Script Engine.
JRuby pozwala na dużo więcej, wszystkich zainteresowanych odsyłam na stronę projektu http://jruby.codehouse.org
Materiały źródłowe:
* http://java.sun.com/developer/technicalArticles/scripting/jruby/
** http://www.ics.muni.cz/~makub/ruby/
*** http://wiki.jruby.org/wiki/Calling_Java_from_JRuby
Autor: Adam Andrzejewski
