Spring DeferredResult导致IOException:已建立的连接被主机中的软件中止

时间:2015-08-20 23:49:01

标签: 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>

控制器看起来像这样:

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 here上找到该项目的一个mavenized版本。

最后我试图在我的Spring网站上添加一个通知系统,类似于StackOverflow的通知系统。如果有一个更好的方法来做到这一点与春季和长期民意调查,我都是耳朵。

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

3 个答案:

答案 0 :(得分:5)

它失败的原因是因为当第一个用户关闭浏览器时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仍然没有取消延迟,那么防止异常实际发生并没有太多事情要做:Detecting client disconnect in tomcat servlet?

请注意,长整流和彗星一般来说很难从头开始构建,你应该考虑使用Atmosphere这样的东西。它将为您提供旧的浏览器兼容性的websockets和comet。

关于您的其他问题

  • 做这种事情的最佳做法是什么,最好是以不产生例外的方式?

    • 您应该考虑使用WebSockets。 Spring 4.2附带了一些不错的功能,还可以检查Atmosphere
    • 你也可以给server-sent events
    • 一个机会
  • 是否有更好的方法来跟踪长轮询的DeferredResults?

    • 如果每个人都能点击按钮并通知所有人都在倾听,我猜你的方式还可以。如果您想要一些更具针对性的行为(比如专门通知用户他的个人事件),那么您应该保留一个用户地图和待处理的长轮询DeferredResults。
  • 有没有办法围绕后续的getOutputStream()调用此响应异常,我收集的是异常处理程序的错误页面引起的?

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

答案 1 :(得分:1)

  

导致原始ClientAbortException的原因是什么?我应该这样做吗?   不同的东西?

当我们使用DeferredResult或Callable时:

ClientAbortException:客户端发出了一些其他请求/单击其他链接。 这意味着当前操作被挂起,它会在当前运行的线程上抛出ClientAbortException。

  

已建立的连接已被主机中的软件中止   机

最重要的是,Windows防火墙也可以执行此类中止功能检查一次。

  

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

的WebSockets

  

是否有更好的方法可以长时间跟踪DeferredResults   轮询?

为不同的用户定义一个集合以及长轮询。

  

有没有办法解决后续的getOutputStream()问题   被称为此响应异常,我收集的是由此引起的   异常处理程序的错误页面?

这也可能是引发此当前异常的原因。

答案 2 :(得分:-1)

<a href="clickTheThing">Click this thing.</a> . If you use a tag href this web page will reload. should use preventDefault to disable reload page.

然后浏览器不会关闭连接。