Java Scripting With Nashorn (JSR 223) & Pre-compilation
Asked Answered
B

3

10

I am using Nashorn via JSR 223 to execute small snippets of user entered script:

public Invocable buildInvocable(String script) throws ScriptException {
    ScriptEngine engine = new ScriptEngineManager().getEngineByName(ENGINE);
    engine.eval(functions);
    engine.eval(script);
    return (Invocable) engine;
}

The varying user script calls JavaScript functions that are defined in a static, central library (held in the functions String in the code snippet above).

Every time I want to get hold of an Invocable that I can call from my Java I am constantly having to recompile the large library code.

Is there any way to join a previously compiled piece of code in with new code?

Brumbaugh answered 31/3, 2014 at 1:17 Comment(1)
Are there some security issues? e.g. inject malware codesAzoth
K
12

This is by design of JSR-223; eval(String) can't really have a code cache behind it. Well, theoretically it could, but it'd embody a lot of speculation on the part of what the developer wants (and as all speculations, it'd be bound to be wrong some of the time).

What you should do is evaluate your Invocable once, keep it around and use it repeatedly.

When doing so, note that Nashorn does not provide thread safety (JavaScript has no concept of threading, so Nashorn is intentionally not thread safe in order to not have to pay synchronization costs when they aren't mandated by the language semantics). For that reason, your created Invocable will not be safe to use from multiple threads with regard to the state of the global variables in the underlying script. (Concurrently running functions that don't interact with the script's global state is fine.)

If you need to share it across threads and the functions depend on global state, and the global state can change, then you'll need to add your own scaffolding for that (either synchronization, or resource pooling, or whatever else is currently in fashion for this purpose).

Kho answered 31/3, 2014 at 9:53 Comment(0)
A
18

Put compiled functions into Bindings like:

private static final String FUNCTIONS =
    "function() {" +
    "  return \"Hello\";" +
    "}";

public static void main(String... args) throws Exception {
    ScriptEngine engine = new ScriptEngineManager().getEngineByMimeType("text/javascript");

    // Compile common functions once
    CompiledScript compiled = ((Compilable) engine).compile(FUNCTIONS);
    Object sayHello = compiled.eval();

    // Load users' script each time
    SimpleBindings global = new SimpleBindings();
    global.put("sayHello", sayHello);
    String script = "sayHello()";
    System.out.println(engine.eval(script, global));
}
Azoth answered 27/11, 2014 at 11:55 Comment(2)
Is there a way you could pass an argument to the script?Subdual
try String script = "sayHello(\"My Argument\")";Azoth
K
12

This is by design of JSR-223; eval(String) can't really have a code cache behind it. Well, theoretically it could, but it'd embody a lot of speculation on the part of what the developer wants (and as all speculations, it'd be bound to be wrong some of the time).

What you should do is evaluate your Invocable once, keep it around and use it repeatedly.

When doing so, note that Nashorn does not provide thread safety (JavaScript has no concept of threading, so Nashorn is intentionally not thread safe in order to not have to pay synchronization costs when they aren't mandated by the language semantics). For that reason, your created Invocable will not be safe to use from multiple threads with regard to the state of the global variables in the underlying script. (Concurrently running functions that don't interact with the script's global state is fine.)

If you need to share it across threads and the functions depend on global state, and the global state can change, then you'll need to add your own scaffolding for that (either synchronization, or resource pooling, or whatever else is currently in fashion for this purpose).

Kho answered 31/3, 2014 at 9:53 Comment(0)
K
2

If you need you precompile and call JavaSctipt functions with various arguments you may compile them separately and assemble execution flow in Java. With JavaScript engine Nashorn available in Java8 you can do:

private static final String FUNCTIONS =
  "function hello( arg ) {" +        //<-- passing java.lang.String from Java
    "  return 'Hello ' + arg;" +     //<-- returning string back
    "};" +
    "function sayTime( arg ) {" +   //<-- passing java.util.HashMap from Java
    "  return 'Java time ' + arg.get( 'time' );" +  //<-- returning string back
    "};" +
    "function () {" +                 //<-- this callable "function pointer" is being returned on [step1] below
    "  return { 'hello': hello, 'sayTime': sayTime };" +
    "};";

public static void main(String... args) throws Exception {
  ScriptEngine engine = new ScriptEngineManager().getEngineByName( "Nashorn" );

  CompiledScript compiled = ((Compilable) engine).compile(FUNCTIONS);
  ScriptObjectMirror lastFunction = (ScriptObjectMirror)compiled.eval();   // [step1]

  ScriptObjectMirror functionTable = (ScriptObjectMirror)lastFunction.call( null ); // this method retrieves function table
  String[] functionNames = functionTable.getOwnKeys( true );
  System.out.println( "Function names: " + Arrays.toString( functionNames ) );

  System.out.println( functionTable.callMember( "hello", "Robert" ) ); //<-- calling hello() with String as argiment

  Map map = new HashMap();
  map.put( "time", new Date().toString() ); //<-- preparing hashmap

  System.out.println( functionTable.callMember( "sayTime", map ) );  //<-- calling sayTime() with HashMap as argument
}

You may pass Java objects inside of JavaSctipt, see example with java.util.HashMap above.

Output is:

Function names: [hello, sayTime]
Hello Robert
Java time Fri Jan 12 12:23:15 EST 2018
Korry answered 12/1, 2018 at 17:35 Comment(2)
Are the calls to the members thread-safe? Or asked differently: Can the ScriptObjectMirror functionTable shared among threads?Joke
if you do not keep state on JavaScript side, and call only thread safe methods from Javascript -- then yes. in other words: if you reference some global variables from your JavaScript -- you are not thread safe; referencing constants is ok. in yet other words: if you passing values into computation not via argument list -- then some of these values could be shared -- then you are not thread safe. you can jump from Java to JavaSctipt -- back and forth as many hoops as you like, so if you're calling Java code from your JavaScript you need those are also thread safe.Good LuckKorry

© 2022 - 2024 — McMap. All rights reserved.