Skip to content

Commit

Permalink
Merge pull request #9 from terwer/develop
Browse files Browse the repository at this point in the history
New nashorn Config
  • Loading branch information
terwer committed Jan 20, 2019
2 parents 6af5ccd + 8d1599d commit 1f6d0ea
Show file tree
Hide file tree
Showing 17 changed files with 892 additions and 324 deletions.
98 changes: 98 additions & 0 deletions src/main/java/com/terwergreen/next/utils/NashornUtil.java
@@ -0,0 +1,98 @@
package com.terwergreen.next.utils;

import jdk.nashorn.api.scripting.NashornScriptEngine;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.script.ScriptContext;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

/**
* Nashorn工具类
*
* @author Terwer
* @version 1.0
* 2019/1/20 22:35
**/
public class NashornUtil {
private static final Logger logger = LoggerFactory.getLogger(NashornUtil.class);
private static NashornUtil nashornUtil;
private final NashornScriptEngine engine;
private static ScriptContext sc = new SimpleScriptContext();
private static ScheduledExecutorService globalScheduledThreadPool = Executors.newScheduledThreadPool(20);

/**
* Vue资源文件目录
*/
private static final String LIB_DIR = "static/lib";
private static final String POLYFILL_FILE_NAME = "nashorn-polyfill.js";

public static synchronized NashornUtil getInstance() {
if (nashornUtil == null) {
long start = System.currentTimeMillis();
nashornUtil = new NashornUtil();
long end = System.currentTimeMillis();
logger.info("init nashornHelper cost time {}ms", (end - start));
}

return nashornUtil;
}

private NashornUtil() {
// 获取Javascript引擎
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
engine = (NashornScriptEngine) factory.getScriptEngine(new String[]{"--language=es6"});
sc.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);
sc.setAttribute("__IS_SSR__", true, ScriptContext.ENGINE_SCOPE);
sc.setAttribute("__NASHORN_POLYFILL_TIMER__", globalScheduledThreadPool, ScriptContext.ENGINE_SCOPE);
engine.setBindings(sc.getBindings(ScriptContext.ENGINE_SCOPE), ScriptContext.ENGINE_SCOPE);

try {
// 编译nashorn-polyfill
engine.eval(read(LIB_DIR + File.separator + POLYFILL_FILE_NAME));
// for (String fileName : NashornUtil.VENDOR_FILE_NAME) {
// engine.eval(read(SRC_DIR + File.separator + fileName));
// }
// engine.eval(read(SRC_DIR + File.separator + "app.js"));
// 编译server
engine.eval(VueUtil.readVueFile("server.js"));
logger.info("Vue app.js编译成功,编译引擎为Nashorn");
} catch (ScriptException e) {
logger.error("Nashorn引擎Javascript解析错误", e);
}
}

public NashornScriptEngine getNashornScriptEngine() {
return engine;
}

public ScriptObjectMirror getGlobalGlobalMirrorObject(String objectName) {
return (ScriptObjectMirror) engine.getBindings(ScriptContext.ENGINE_SCOPE).get(objectName);
}

public Object callRender(String methodName, Object... input) {
try {
return engine.invokeFunction(methodName, input);
} catch (ScriptException e) {
logger.error("run javascript failed.", e);
return null;
} catch (NoSuchMethodException e) {
logger.error("no such method.", e);
return null;
}
}

private Reader read(String path) {
InputStream in = getClass().getClassLoader().getResourceAsStream(path);
return new InputStreamReader(in);
}
}
72 changes: 41 additions & 31 deletions src/main/java/com/terwergreen/next/vue/VueRenderer.java
@@ -1,50 +1,60 @@
package com.terwergreen.next.vue;

import com.terwergreen.next.utils.VueUtil;
import jdk.nashorn.api.scripting.NashornScriptEngine;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
import com.terwergreen.next.utils.NashornUtil;
import jdk.nashorn.api.scripting.ScriptObjectMirror;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.script.Bindings;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import java.util.function.Consumer;

