Java AsyncContext响应与原始传入请求不匹配?

Java AsyncContext响应与原始传入请求不匹配?,java,spring,spring-mvc,jakarta-ee,asynchronous,Java,Spring,Spring Mvc,Jakarta Ee,Asynchronous,我们有一个web应用程序,它有一个仪表板,不断轮询更新。在服务器端,更新请求是异步的,这样我们就可以在更新发生时通过侦听器/通知系统进行响应 我们看到的问题是,当其中一个轮询请求被响应时,在某些情况下,它可以为用户单击的链接写入请求/响应 异步更新的传入请求如下所示: @RequestMapping("/getDashboardStatus.json") public void getDashboardStatus(HttpServletRequest request, ...) { f

我们有一个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(...);
}
当点击访问
/result/1234
的仪表板上的链接时,每个blue moon页面都会加载200 OK状态,但不是包含预期的HTML,而是实际包含轮询请求的JSON

每个客户端只允许一个请求吗?由单击的链接启动的请求是否会覆盖服务器端已存在的来自同一客户端的任何异步请求

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

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

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

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

我已经弄明白了。请求/响应确实在循环使用,因此通过挂起分配给
AsyncContext
的响应,我正在向与不同请求相关联的响应写入数据

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

超时。

我可以在没有服务器端活动的情况下等待10秒以上,允许JSON更新请求超时,然后单击一个链接,从而始终如一地重现这个问题。超时后,与异步上下文关联的请求/响应超时,从而完成,从而回收

我发现了两个解决方案

第一种方法是在上下文中添加一个
AsyncListener
,并跟踪超时是否发生。当侦听器检测到超时时,您可以翻转一个
布尔值,并在写入响应之前检查它

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

如果您阅读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 ...;
        }
    });
}
@RequestMapping(value=“/getDashboardStatus.json”,
products=MediaType.APPLICATION\u JSON\u值)
公共可调用getDashboardStatus(){
返回新的可调用(){
@凌驾
公共MostRecentDashboard调用(){
//..(此处的代码等待更新发生)。。
返回。。。;
}
});
}
MostRecentDashboard
实例将使用Jackson进行序列化(前提是Jackson 2位于类路径上)

接下来,您需要一个
任务执行器
,该执行器将执行
可调用的
(它也常用于配置默认超时),请阅读段落中的选项。或者,如果Spring不知道分配
MostRecentDashboard
值的线程,则可以返回
DeferredResult
,例如
MostRecentDashboard
实例是从JMS、Redis或类似工具读取的。有关更多信息,请阅读博客文章


截至,您可以直接从控制器返回
ListenableFuture
,如果您使用
AsyncRestTemplate

获取
MostRecentDashboard
,这将非常有用。关于此问题中使用的标记,目前正在进行元讨论:@Craig Otis您是否尝试使用
request.startAsync(请求,响应);
而不是
request.startAsync()
?@fmodos我阅读了文档,但实际上没有使用它-在这种情况下,行为似乎不会改变?似乎传入该方法的请求/响应参数需要与传入的请求/响应相同(或包装器),因此它的操作似乎相同。可能与此相关。@CraigOtis我认为在您的场景中,异步上下文与实际附加的响应丢失了。我同意您关于javadoc的看法,也希望在内部启动一个请求的异步
startAsync()。。。但出于测试目的,可能值得一试:)第二种解决方案可能会偶尔失败,因为处理线程和超时处理线程之间存在竞争,因此在调用isAsyncStarted和后续写入之间可能会发生超时。使用超时侦听器的解决方案还必须在超时侦听器和写入之间应用互斥,以避免相同的问题。您能否给我一个如何删除布尔值的示例。我使用的是Tomact 9.0.3+,我无法再获取请求或响应,因为它引发了非法状态异常。除了读取不同的线程外,还有其他方法可以删除布尔标志吗