/jshell-scriptengine

JShell script engine for Java (JSR-223 compatible)

Primary LanguageJavaMIT LicenseMIT

Build Status Code Coverage Quality Gate Status Open Issues Last Commits Maven Central - jshell-scriptengine

JShell scripting engine

The JShell was introduced with Java 9 and was designed to be used for interactive execution of code snippets in Java.

The jshell-scriptengine library is a Java 11 wrapper around the JShell API that executes an entire script and handles the binding of variables.

jshell-scriptengine is a good alternative to the usual javascript commonly used, especially if the users that will end up writing the scripts are already experienced Java programmers or can use the leverage that a Java framework can provide.

Using a scripting engine is a powerful choice if your project needs to execute code that can be easily changed outside of the development cycle and when already deployed.

Typical use cases are:

  • directory in your deployed application containing scripts for customizable business logic
  • editor in your application to edit (and persist) scripts

If you believe that Java as a scripting language does not exactly fit your needs, consider the sibling project spel-scriptengine (Spring Expression Language Scripting Engine).

Using JShell scripting engine in your projects

To use the JShell scripting you can either download the newest version of the .jar file from the published releases on Github or use the following dependency to Maven Central in your build script (please verify the version number to be the newest release):

Use JShell scripting engine in Maven build

<dependency>
  <groupId>ch.obermuhlner</groupId>
  <artifactId>jshell-scriptengine</artifactId>
  <version>1.1.0</version>
</dependency>

Use JShell scripting engine in Gradle build

repositories {
  mavenCentral()
}

dependencies {
  compile 'ch.obermuhlner:jshell-scriptengine:1.1.0'
}

Simple usage

The following code snippet shows a simple usage of the JShell script engine:

try {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("jshell");

    String script = "" +
            "System.out.println(\"Input A: \" + inputA);" +
            "System.out.println(\"Input B: \" + inputB);" +
            "var output = inputA + inputB;" +
            "1000 + output;";

    engine.put("inputA", 2);
    engine.put("inputB", 3);

    Object result = engine.eval(script);
    System.out.println("Result: " + result);

    Object output = engine.get("output");
    System.out.println("Output Variable: " + output);

} catch (ScriptException e) {
    e.printStackTrace();
}

The console output of this snippet shows that the bindings for input and output variables are working correctly. The return value of the JShell script is the value of the last statement 1000 + output.

Input A: 2
Input B: 3
Result: 1005
Output Variable: 5

Access to classes

The JShell script is executed in the same Thread as the caller and has therefore access to the same classes.

Assume your project has the following class:

package ch.obermuhlner.scriptengine.example;

public class Person {
    public String name;
    public int birthYear;

    @Override
    public String toString() {
        return "Person{name=" + name + ", birthYear=" + birthYear + "}";
    }
}

In this case you can run a JShell script that uses this class Person:

try {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("jshell");

    String script = "" +
            "import ch.obermuhlner.scriptengine.example.Person;" +
            "var person = new Person();" +
            "person.name = \"Eric\";" +
            "person.birthYear = 1967;";

    Object result = engine.eval(script);
    System.out.println("Result: " + result);

    Object person = engine.get("person");
    System.out.println("Person Variable: " + person);

} catch (ScriptException e) {
    e.printStackTrace();
}

The console output of this snippet shows that the variable person created inside the JShell script is now available in the calling Java:

Result: 1967
Person Variable: Person{name=Eric, birthYear=1967}

Error handling

try {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("jshell");

    String script = "" +
            "System.out.println(unknown);" +
            "var message = \"Should never reach this point\"";

    Object result = engine.eval(script);
    System.out.println("Result: " + result);
} catch (ScriptException e) {
    e.printStackTrace();
}

The console output of this snippet shows that the variable unknown cannot be found:

javax.script.ScriptException: cannot find symbol
  symbol:   variable unknown
  location: class 
System.out.println(unknown);
	at ch.obermuhlner.scriptengine.jshell.JShellScriptEngine.evaluateScript(JShellScriptEngine.java:216)
	at ch.obermuhlner.scriptengine.jshell.JShellScriptEngine.eval(JShellScriptEngine.java:98)
	at ch.obermuhlner.scriptengine.jshell.JShellScriptEngine.eval(JShellScriptEngine.java:84)
	at ch.obermuhlner.scriptengine.jshell.JShellScriptEngine.eval(JShellScriptEngine.java:74)
	at ch.obermuhlner.scriptengine.example.ScriptEngineExample.runErrorExample(ScriptEngineExample.java:84)
	at ch.obermuhlner.scriptengine.example.ScriptEngineExample.main(ScriptEngineExample.java:14)

Compiling

The JShellScriptEngine implements the Compilable interface.

You can compile a script into a CompiledScript and execute it multiple times with different bindings.

try {
    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByName("jshell");
    Compilable compiler = (Compilable) engine;

    CompiledScript compiledScript = compiler.compile("var output = alpha + beta");

    {
        Bindings bindings = engine.createBindings();
        bindings.put("alpha", 2);
        bindings.put("beta", 3);

        Object result = compiledScript.eval(bindings);
        Integer output = (Integer) bindings.get("output");
        System.out.println("Result (Integer): " + result);
        System.out.println("Output (Integer): " + output);
    }

    {
        Bindings bindings = engine.createBindings();
        bindings.put("alpha", "aaa");
        bindings.put("beta", "bbb");

        Object result = compiledScript.eval(bindings);
        String output = (String) bindings.get("output");
        System.out.println("Result (String): " + result);
        System.out.println("Output (String): " + output);
    }
} catch (ScriptException e) {
    e.printStackTrace();
}

The console output shows that the same compiled script was able to run with different bindings, which where even of different runtime types.

Result (Integer): 5
Output (Integer): 5
Result (String): aaabbb
Output (String): aaabbb

Separating the compilation from the evaluation is more efficient if you need to evaluate the same script multiple times.

Here the execution times in milliseconds for:

  • Multi Eval
    • many calls to JShellScriptEngine.eval(String) (essentially compile and evaluate every time)
  • Compile + Multi Eval
    • single call to JShellScriptEngine.compile(String)
    • many calls to JShellCompiledScript.eval(Bindings)

Performance: Compile Multiple Evaluations