Wykorzystując silnik skryptów dostępny w wirtualnej maszynie Javy masz możliwość połączenia swojego kodu z logiką umieszczoną w zewnętrznym źródle. Łatwo wyobrazić sobie korzyści płynące z takiego rozwiązania. Możesz np. spersonalizować zaimplementowane w kodzie algorytmy.
Wyobraźmy sobie program do obsługi magazynu, którego jednym z zadań jest obliczanie dostępnego stanu towaru. Każdy z klientów może chcieć obliczać tę wartość trochę inaczej. Firma A będzie chciała widzieć tylko stan towaru dostępnego “fizycznie” w danej chwili, firma B chce natomiast uwzględnić nie tylko posiadany towar, lecz również ilość którą dostawca zobowiązał się dostarczyć w danym dniu.
Innym przykładem może być np. program obliczający premię pracownika; każdy z klientów zapewne będzie chciał ją zdefiniować inaczej.
ScriptEngineManger
Za pomocą ScriptEngineManager
możesz utworzyć obiekt silnika skryptów, możesz również sprawdzić dostępne fabryki oraz uzyskać o nich szczegółowe informacje:
ScriptEngineManager manager = new ScriptEngineManager();
manager.getEngineFactories()
.forEach(scriptEngineFactory -> {
System.out.println("Engine name: " + scriptEngineFactory.getEngineName());
System.out.println("Language name: " + scriptEngineFactory.getLanguageName());
System.out.println("Names: " + scriptEngineFactory.getNames());
System.out.println("MIME types: " + scriptEngineFactory.getMimeTypes());
System.out.println("Extensions" + scriptEngineFactory.getExtensions());
});
Zarówno dla Javy 8 jak i 11 będzie to Oracle Nashorn. Poniżej zestawienie informacji o tym silniku.
Engine name: Oracle Nashorn
Language name: ECMAScript
Names: [nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]
MIME types: [application/javascript, application/ecmascript, text/javascript, text/ecmascript]
Extensions: [js]
Jeżeli uruchamiasz kod w Javie 11 to podczas kompilacji otrzymasz ostrzeżenie o porzuceniu silnika w przyszłych wydaniach JDK:
Warning: Nashorn engine is planned to be removed from a future JDK release
Możesz zechcieć użyć parametrów -Dnashorn.args=–no-deprecation-warning aby wyciszyć to ostrzeżenie. Więcej w tym temacie pod linkiem: https://openjdk.java.net/jeps/335
Począwszy od Javy 15 aby skorzystać z Nashorn’a dodaj zależność Mavena:
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>15.2</version>
</dependency>
Wykonywanie skryptów
Spróbujmy wykonać prosty skrypt, którego zadaniem będzie zsumowanie dwóch zmiennych.
@Test
void testAddingExpression() throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
String scriptSource = "" +
"var foo = 2; " +
"var bar = 1; " +
"foo + bar";
Object result = engine.eval(scriptSource);
assertEquals(3.0, result);
assertEquals(Double.class, result.getClass());
}
Wynik wykonania skryptu ustalany jest na podstawie ostatniego wyrażania. W powyższym przypadku będzie to liczba 3. Zwracane liczby są typu Double.
Wiązania zmiennych, klas i funkcji
Pojawia się pytanie, jak przekazać do skryptu własną zmienną – spójrz na poniższy przykład:
@Test
void testPassingVariable() throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
Bindings bindings = engine.createBindings();
bindings.put("bar", 1);
String scriptSource = "" +
"var foo = 2; " +
"foo + bar";
Object result = engine.eval(scriptSource, bindings);
assertEquals(3.0, result);
}
Jak widzisz, tym razem wywołując skrypt przekazaliśmy obiekt wiążący Bindings
, w którym przypisano do zmiennej bar
wartość 1.
Wykorzystując wiązania można udostępnić również funkcje i klasy Java. Na poniższym przykładzie zdefiniujemy i przekażemy do silnika własną funkcję log
.
@Test
void testConsumerPassing() throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
Bindings bindings = engine.createBindings();
bindings.put("log", (Consumer) message ->
System.out.println("LOG={" + message + "}"));
String scriptSource = "" +
"log('Hello scripting world');" +
"var utc = new Date().toJSON();" +
"log(utc);";
engine.eval(scriptSource, bindings);
}
Wynikiem wykonania skryptu będzie wyświetlenie w konsoli przekazanych do funkcji parametrów:
LOG={Hello scripting world}
LOG={2021-05-29T07:35:49.545Z}
Jak widzisz za pomocą obiektu Bindings
przekazaliśmy do skryptu implementację interfejsu funkcyjnego Consumer
.
W ten sam sposób możesz zdefiniować funkcję wykorzystującą dwa argumenty. Do tego celu zaimplementujemy inny interfejs funkcyjny: BiFunction
.
@Test
void testBiFunctionPassing() throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
Bindings bindings = engine.createBindings();
bindings.put("add", (BiFunction<Integer, Integer, Integer>) (v1, v2) -> v1 + v2);
String scriptSource = "" +
"var result = add(1, 2);" +
"result";
Object result = engine.eval(scriptSource, bindings);
assertEquals(3, result);
assertEquals(Integer.class, result.getClass());
}
A co jeśli, chciałbyś przekazać do funkcji tablicę? Spójrz na poniższą funkcję max
@Test
void testArrayArgFunctionPassing() throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
Bindings bindings = engine.createBindings();
bindings.put("max", (Function<Object, Integer>) inp -> {
if (inp instanceof ScriptObjectMirror) {
int[] ints = ((ScriptObjectMirror) inp).to(int[].class);
return Arrays
.stream(ints)
.max()
.orElse(0);
} else {
return 0;
}
});
String scriptSource = "" +
"var result = max([5, 3, 1, 9, 7]);" +
"result";
Object result = engine.eval(scriptSource, bindings);
assertEquals(9, result);
}
Jak wcześniej wspomniałem, Bindings
umożliwia również udostępnianie klas. Mogą to być zarówno klasy “twoje” jak i systemowe. Spójrz na poniższą klasę Foo
:
package pl.javascratches.eval;
import java.util.Random;
public class Foo {
public Integer abs(int value) {
return Math.abs(value);
}
public static Integer random() {
Random random = new Random();
return random.nextInt();
}
Tym razem skrypt umieściłem w osobnym pliku. Do jego wykonania użyjemy metody eval
z wykorzystaniem FileReader
.
@Test
void testClassPassing() throws ScriptException, IOException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
Bindings bindings = engine.createBindings();
bindings.put("outputFile", "result.txt");
bindings.put("Files", engine.eval("Java.type('java.nio.file.Files')"));
bindings.put("Paths", engine.eval("Java.type('java.nio.file.Paths')"));
bindings.put("Foo", engine.eval("Java.type('pl.javascratches.eval.Foo')"));
engine.eval(new FileReader("script.js"), bindings);
String result = Files.readString(Paths.get("result.txt"));
System.out.println(result);
}
W powyższym przykładzie do skryptu przekazana została zmienna outputFile
oraz klasy Foo
, Files
i Paths
.
Poniżej zawartość wykonanego skryptu script.js
var r = Foo.random();
if (r > 0) {
r = r * -1;
}
r = new Foo().abs(r);
Files.writeString(Paths.get(outputFile), r.toString());
Wynikiem wykonania skryptu będzie wylosowana, zawsze dodatnia liczba. Liczbę tę zapisano w pliku result.txt
.
Wywoływanie funkcji
Silnik Nashhorn pozwala na wykonanie funkcji zdefiniowanej w skrypcie. Spójrz na poniższy przykład:
@Test
void testCallFunction() throws ScriptException, NoSuchMethodException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
String script = "function sum(x, y) { return x + y}";
engine.eval(script);
Invocable invocable = (Invocable) engine;
Object result = invocable.invokeFunction("sum", 1, 2);
assertEquals(3.0, result);
}
Jak widzisz, wywołujemy funkcję sum
zdefiniowaną w skrypcie.
Możemy pójść o krok dalej i zdefiniować za pomocą silnika skryptów implementację interfejsu. Załóżmy prosty interfejs Greeter:
public interface Greeter {
String greet(String name);
}
Poniżej jego implementacja i wywołanie:
@Test
void testInterfaceImplementation() throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
String script = "function greet(x) { return 'Hello, ' + x + '!';}";
engine.eval(script);
Invocable invocable = (Invocable) engine;
Greeter greeter = invocable.getInterface(Greeter.class);
String result = greeter.greet("Tom");
assertEquals("Hello, Tom!", result);
}
Kompilacja skryptu
Kompilacja do postaci pośredniej umożliwiaj efektywniejsze wykonywanie, szczególnie w przypadku gdy skrypt ma być wykonywany wielokrotnie.
Spójrz na nieco zmodyfikowany kod z poprzedniego listingu:
@Test
void testCompiledScriptCallFunction()
throws ScriptException, IOException, NoSuchMethodException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");
String script = "function sum(x, y) { return x + y}";
CompiledScript compiledScript = ((Compilable) engine).compile(script);
Invocable invocable = (Invocable) compiledScript.getEngine();
Bindings bindings = compiledScript.getEngine().getBindings(ScriptContext.ENGINE_SCOPE);
compiledScript.eval(bindings);
Object result = invocable.invokeFunction("sum", 1, 2);
assertEquals(3.0, result);
}