Выполнение функции в определенном контексте в Nashorn

У меня есть собственная среда выполнения Nashorn, которую я настроил с некоторыми глобальными функциями и объектами — некоторые из них не имеют состояния, а некоторые — с сохранением состояния. В этой среде выполнения я запускаю несколько пользовательских сценариев.

Для каждого выполнения я планирую создать новый контекст, поддерживаемый глобальным контекстом:

myContext.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);
engine.eval(myScript, myContext);

Судя по тому, что я прочитал, любые изменения глобальной области видимости (с точки зрения сценария) будут ограничены созданным мной новым контекстом.

Эти сценарии при оценке предоставляют некоторые объекты (с четко определенными именами и именами методов). Я могу вызвать метод объекта, приведя engine к Invocable. Но как мне узнать контекст, в котором будет выполняться функция? Это вообще проблема, или контекст выполнения этой функции настроен на основе контекста, в котором она была оценена?

Какое поведение я могу ожидать в многопоточной ситуации, когда все потоки совместно используют один и тот же экземпляр механизма сценариев, и все они пытаются запустить один и тот же сценарий (который предоставляет доступ к глобальному объекту). Когда я затем вызову метод для объекта, в каком контексте будет выполняться функция? Как он узнает, какой экземпляр объекта использовать?

Я ожидал увидеть метод invoke, в котором я могу указать контекст, но, похоже, это не так. Есть ли способ сделать это, или я делаю это совершенно неправильно?

Я знаю, что простой способ обойти это — создать новый экземпляр скриптового движка для каждого выполнения, но, как я понимаю, я потеряю оптимизацию (особенно в общем коде). При этом поможет ли здесь предварительная компиляция?


person Vivin Paliath    schedule 09.11.2015    source источник
comment
Я читал, что предварительная компиляция на самом деле не работает в Nashorn, поэтому она не помогает. Однако не могу найти источник. Я могу ошибаться.   -  person Stijn de Witt    schedule 07.12.2015


Ответы (1)


Я понял это. Проблема, с которой я столкнулся, заключалась в том, что invokeFunction вызывал NoSuchMethodException, потому что функции, предоставляемые пользовательским скриптом, не существовали в привязках из области действия движка по умолчанию:

ScriptContext context = new SimpleScriptContext();
context.setBindings(nashorn.createBindings(), ScriptContext.ENGINE_SCOPE);
engine.eval(customScriptSource, context);
((Invocable) engine).invokeFunction(name, args); //<- NoSuchMethodException thrown

Итак, что мне нужно было сделать, это вытащить функцию из контекста по имени и вызвать ее явно так:

JSObject function = (JSObject) context.getAttribute(name, ScriptContext.ENGINE_SCOPE);
function.call(null, args); //call to JSObject#isFunction omitted brevity 

Это вызовет функцию, которая существует в вашем только что созданном контексте. Вы также можете вызывать методы для объектов следующим образом:

JSObject object = (JSObject) context.getAttribute(name, ScriptContext.ENGINE_SCOPE);
JSObject method = (JSObject) object.getMember(name);
method.call(object, args);

call генерирует непроверенное исключение (либо Throwable, завернутое в RuntimeException, либо NashornException, которое было инициализировано информацией о стеке JavaScript), поэтому вам, возможно, придется явно обработать это, если вы хотите предоставить полезную обратную связь.

Таким образом, потоки не могут перешагивать друг через друга, потому что для каждого потока существует отдельный контекст. Я также смог разделить пользовательский код среды выполнения между потоками и убедиться, что изменения состояния изменяемых объектов, предоставляемых пользовательской средой выполнения, были изолированы контекстом.

Для этого я создаю экземпляр CompiledScript, который содержит скомпилированное представление моей пользовательской библиотеки времени выполнения:

public class Runtime {

    private ScriptEngine engine;
    private CompiledScript compiledRuntime;

    public Runtime() {
        engine = new NashornScriptEngineFactory().getScriptEngine("-strict");
        String source = new Scanner(
            this.getClass().getClassLoader().getResourceAsStream("runtime/runtime.js")
        ).useDelimiter("\\Z").next();

        try {
            compiledRuntime = ((Compilable) engine).compile(source);
        } catch(ScriptException e) {
            ...
        }
    }

    ...
}

Затем, когда мне нужно выполнить скрипт, я оцениваю скомпилированный исходный код, а затем также оцениваю скрипт в этом контексте:

ScriptContext context = new SimpleScriptContext();
context.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);

//Exception handling omitted for brevity

//Evaluate the compiled runtime in our new context
compiledRuntime.eval(context); 

//Evaluate the source in the same context
engine.eval(source, context);

//Call a function
JSObject jsObject = (JSObject) context.getAttribute(function, ScriptContext.ENGINE_SCOPE);
jsObject.call(null, args);

Я протестировал это с несколькими потоками и смог убедиться, что изменения состояния ограничены контекстами, принадлежащими отдельным потокам. Это связано с тем, что скомпилированное представление выполняется в определенном контексте, а это означает, что экземпляры всего, что оно предоставляет, ограничено этим контекстом.

Один небольшой недостаток здесь заключается в том, что вы можете без необходимости переоценивать определения объектов для объектов, которым не нужно иметь состояние, зависящее от потока. Чтобы обойти это, оцените их непосредственно в движке, что добавит привязки для этих объектов к ENGINE_SCOPE движка:

public Runtime() {
    ...
    String shared = new Scanner(
        this.getClass().getClassLoader().getResourceAsStream("runtime/shared.js")
    ).useDelimiter("\\Z").next();

    try {
        ...        
        nashorn.eval(shared);
        ...
    } catch(ScriptException e) {
        ...
    }
}

Позже вы можете заполнить специфичный для потока контекст из движка ENGINE_SCOPE:

context.getBindings(ScriptContext.ENGINE_SCOPE).putAll(engine.getBindings(ScriptContext.ENGINE_SCOPE));

Одна вещь, которую вам нужно сделать, это убедиться, что все такие объекты, которые вы выставляете, были заморожены. В противном случае их можно переопределить или добавить свойства.

person Vivin Paliath    schedule 10.11.2015
comment
Спасибо, что нашли время ответить на свой вопрос. Очень полезно! - person Stijn de Witt; 07.12.2015
comment
Вопрос однако. В чем разница между оценкой скомпилированного сценария и исходного кода? Мне кажется он двойной? - person Stijn de Witt; 07.12.2015
comment
Я реализовал на основе этого, но пропустил engine.eval(source, context), и, похоже, он работает нормально. - person Stijn de Witt; 07.12.2015
comment
@StijndeWitt Разница в том, что предварительно скомпилированный исходный код предназначен для любой библиотеки, которая поддерживает состояние для конкретного выполнения пользовательского скрипта. На самом деле это не обязательно, и я думаю, что правильным подходом может быть просто написать JS-библиотеку, чтобы такое общее состояние поддерживалось только для каждого экземпляра (т. Е. Сама библиотека возвращает экземпляр). Но если вы просто запускаете предварительно скомпилированные пользовательские сценарии, то второй eval не нужен. - person Vivin Paliath; 07.12.2015
comment
Бинго, мне понадобился день, чтобы в конце концов закончить поиск твоим ответом. На данный момент это лучшее решение с точки зрения повторного использования общего скомпилированного сценария с различными исполнениями пользовательского сценария. Я считаю, что compiledRuntime.eval(context) просто выполнит любой предварительно скомпилированный общий код (как указано в javadoc). И именно здесь проявляется преимущество скомпилированного сценария. Если кто-нибудь может подтвердить это, мы будем очень признательны. - person t7tran; 07.09.2016