I am trying to use Spring DeferredResult to do a lengthy poll. In this example, one user visits a page that uses a lengthy survey to wait for another user to click the link. The second user (you are in a different browser) then clicks on this link, and a long survey returns to the first user, notifying her of the second user click.
jsp looks like this:
<!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>
And the controller is as follows:
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); } }
And, for completeness, here is my class 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"; } }
Everything seems to be working fine. The first user is really notified when another user clicks on the link.
However, if this first user refreshes the page before the second user clicks on the link, I also get a stack trace for each "old" 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)
I can simply ignore these exceptions, but that seems wrong.
So my questions are:
- What causes the initial ClientAbortException? Should I do something else?
- What are the best practices for this kind of thing, preferably so as not to throw exceptions?
- Is there a better way to track DeferredResults for a long survey?
- Is there a way for subsequent exceptions
getOutputStream() has already been called for this response , which, as I understand it, is caused by the exception handler error page?
I have a mavenized version of this project available on GitHub here if you want to try it yourself.
In the end, I'm trying to add a notification system to my Spring website, similar to the StackOverflow notification system. If there is a better way to do this with Spring and a long poll, I'm all ears.
Edit: I did not receive any answers (or even comments), so I added generosity. I would definitely appreciate any feedback!