Wraz z pojawieniem się Javy 8 został udostępniony nowy silnik JavaScriptowy dla JVM o nazwie Nashorn. Zastąpił on starszą implementację Rhino, dostępnego od Javy 6. JavaScriptowy silnik Rhino został stworzony dawno temu przez firmę Netscape (chyba w 1997 roku), a w późniejszym czasie rozwijany był przez Mozillę. Nashorn to silnik pozwalający uruchamiać kod Javascriptowy po stronie serwera. Daje to duże możliwości, bo w łatwy sposób można udostępnić „język / składnie” jakiejś konfiguracji dla użytkownika, dzięki czemu w trakcie działania aplikacji można modyfikować implementację pewnych zachowań i dodawać nowe. Dzięki wykorzystaniu takiego silnika można np. dać użytkownikowi możliwość oprogramowania jakiegoś fragmentu systemu.
Wykorzystując taki silnik można zaimplementować np. walidację formularzy przechowując ją tylko w jednym miejscu systemu / modelu. Z formularza przekazujemy JSONa, po stronie serwera wykonujemy ten sam kod który był wykonany po stronie przeglądarki.
Bez zbędnego wstępu, przejdę do konkretnych przykładów pokazujących możliwości wywoływania kodu JavaScript z poziomu Javy.
Przykład Hello World:
1 2 |
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js"); engine.eval("print('Cześć, tu Notatnik Programisty!');"); |
W wyniku uruchomienia czego, na konsoli pojawi się tekst: „Cześć, tu Notatnik Programisty!”
Interfejs ScriptEngine posiada przeciążoną metodę eval przyjmującą obiekt klasy Reader, dzięki czemu można wywołać kod bezpośrednio z zewnętrznego pliku:
1 2 3 4 |
//Zawartość pliku script.js: //print('Cześć, tu Notatnik Programisty (przykład z pliku script.js)!'); engine.eval(new FileReader("e:\\krzysztofjelonek.net\\JavaScriptEngine\\script.js")); |
W Javie 7 metoda getEngineByName(„js”) zwróci implementację Rhino. Od wersji ósmej będzie to już silnik Nashorn.
Wywoływanie funkcji:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//Plik js: var formatName = function(name) { return "Hello " + name; }; var helloWorldFunction = function (name) { print("Wynik wykonania helloWorldFunction: " + formatName(name)); }; helloWorldFunction('Notatnik Programisty'); //Kod w Javie: engine.eval(new FileReader("e:\\krzysztofjelonek.net\\JavaScriptEngine\\script.js")); //wynik w konsoli: //Wynik wykonania helloWorldFunction: Hello Notatnik Programisty |
Przekazywanie parametrów:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//Plik js: var helloWorldFunction = function (name) { print("Hello: " + name); }; //W Javie: //1. Odczytujemy plik FileReader scriptReader = new FileReader("e:\\krzysztofjelonek.net\\JavaScriptEngine\\script.js"); engine.eval(scriptReader); //2. Wywołujemy funkcję helloWorldFunction z parametrem "Notatnik Programisty" Invocable invocable = (Invocable) engine; Object scriptResult = invocable.invokeFunction("helloWorldFunction", "Notatnik Programisty"); //3. Wynik zwrócony przez funkcję: System.out.println(scriptResult); |
Wywoływanie kodu Javy z poziomu JavaScript:
Przykład klasy z publiczną metodą statyczną:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package net.krzysztofjelonek.scriptengine; /** * * @author Krzysztof Jelonek */ public class JSEngine { public static String helloWorldFromJava(String name) { return "Hello " + name; } } |
W pliku js wywołujemy ją następująco:
1 2 3 |
var JavaClass = Java.type('net.krzysztofjelonek.scriptengine.JSEngine'); var result = JavaClass.helloWorldFromJava('Notatnik Programisty'); print(result); |
Po wywołaniu engine.eval(scriptReader) z poziomu Javy, na konsoli wyświetli się: „Hello Notatnik Programisty”.
1 2 3 4 5 6 7 8 9 |
var System = Java.type('java.lang.System'); System.out.println('test 1'); System.err.println('test 2'); print(System.currentTimeMillis()); //Po wywołaniu w Javie engine.eval(scriptReader); test 1 test 2 1481361438923 |
Konwersja typów
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//Plik script.js: var JavaClass = Java.type('net.krzysztofjelonek.scriptengine.JSEngine'); JavaClass.printObjectClass('Notatnik Programisty'); JavaClass.printObjectClass(32768); JavaClass.printObjectClass(32768.99); JavaClass.printObjectClass(false); JavaClass.printObjectClass(new Date()); JavaClass.printObjectClass(new Number(32768)); JavaClass.printObjectClass({name: 'Notatnik Programisty'}); //Plik JSEngine.java public static void printObjectClass(Object object) { System.out.println(object.toString() + ": " + object.getClass()); } //Wynik: Notatnik Programisty: class java.lang.String 32768: class java.lang.Integer 32768.99: class java.lang.Double false: class java.lang.Boolean [Date 2016-12-10T09:11:07.233Z]: class jdk.nashorn.api.scripting.ScriptObjectMirror [Number 32768.0]: class jdk.nashorn.api.scripting.ScriptObjectMirror [object Object]: class jdk.nashorn.api.scripting.ScriptObjectMirror |
Implementacja interfejsów z poziomu JavaScript:
1 2 3 4 5 6 7 8 |
var Runnable = Java.type("java.lang.Runnable"); var MyRunnableTask = Java.extend(Runnable, { run: function() { print("Metoda run w oddzielnym wątku"); } }); var Thread = Java.type("java.lang.Thread"); new Thread(new MyRunnableTask()).start(); |
W powyższym kodzie implementowany jest interfejs Runnable, przekazywany do obiektu Thread. Tekst „Metoda run w oddzielnym wątku” zostanie wyświetlony na oucie w nowym wątku.
Przekazywanie obiektów (binding)
Definicja klasy z dostępnymi metodami.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package net.krzysztofjelonek.scriptengine; /** * * @author Krzysztof Jelonek */ public class Api { public String objectHelloWorld(String name) { return "Object Hello " + name; } public static String staticHelloWorld(String name) { return "Static Hello " + name; } } |
Jedna z nich jest statyczna.
1 2 3 4 5 6 7 8 9 10 |
//Podpinami pod nazwą Api obiekt klasy Api: ScriptEngine engine = new ScriptEngineManager().getEngineByName("js"); engine.getBindings(ScriptContext.ENGINE_SCOPE).put("Api", new Api()); //Dzięki temu nie trzeba wykonywać importu w kodzie JavaScriptu String result = (String) engine.eval("Api.objectHelloWorld('Notatnik Programisty');"); System.out.println(result); OUT: Object Hello Notatnik Programisty |
Jednak wywołanie tego kodu spowoduje wyrzucenie wyjątku:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
String result = (String) engine.eval("Api.<strong>staticHelloWorld</strong>('Notatnik Programisty');"); //Wyjątek: Exception in thread "main" javax.script.ScriptException: TypeError: Api.staticHelloWorld is not a function in <eval> at line number 1 at jdk.nashorn.api.scripting.NashornScriptEngine.throwAsScriptException(NashornScriptEngine.java:467) at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:451) at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:403) at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:399) at jdk.nashorn.api.scripting.NashornScriptEngine.eval(NashornScriptEngine.java:155) at javax.script.AbstractScriptEngine.eval(AbstractScriptEngine.java:264) at net.krzysztofjelonek.scriptengine.Api.main(Api.java:16) Caused by: <eval>:1 TypeError: Api.staticHelloWorld is not a function at jdk.nashorn.internal.runtime.ECMAErrors.error(ECMAErrors.java:57) at jdk.nashorn.internal.runtime.ECMAErrors.typeError(ECMAErrors.java:213) at jdk.nashorn.internal.runtime.ECMAErrors.typeError(ECMAErrors.java:185) at jdk.nashorn.internal.runtime.ECMAErrors.typeError(ECMAErrors.java:172) at jdk.nashorn.internal.runtime.Undefined.lookup(Undefined.java:102) at jdk.nashorn.internal.runtime.linker.NashornLinker.getGuardedInvocation(NashornLinker.java:106) at jdk.nashorn.internal.runtime.linker.NashornLinker.getGuardedInvocation(NashornLinker.java:98) at jdk.internal.dynalink.support.CompositeTypeBasedGuardingDynamicLinker.getGuardedInvocation(CompositeTypeBasedGuardingDynamicLinker.java:176) at jdk.internal.dynalink.support.CompositeGuardingDynamicLinker.getGuardedInvocation(CompositeGuardingDynamicLinker.java:124) at jdk.internal.dynalink.support.LinkerServicesImpl.getGuardedInvocation(LinkerServicesImpl.java:154) at jdk.internal.dynalink.DynamicLinker.relink(DynamicLinker.java:253) at jdk.nashorn.internal.scripts.Script$\^eval\_.:program(<eval>:1) at jdk.nashorn.internal.runtime.ScriptFunctionData.invoke(ScriptFunctionData.java:623) at jdk.nashorn.internal.runtime.ScriptFunction.invoke(ScriptFunction.java:494) at jdk.nashorn.internal.runtime.ScriptRuntime.apply(ScriptRuntime.java:393) at jdk.nashorn.api.scripting.NashornScriptEngine.evalImpl(NashornScriptEngine.java:446) ... 5 more |
Wywoływanie metod statycznych można wykonać tak:
1 2 3 4 5 |
String result = (String) engine.eval("Api.class.static.staticHelloWorld('Notatnik Programisty');"); System.out.println(result); OUT: Static Hello Notatnik Programisty |
Przykład najbanalniejszego zastosowania?
Np. chcemy aby w aplikacji użytkownik mógł definiować pewne warunki logiczne, dzięki którym coś się będzie wykonywało, czy wyświetlało. Włącznie z wykorzystaniem operatorów && || i nawiasów grupujących.
Przykład takiego wyrażenia:
„(EDYCJA || USUNIECIE) && !BRAK.DOSTEPU”, które po zastąpieniu zmiennymi logicznymi po stronie serwera zostanie zaktualizowane na „(true||false) && true” i wywołane przez silnik JSowy (jako prosty parser wyrażeń logicznych) zwróci:
1 2 3 4 5 |
boolean rights = (boolean) engine.eval("(true||false) && true"); System.out.println(rights); //OUT: true |
Jako ciekawostkę dodam, że gdy chodzi o parsowanie samych wyrażeń logicznych, to np. dedykowana do tego, choć już leciwa biblioteka http://jboolexpr.sourceforge.net/ zadziała w tym przypadku ok. 50 razy szybciej. Jednak nikt nie powiedział, że w tym przypadku zamiast wyrazu EDYCJA nie mogłoby być inne wyrażenie np. $api.editRights(4), co zmienia już postać rzeczy.
Problemem jest wydajność, jednak przy odpowiedniej ilości iteracji kodu Javowy Just In Time (JIT) compiler zacznie działać i wykona sporą optymalizację. Mimo wszystko, Node.js i tak będzie dwa razy szybszy.