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)
- many calls to
- Compile + Multi Eval
- single call to
JShellScriptEngine.compile(String)
- many calls to
JShellCompiledScript.eval(Bindings)
- single call to