java - Spring DeferredResult 导致 IOException : An established connection was aborted by the software in your host machine

标签 java spring spring-mvc tomcat

我正在尝试使用 Spring 的 DeferredResult 来执行长轮询。在此示例中,一个用户访问一个使用长轮询等待另一个用户单击链接的页面。然后第二个用户(您在另一个浏览器中)点击该链接,长轮询返回给第一个用户,通知她第二个用户的点击。

jsp 看起来像这样:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Spring Example</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
    <script>
    function pollContent() {
        $.ajax({url: "waitForClick", success: function(result){
            console.log("Polled result: " + result);
            $("#polledContent").html(result);
            pollContent();
        }});
    }
    $(pollContent);
    </script>
  </head>
<body>
    <p><a href="clickTheThing">Click this thing.</a></p>
    <p id="polledContent">Waiting for somebody to click the thing...</p>
</body>
</html>

Controller 看起来像这样:

package com.example.controller;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Component;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.async.DeferredResult;

import com.example.controller.interfaces.ExampleControllerInterface;

@Component
public class ExampleController implements ExampleControllerInterface{

    private int clickCount = 0;

    private List<DeferredResult<String>> waiting = new ArrayList<>();

    @Override
    public String viewHomePage(HttpServletRequest request, ModelMap model, HttpSession session){
        return "index";
    }

    @RequestMapping(value = "/clickTheThing", method = RequestMethod.GET)
    public String clickTheThing(HttpServletRequest request, ModelMap model, HttpSession session){

        new Thread(){
            public void run(){

                clickCount++;
                System.out.println("Somebody clicked the thing! Click count: " + clickCount);
                Iterator<DeferredResult<String>> iterator = waiting.iterator();
                while(iterator.hasNext()){
                    DeferredResult<String> result = iterator.next();
                    System.out.println("Setting result.");
                    result.setResult("Somebody clicked the thing! Click count: " + clickCount);
                    iterator.remove();
                }
            }
        }.start();

        return "clicked";
    }

    @ResponseBody
    @RequestMapping(value = "/waitForClick", method = RequestMethod.GET)
    public DeferredResult<String> waitForClick(HttpServletRequest request, ModelMap model, HttpSession session){
        final DeferredResult<String> result = new DeferredResult<>();
        waiting.add(result);
        return result;
    }

    @ResponseBody
    @RequestMapping(value = "/getClickCount", method = RequestMethod.GET)
    public String getClickCount(HttpServletRequest request, ModelMap model, HttpSession session){
        return String.valueOf(clickCount);
    }
}

为了完整起见,这是我的 ErrorConfig 类:

package com.example.config;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class ErrorConfig{

    @ExceptionHandler(Exception.class)
    public String handleException (HttpServletRequest request, HttpServletResponse response, HttpSession session, Exception e) {
        e.printStackTrace();
        return "index";
    }
}

这似乎工作正常。每当其他用户单击该链接时,第一位用户确实会收到通知。

但是,如果第一个用户在第二个用户单击链接之前刷新了页面,我还会得到每个“旧”DeferredResult 的堆栈跟踪:

