JRuby, Script Engine i Java

post_img

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:

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

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *