ClassCastException NativeRegExpExecResult 无法转换为 NativeArray

ClassCastException NativeRegExpExecResult cannot be cast to NativeArray

我正在 OpenShift 上的 WildFly 10 运行 上开发应用程序,利用 Nashorn 对 React 应用程序进行服务器端渲染。

该应用程序在我的本地计算机上运行正常(我不喜欢性能级别),但是当我将它部署到 OpenShift 时,发生了一些神秘的事情。

几次请求后,运行 在之前的请求中找到的代码突然开始抛出

java.lang.ClassCastException: jdk.nashorn.internal.objects.NativeRegExpExecResult cannot be cast to jdk.nashorn.internal.objects.NativeArray

堆栈跟踪显示这来自一行 code in React-Router...该 react-router 在失败的情况下正在处理。这是更改后的代码的样子:

if (match != null) {
  if (captureRemaining) {if (typeof global != 'undefined') {global.log.warn('match.length=' + match.length);}
    remainingPathname = match.pop();
    var matchedPath = match[0].substr(0, match[0].length - remainingPathname.length);
    if (typeof global != 'undefined') {global.log.warn('remainingPathname=' + remainingPathname + ', matchedPath=' + matchedPath);}
    // If we didn't match the entire pathname, then make sure that the match
    // we did get ends at a path separator (potentially the one we added
    // above at the beginning of the path, if the actual match was empty).

(注意对 global.log.warn 的调用...我添加了那些)

如果您查看 full logs,您会发现对于第一个请求,事情似乎工作正常,但突然,它开始抛出此 ClassCastException 并且不会停止了。对于任何请求,我的应用程序所做的只是 return 503 service not available

我弄乱了代码,多次重写它以获得正确的行为,但我有点卡住了。最后,我卡在 synchronized 块中以尝试消除线程问题,但问题仍然存在。奇怪的是,如果我在 WildFly 中将 max-worker-threads 设置为 1,问题似乎就消失了。

我说似乎是因为我发现很难确定问题所在,OpenShift 的部署时间长以及问题的 'random' 行为。

下面是我的ReactRenderFilter的相关代码。 Full code on pastebin.

public class ReactRenderFilter implements Filter {
    private static final Object LOCK = new Object();

    private static final ScriptEngine engine = new ScriptEngineManager().getEngineByMimeType("text/javascript");  
    private static final List<CompiledScript> scripts = new ArrayList<>();
    private static final ThreadLocal<RenderEngine> renderEngine = new ThreadLocal<>();

    private FilterConfig config;
    private String bundlePath;
    private String jspPath;

    public static class RenderEngine {
        private final ScriptContext context;
        private final ReactRenderer renderer;
        private final long lastModified;

        public RenderEngine(File bundle) throws ScriptException, UnsupportedEncodingException, FileNotFoundException {
            context = new SimpleScriptContext();
            Bindings global = engine.createBindings();
            context.setBindings(global, ScriptContext.ENGINE_SCOPE);
            global.put("global", global);
            for (CompiledScript script : scripts) {
                script.eval(context);
            }
            engine.eval(new InputStreamReader(new FileInputStream(bundle), "utf-8"), context);
            lastModified = bundle.lastModified(); 
            LOG.finer("Getting renderer");
            renderer = ((ScriptObjectMirror) engine.eval("global.render", context)).to(ReactRenderer.class);
        }

        String render(String path, String initialDataJSON) {
            return renderer.render(path, initialDataJSON);
        }

        boolean isOutdated(File bundle) {
            return lastModified != bundle.lastModified();
        }
    }


    public ReactRenderFilter() {super();}
    @Override public void destroy() {}

    @Override public void init(FilterConfig filterConfig) throws ServletException {
        config = filterConfig;
        try {
            String[] paths = ...
            for (String path : paths) {
                if (path.trim().isEmpty()) {continue;}
                File file = new File(config.getServletContext().getRealPath(path.trim()));
                scripts.add(((Compilable) engine).compile(new InputStreamReader(new FileInputStream(file), "utf-8")));
            }
            bundlePath = config.getServletContext().getRealPath(config.getInitParameter(PARAM_APP_BUNDLE_PATH).trim());
            jspPath = config.getInitParameter(PARAM_MARKUP_JSP_PATH).trim();
        } catch (UnsupportedEncodingException | FileNotFoundException | ScriptException e) {
            throw new ServletException("Unable to initialize ReactRenderServlet.", e);
        }
    }

    @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        File bundle = new File(bundlePath);
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        String path = req.getRequestURI().substring(req.getContextPath().length());
        String initialDataJSON = "{}";
        @SuppressWarnings("unchecked")
        Map<String, Object> initialData = (Map<String, Object>) req.getAttribute("initialData");
        if  (initialData != null) {
            ObjectMapper mapper = new ObjectMapper();
            initialDataJSON = mapper.writeValueAsString(initialData);
            req.setAttribute("initialDataJSON", initialDataJSON);
        }
        String renderResult = null;
        try {
            if (renderEngine.get() == null || renderEngine.get().isOutdated(bundle)) {
                // prevent multiple render engines to be instantiated simultaneously
                synchronized (LOCK) {
                    renderEngine.set(new RenderEngine(bundle));
                }
            }

            // I sure hope there is a way around this... locking on central object
            // during rendering can't be good for performance... But it beats having
            // only one worker thread
            synchronized (LOCK) {
                renderResult = renderEngine.get().render(path, initialDataJSON);
            }

            if (renderResult.startsWith(MARKUP)) {
                String markup = renderResult.substring(MARKUP.length());
                req.setAttribute("markup", markup);
                int maxAge = 60 * 60; // 60 minutes 
                res.addHeader("Cache-Control", "public, max-age=" + maxAge);
                res.addDateHeader("Expires", new Date().getTime() + maxAge);
                req.getRequestDispatcher(jspPath).forward(request, response);       
            }
            else if (renderResult.startsWith(REDIRECT)) {
                String url = renderResult.substring(REDIRECT.length());
                res.sendRedirect(url); 
            }
            else if (renderResult.startsWith(NOTFOUND)) {
                int maxAge = 365 * 24 * 60 * 60; // 365 days 
                res.addHeader("Cache-Control", "public, max-age=" + maxAge);
                res.addDateHeader("Expires", new Date().getTime() + maxAge);
                chain.doFilter(request, response);
            }
            else {
                String msg = renderResult.substring(ERROR.length());
                throw new ServletException("Unable to generate response for route [" + path + "]: " + msg);
            }
        } catch (ScriptException e) {
            throw new ServletException(e);
        }
    }
}

如您所见,我有一个静态 ScriptEngine 和一个单独的 ScriptContext + Bindings 每个线程(在 ThreadLocal 中)......我想(基于我发现的文档)这应该是线程安全的......无奈之下我在这个锁上添加了一个 LOCKsynchronized 块,但它似乎没有帮助。

我什至不确定它是否与线程有关,但确实如此。

上面的代码看起来是创建多个同时使用的脚本上下文的正确方法吗?

有什么技巧可以解决这个问题,甚至调试它吗?

此问题与 https://bugs.openjdk.java.net/browse/JDK-8145550 which has been fixed in jdk9 and backported to jdk8u-dev ( http://hg.openjdk.java.net/jdk8u/jdk8u-dev/nashorn/rev/fa7dce1af94e ). It'd be great If you can pull http://hg.openjdk.java.net/jdk8u/jdk8u-dev/nashorn 相似,构建 nashorn.jar 以针对您的应用进行测试。谢谢