org.apache.catalina.connector.ClientAbortException: java.io.IOException: An established connection was aborted by the software in your host machine
    at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:396)
    at org.apache.tomcat.util.buf.ByteChunk.flushBuffer(ByteChunk.java:426)
    at org.apache.catalina.connector.OutputBuffer.doFlush(OutputBuffer.java:342)
    at org.apache.catalina.connector.OutputBuffer.flush(OutputBuffer.java:316)
    at org.apache.catalina.connector.CoyoteOutputStream.flush(CoyoteOutputStream.java:110)
    at sun.nio.cs.StreamEncoder.implFlush(StreamEncoder.java:297)
    at sun.nio.cs.StreamEncoder.flush(StreamEncoder.java:141)
    at java.io.OutputStreamWriter.flush(OutputStreamWriter.java:229)
    at org.springframework.util.StreamUtils.copy(StreamUtils.java:106)
    at org.springframework.http.converter.StringHttpMessageConverter.writeInternal(StringHttpMessageConverter.java:106)
    at org.springframework.http.converter.StringHttpMessageConverter.writeInternal(StringHttpMessageConverter.java:40)
    at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:208)
    at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:143)
    at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:89)
    at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:193)
    at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:71)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:122)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:749)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:689)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:83)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:938)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:870)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:961)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:852)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:618)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:837)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:301)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:721)
    at org.apache.catalina.core.ApplicationDispatcher.doDispatch(ApplicationDispatcher.java:639)
    at org.apache.catalina.core.ApplicationDispatcher.dispatch(ApplicationDispatcher.java:605)
    at org.apache.catalina.core.AsyncContextImpl$1.run(AsyncContextImpl.java:208)
    at org.apache.catalina.core.AsyncContextImpl.doInternalDispatch(AsyncContextImpl.java:363)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:214)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:503)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:136)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:78)
    at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:610)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
    at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:405)
    at org.apache.coyote.http11.AbstractHttp11Processor.asyncDispatch(AbstractHttp11Processor.java:1636)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:646)
    at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:222)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1566)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1523)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.io.IOException: An established connection was aborted by the software in your host machine
    at sun.nio.ch.SocketDispatcher.write0(Native Method)
    at sun.nio.ch.SocketDispatcher.write(SocketDispatcher.java:51)
    at sun.nio.ch.IOUtil.writeFromNativeBuffer(IOUtil.java:93)
    at sun.nio.ch.IOUtil.write(IOUtil.java:65)
    at sun.nio.ch.SocketChannelImpl.write(SocketChannelImpl.java:470)
    at org.apache.tomcat.util.net.NioChannel.write(NioChannel.java:122)
    at org.apache.tomcat.util.net.NioBlockingSelector.write(NioBlockingSelector.java:101)
    at org.apache.tomcat.util.net.NioSelectorPool.write(NioSelectorPool.java:173)
    at org.apache.coyote.http11.InternalNioOutputBuffer.writeToSocket(InternalNioOutputBuffer.java:139)
    at org.apache.coyote.http11.InternalNioOutputBuffer.addToBB(InternalNioOutputBuffer.java:197)
    at org.apache.coyote.http11.InternalNioOutputBuffer.access$000(InternalNioOutputBuffer.java:41)
    at org.apache.coyote.http11.InternalNioOutputBuffer$SocketOutputBuffer.doWrite(InternalNioOutputBuffer.java:320)
    at org.apache.coyote.http11.filters.IdentityOutputFilter.doWrite(IdentityOutputFilter.java:84)
    at org.apache.coyote.http11.AbstractOutputBuffer.doWrite(AbstractOutputBuffer.java:257)
    at org.apache.coyote.Response.doWrite(Response.java:523)
    at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:391)
    ... 50 more
Aug 20, 2015 7:19:24 PM org.apache.catalina.core.ApplicationDispatcher invoke
SEVERE: Servlet.service() for servlet jsp threw exception
java.lang.IllegalStateException: getOutputStream() has already been called for this response
    at org.apache.catalina.connector.Response.getWriter(Response.java:535)
    at org.apache.catalina.connector.ResponseFacade.getWriter(ResponseFacade.java:212)
    at javax.servlet.ServletResponseWrapper.getWriter(ServletResponseWrapper.java:103)
    at org.apache.jasper.runtime.JspWriterImpl.initOut(JspWriterImpl.java:115)
    at org.apache.jasper.runtime.JspWriterImpl.flushBuffer(JspWriterImpl.java:108)
    at org.apache.jasper.runtime.PageContextImpl.release(PageContextImpl.java:173)
    at org.apache.jasper.runtime.JspFactoryImpl.internalReleasePageContext(JspFactoryImpl.java:120)
    at org.apache.jasper.runtime.JspFactoryImpl.releasePageContext(JspFactoryImpl.java:75)
    at org.apache.jsp.WEB_002dINF.jsp.index_jsp._jspService(index_jsp.java:93)
    at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:70)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
    at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:432)
    at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:403)
    at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:347)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:301)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:721)
    at org.apache.catalina.core.ApplicationDispatcher.doInclude(ApplicationDispatcher.java:584)
    at org.apache.catalina.core.ApplicationDispatcher.include(ApplicationDispatcher.java:523)
    at org.springframework.web.servlet.view.InternalResourceView.renderMergedOutputModel(InternalResourceView.java:201)
    at org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:267)
    at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1221)
    at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1005)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:952)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:870)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:961)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:852)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:618)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:837)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:301)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:721)
    at org.apache.catalina.core.ApplicationDispatcher.doDispatch(ApplicationDispatcher.java:639)
    at org.apache.catalina.core.ApplicationDispatcher.dispatch(ApplicationDispatcher.java:605)
    at org.apache.catalina.core.AsyncContextImpl$1.run(AsyncContextImpl.java:208)
    at org.apache.catalina.core.AsyncContextImpl.doInternalDispatch(AsyncContextImpl.java:363)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:214)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:503)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:136)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:78)
    at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:610)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
    at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:405)
    at org.apache.coyote.http11.AbstractHttp11Processor.asyncDispatch(AbstractHttp11Processor.java:1636)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:646)
    at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:222)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1566)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1523)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)