/**
* 渲染Vue
*/
public class VueRenderer {
private final Log logger = LogFactory.getLog(this.getClass());
private Object renderServerFunction;
private NashornUtil engine;

public VueRenderer() {
try {
// 获取Javascript引擎
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
NashornScriptEngine engine = (NashornScriptEngine) factory.getScriptEngine(new String[]{"--language=es6"});
// 编译
CompiledScript compiled = engine.compile(VueUtil.readVueFile("server-bundle.js"));
this.renderServerFunction = compiled.eval();
logger.info("Vue app.js编译成功,编译引擎为Nashorn");
} catch (ScriptException e) {
logger.error("Nashorn引擎Javascript解析错误", e);
throw new RuntimeException(e);
private final Object promiseLock = new Object();
private volatile boolean promiseResolved = false;
private String html = null;

private Consumer<Object> fnResolve = object -> {
synchronized (promiseLock) {
html = (String) object;
promiseResolved = true;
}
};

public VueRenderer() {
// 获取Javascript引擎
engine = NashornUtil.getInstance();
}

public String renderContent() {
NashornScriptEngine engine = (NashornScriptEngine) new ScriptEngineManager().getEngineByName("nashorn");
try {
ScriptContext newContext = new SimpleScriptContext();
newContext.setBindings(engine.createBindings(), ScriptContext.ENGINE_SCOPE);
Bindings engineScope = newContext.getBindings(ScriptContext.ENGINE_SCOPE);
engineScope.put("renderServer", this.renderServerFunction);
engine.setContext(newContext);
Object html = engine.invokeFunction("renderServer");
return String.valueOf(html);
ScriptObjectMirror promise = (ScriptObjectMirror) engine.callRender("renderServer");
promise.callMember("then", fnResolve);
ScriptObjectMirror nashornEventLoop = engine.getGlobalGlobalMirrorObject("nashornEventLoop");
// 执行nashornEventLoops.process()使主线程执行回调函数
nashornEventLoop.callMember("process");
int i = 0;
int jsWaitTimeout = 1000 * 60;
int interval = 200; // 等待时间间隔
int totalWaitTime = 0; // 实际等待时间
while (!promiseResolved && totalWaitTime < jsWaitTimeout) {
nashornEventLoop.callMember("process");
try {
Thread.sleep(interval);
} catch (InterruptedException e) {
logger.error("Thread error:", e);
}
totalWaitTime = totalWaitTime + interval;
if (interval < 500) interval = interval * 2;
i = i + 1;
}
return html;
} catch (Exception e) {
throw new IllegalStateException("failed to render vue component", e);
}
}

}
199 changes: 199 additions & 0 deletions src/main/resources/static/lib/nashorn-polyfill.js
@@ -0,0 +1,199 @@
var self = this;
// 模拟global
var global = this;

// 模拟process
var process = {
env: {
VUE_ENV: "server",
NODE_ENV: "production"
},
nextTick: function (fn) {
global.setTimeout(fn, 0)
}
};
global.process = process;

// 模拟console
var console = {};
console.debug = print;
console.warn = print;
console.log = print;
console.error = print;
console.trace = print;
console.assert = print;
global.console = console;

Object.assign = function (t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};

/*
Source is originated from https://github.com/morungos/java-xmlhttprequest
Articles about Nashorn:
- https://blog.codecentric.de/en/2014/06/project-nashorn-javascript-jvm-polyglott/
How it work:
in https://github.com/morungos/java-xmlhttprequest, it uses Timer to run setTimeout and setInterval task,
but they are run in a separate thread of the Timer creates that is different with the main JavaScript thread.
This implementation uses ScheduledExecutorService instead of Timer so the threads for task scheduling can be
reused instead of each JavasScript thread create a Timer thread when using Timer.
And most important thing is this adds global.nashornEventLoop and scheduled tasks only add function callback
object in eventLoop (ArrayQueue), and it is main JavaScript thread to run these function callback by calling
`global.nashornEventLoop.process();` at the end of JavaScript Application. It is just like browser or NodeJS
that event loop is called when the main stack is cleared.
When runs on server with Promise, remember to call `nashornEventLoop.process()` when waiting for Promise by
Thread.sleep(), and call `nashornEventLoop.reset()` if server thread (e.g. Servlet thread) decides to be
timeout so that eventLoop will be clean for next request.
*/
(function nashornEventLoopMain(context) {
'use strict';

var Thread = Java.type('java.lang.Thread');
var Phaser = Java.type('java.util.concurrent.Phaser');
var ArrayDeque = Java.type('java.util.ArrayDeque');
var HashMap = Java.type('java.util.HashMap');
var TimeUnit = Java.type("java.util.concurrent.TimeUnit");
var Runnable = Java.type('java.lang.Runnable');


var globalTimerId;
var timerMap;
var eventLoop;
var phaser = new Phaser();

// __NASHORN_POLYFILL_TIMER__ type is ScheduledExecutorService
var scheduler = context.__NASHORN_POLYFILL_TIMER__;

resetEventLoop();

console.log('main javasript thread ' + Thread.currentThread().getName());

function resetEventLoop() {
globalTimerId = 1;
if (timerMap) {
timerMap.forEach(function (key, value) {
value.cancel(true);
})
}
timerMap = new HashMap();
eventLoop = new ArrayDeque();
}

function waitForMessages() {
phaser.register();
var wait = !(eventLoop.size() === 0);
phaser.arriveAndDeregister();

return wait;
}

function processNextMessages() {
var remaining = 1;
while (remaining) {
// console.log('eventLoop size ' + eventLoop.size() + 'in thread ' + Thread.currentThread().getName());
phaser.register();
var message = eventLoop.removeFirst();
remaining = eventLoop.size();
phaser.arriveAndDeregister();

var fn = message.fn;
var args = message.args;

try {
// console.log('processNextMessages in thread ' + Thread.currentThread().getName());
fn.apply(context, args);
} catch (e) {
console.trace(e);
console.trace(fn);
console.trace(args);
}
}
}

context.nashornEventLoop = {
process: function () {
console.log('nashornEventLoop.process is called in thread ' + Thread.currentThread().getName())
while (waitForMessages()) {
processNextMessages()
}
},
reset: resetEventLoop
};

function createRunnable(fn, timerId, args, repeated) {
var Runner = Java.extend(Runnable, {
run: function () {
try {
var phase = phaser.register();
eventLoop.addLast({
fn: fn,
args: args
});
console.log('TimerTask add one event, and eventLoop size is:' + eventLoop.size() + ' in thread ' + Thread.currentThread().getName());
} catch (e) {
console.trace(e);
} finally {
if (!repeated) timerMap.remove(timerId);
phaser.arriveAndDeregister();
}
}
});
return new Runner();
}

var setTimeout = function (fn, millis /* [, args...] */) {
var args = [].slice.call(arguments, 2, arguments.length);

var timerId = globalTimerId++;
var runnable = createRunnable(fn, timerId, args, false);

var task = scheduler.schedule(runnable, millis, TimeUnit.MILLISECONDS);
timerMap.put(timerId, task);

return timerId;
};

var setImmediate = function (fn /* [, args...] */) {
var args = [].slice.call(arguments, 1, arguments.length);
return setTimeout(fn, 0, args);
}

var clearImmediate = function (timerId) {
clearTimeout(timerId);
}

var clearTimeout = function (timerId) {
var task = timerMap.get(timerId);
if (task) {
task.cancel(true);
timerMap.remove(timerId);
}
};

var setInterval = function (fn, delay /* [, args...] */) {
var args = [].slice.call(arguments, 2, arguments.length);

var timerId = globalTimerId++;
var runnable = createRunnable(fn, timerId, args, true);
var task = scheduler.scheduleWithFixedDelay(runnable, delay, delay, TimeUnit.MILLISECONDS);
timerMap.put(timerId, task);

return timerId;
};

var clearInterval = function (timerId) {
clearTimeout(timerId);
};

context.setTimeout = setTimeout;
context.clearTimeout = clearTimeout;
context.setImmediate = setImmediate;
context.clearImmediate = clearImmediate;
context.setInterval = setInterval;
context.clearInterval = clearInterval;
})(typeof global !== "undefined" && global || typeof self !== "undefined" && self || this);

0 comments on commit 1f6d0ea

Please sign in to comment.