异步上下文响应与原始传入请求不匹配?

我们有一个带有仪表板的Web应用程序,该应用程序不断轮询更新。在服务器端,更新请求是异步的,以便我们可以在通过侦听器/通知系统进行更新时做出响应。

我们看到的问题是,当其中一个轮询请求得到响应时,在某些情况下,它可以写入用户单击链接的请求/响应。

异步更新的传入请求如下所示:

@RequestMapping("/getDashboardStatus.json")
public void getDashboardStatus(HttpServletRequest request, ...) {
    final AsyncContext asyncContext = request.startAsync();
    // 10 seconds
    asyncContext.setTimeout(10000);
    asyncContext.start(new Runnable() {
        public void run() {
            // .. (code here waits for an update to occur) ..
            sendMostRecentDashboardJSONToResponse(asyncContext.getResponse());
            if (asyncContext.getRequest().isAsyncStarted()) {
                asyncContext.complete();
            }
        }
    });
}

奇怪的是,此仪表板上有指向其他页面的链接。每大约100次点击,其中一次将不显示所选页面,而是实际显示上面发送的JSON!

例如,我们有一个单独的 MVC 方法:

@RequestMapping("/result/{resultId}")
public ModelAndView getResult(@PathVariable String resultId) {
    return new ModelAndView(...);
}

当单击仪表板上访问的链接时,每个蓝月亮,该页面将加载200 OK状态,但不包含预期的HTML,实际上包含轮询请求的JSON!/result/1234

是否每个客户端只允许一个请求?由单击的链接发起的请求是否会覆盖已位于服务器端的同一客户端的任何异步请求?

我们如何管理这些请求以确保异步响应转到异步请求?

我注意到对象上有一个方法,但很难从Javadoc中理解它是否是我正在寻找的。hasOriginalRequestAndResponse()AsyncContext

更新:我刚刚添加了一个这样的片段:

String requestURI = ((HttpServletRequest)asyncContext.getRequest()).getRequestURI());
System.out.println("Responding w/ Dashboard to: " + requestURI);
sendMostRecentDashboardJSONToResponse(asyncContext.getResponse(), clientProfileKey);

并且能够在正常行为期间重现问题,我看到:

Responding w/ Dashboard to: /app/getDashboardStatus.json

但是当我看到JSON被推送到点击发起的请求时,我看到:

Responding w/ Dashboard to: null

答案 1

我已经弄清楚了这一点。请求/响应确实被回收,因此通过挂起分配给 的响应,我写出了与不同请求关联的响应。AsyncContext

调用保证在异步上下文完成之前不会回收请求/响应对象。尽管我发现没有一个地方可以过早或错误地完成上下文,但它正在完成:startAsync()

通过超时。

我能够通过在没有服务器端活动的情况下等待10多秒,允许JSON更新请求超时,然后单击链接来持续重现此问题。超时后,与异步上下文关联的请求/响应将超时,从而完成,从而回收

我发现了两种解决方案

第一种方法是向上下文中添加 ,并跟踪超时是否发生。当侦听器检测到超时时,您可以翻转 一个 ,并在写入响应之前对其进行检查。AsyncListenerboolean

种方法是在写入响应之前简单地调用请求。如果上下文已超时,此方法将返回 。如果上下文仍然有效/正在等待,它将返回 。isAsyncStarted()falsetrue


答案 2

如果您阅读Spring参考文档中的异步请求处理一章,您会注意到您可以简化解决方案:

@RequestMapping(value = "/getDashboardStatus.json", 
                produces = MediaType.APPLICATION_JSON_VALUE)
public Callable<MostRecentDashboard> getDashboardStatus() {
    return new Callable() {
        @Override
        public MostRecentDashboard call() {
            // .. (code here waits for an update to occur) ..
            return ...;
        }
    });
}

该实例将使用 Jackson 进行序列化(前提是 Jackson 2 位于类路径上)。MostRecentDashboard

接下来,您需要一个将执行的(它也常用于配置默认超时),阅读Spring MVC异步配置段落以获取选项。或者,如果 Spring 不知道分配值的线程,则可以返回一个,例如,实例是从 JMS、Redis 或类似位置读取的。有关详细信息,请阅读 Servlet 3 异步支持简介博客文章。TaskExecutorCallableDeferredResult<MostRecentDashboard>MostRecentDashboardMostRecentDashboard

Spring版本4.1开始,您可以直接从控制器返回,如果您使用.ListenableFuture<MostRecentDashboard>MostRecentDashboardAsyncRestTemplate


推荐