我可以简单地忽略这些异常,但感觉不对。

所以,我的问题是:

  • 是什么导致了最初的 ClientAbortException?我应该做些不同的事情吗?
  • 做这种事情的最佳实践是什么,最好是以不会产生异常的方式?
  • 是否有更好的方法来跟踪长轮询的 DeferredResults?
  • 是否有办法解决随后的 getOutputStream() has already been called for this response 异常,我认为这些异常是由异常处理程序的错误页面引起的?

我在 GitHub 上提供了这个项目的 mavenized 版本 here如果您想自己尝试一下。

最后,我尝试向我的 Spring 网站添加一个通知系统,类似于 StackOverflow 的通知系统。如果有更好的方法使用 Spring 和长轮询来做到这一点,我会洗耳恭听。

编辑:我没有收到任何答复(甚至评论),所以我增加了赏金。我非常感谢任何反馈!

最佳答案

失败的原因是,当第一个用户关闭浏览器时,Stream 被关闭,当您尝试设置 DeferredResult 的结果时,spring 会尝试将其发送到客户端,从而导致错误。

在写入结果之前,您应该通过调用它的 isSetOrExpired() 方法来尝试检查 DeferredResult 是否处于可用状态:

            while(iterator.hasNext()){
                DeferredResult<String> result = iterator.next();
                System.out.println("Setting result.");
                if(!result.isSetOrExpired()){
                    result.setResult("Somebody clicked the thing! Click count: " + clickCount);
                }
                iterator.remove();
            }

如果当客户端关闭连接时 Spring 仍然没有取消 Deferred,那么没有太多的事情可以防止异常实际发生:Detecting client disconnect in tomcat servlet? .

请注意,长极化和 cometd 通常很难从头开始构建,您应该考虑使用类似 Atmosphere 的东西为了这。它将为您提供 websockets 和 comet 以实现旧浏览器兼容性。

关于您的其他问题

  • 做这种事情的最佳实践是什么,最好是以不产生异常的方式?

    • 您应该考虑为此使用 WebSockets。 Spring 4.2 带有一些不错的特性,同时查看 Atmosphere
    • 您也可以给机会 server-sent events
  • 是否有更好的方法来跟踪长轮询的 DeferredResults?

    • 如果每个人都能点击按钮并通知每个收听的人,我想你的方式是可行的。如果你想要一些更有针对性的行为(比如专门通知用户他的个人事件),那么你应该保留一个用户 map 和挂起的长轮询 DeferredResults。
  • 是否有办法绕过后续的 getOutputStream() 已被调用用于此响应异常,我认为这是由异常处理程序的错误页面引起的?

    • 发生该错误的原因与第一个错误相同。当发生错误时,servlet 容器试图将错误 JSP 打印到导致错误的关闭流。解决这个问题的方法就是在得到答复之前关闭请求时不要发生任何错误:-)。

关于java - Spring DeferredResult 导致 IOException : An established connection was aborted by the software in your host machine,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32130249/

相关文章:

java - @Autowired bean 在另一个 bean 的构造函数中引用时为空

java - 如何使用 Autowiring 在 spring 中从属性文件中检索键的值

Java/Spring JDBC : Batch Insert into 2 Tables: Obtain FK ID from 1st Batch Insert Required for 2nd Table

java - 无需组件扫描或 bean 定义即可定义 Controller

spring - Spring @Async取消并开始吗?

java - GlassFish 中的 mysql 连接器有问题还是只有我?

java - 在 Java Swt 和内存消耗中缩放图像

java - 以原始文件名下载文件

Java:比较字符数组是否区分大小写?

Java 教程 .isEmpty() 方法