轉載出處: https://blog.csdn.net/TimerBin/article/details/90295451
我的解決方法:
當時出這個錯誤時腦子里一頭霧水也在網上找了很多資料說是
response已經被其他對象調用了,按網上的說法做也沒有解決。
於是我試着嘗試在Controller層文件下載接口加入@ResponseBoby注解
沒想到問題竟然解決了,文件下載之后后台也沒有出現這個錯誤了,下載的
文件也能正常打開
一、背景說明
在tomcat的localhost.log日志中時長見到 getOutputStream() has already been called for this response 異常的身影,一直不知由於哪里原因導致異常的產生,此異常並不會影響前端客戶正常使用。
二、認識異常
異常詳情如下所示(部分代碼):
org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [springServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.IllegalStateException: getOutputStream() has already been called for this response] with root cause java.lang.IllegalStateException: getOutputStream() has already been called for this response at org.apache.catalina.connector.Response.getWriter(Response.java:579) at org.apache.catalina.connector.ResponseFacade.getWriter(ResponseFacade.java:212) at org.springframework.web.servlet.view.velocity.VelocityView.mergeTemplate(VelocityView.java:519) at org.springframework.web.servlet.view.velocity.VelocityLayoutView.doRender(VelocityLayoutView.java:169) at org.springframework.web.servlet.view.velocity.VelocityView.renderMergedTemplateModel(VelocityView.java:294) at org.springframework.web.servlet.view.AbstractTemplateView.renderMergedOutputModel(AbstractTemplateView.java:167) at org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:303) at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1257) at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1037) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:980) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:897) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:970) at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:872) at javax.servlet.http.HttpServlet.service(HttpServlet.java:648) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846) at javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
getOutputStream() has already been called for this response 翻譯過來是“getOutputStream 已經被要求做出這種回應”,普通話說就是response.getOutputStream() 已經用過了不能再次使用。
具體模擬此異常的偽代碼如下所示(請自行忽略流關閉等相關代碼):
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ServletOutputStream output= response.getOutputStream(); response.getWriter().print("timerbin"); return true; }
注:問題擴展
getWriter() has already been called for this response 此異常具體原因同上,區別在於,如下所示:
具體模擬此異常的偽代碼如下所示(請自行忽略流關閉等相關代碼):
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { response.getWriter().print("timerbin"); ServletOutputStream output= response.getOutputStream(); return true; }
沒錯你沒有看錯,只要是將response.getOutputStream()和 response.getWriter() 位置調換就會出現不同異常信息
三、了解異常
想要了解它就直接去看源碼:
直接查看Response源碼,其中核心代碼如下所示
protected boolean usingOutputStream = false; protected boolean usingWriter = false; @Override public ServletOutputStream getOutputStream() throws IOException { if (usingWriter) { throw new IllegalStateException (sm.getString("coyoteResponse.getOutputStream.ise")); } usingOutputStream = true; if (outputStream == null) { outputStream = new CoyoteOutputStream(outputBuffer); } return outputStream; } @Override public PrintWriter getWriter() throws IOException { if (usingOutputStream) { throw new IllegalStateException (sm.getString("coyoteResponse.getWriter.ise")); } if (ENFORCE_ENCODING_IN_GET_WRITER) { setCharacterEncoding(getCharacterEncoding()); } usingWriter = true; outputBuffer.checkConverter(); if (writer == null) { writer = new CoyoteWriter(outputBuffer); } return writer; }
一目了然,在Response中有兩個Boolean類型的標usingOutputStream 和 usingWriter,哪個被用過就會被標為true,同一個請求,另一個就會報錯。
繼續查看Response源碼發現一個重置方法,代碼如下所示:
@Override public void reset() { if (included) { //忽略servlet調用 return; } getCoyoteResponse().reset(); outputBuffer.reset(); usingOutputStream = false; usingWriter = false; isCharacterEncodingSet = false; }
通過這里可以找到一個此問題的解決方式,如果在代碼中確實存在同時調用了response.getOutputStream()和 response.getWriter() 的話,可以在兩個方法中間加上response.reset()代碼,解決以上報錯。
四、解決異常
非常幸運的是,我們項目中的問題有兩個特性:
1、在我們項目中並沒有找到可能存在連續調用response.getOutputStream() 和 response.getWriter() 代碼的地方
2、getOutputStream() has already been called for this response 異常在我們項目中是偶現的,同樣的功能時好時壞。
由於以上二個特性導致我定位到我們項目中問題耗時了2天。
使用的手段:
1.修改logback.xml配置文件,增加[%thread] 配置,在每一行日志中輸出線程ID,並將日志級別調整為INFO級別
2.增加全局攔截器輸出所有請求路徑
3.排查代碼中所有使用到response.getOutputStream() 和 response.getWriter() 的地方
了解到的知識點:
SpringMVC 中所有Controller接口進行返回時底層都是用response.getOutputStream() 或 response.getWriter()進行輸出的,同時又增加了排查問題的難度。
通過對線上環境中所有日志進行監控發現,在出現getOutputStream() has already been called for this response 異常之前都出現了java.io.IOException: Broken pipe 異常(日志中ThreadId相同),由於tomcat日志和業務日志分開隔離存儲,導致延后暴漏了問題的本質。
小知識:
java.io.IOException: Broken pipe 翻譯過來是 “斷開的管道” ,具體異常詳情如下所示:
org.apache.catalina.connector.ClientAbortException: java.io.IOException: Broken pipe at org.apache.catalina.connector.OutputBuffer.realWriteBytes(OutputBuffer.java:393) at org.apache.tomcat.util.buf.ByteChunk.flushBuffer(ByteChunk.java:426) at org.apache.tomcat.util.buf.ByteChunk.append(ByteChunk.java:339) at org.apache.catalina.connector.OutputBuffer.writeBytes(OutputBuffer.java:418) at org.apache.catalina.connector.OutputBuffer.write(OutputBuffer.java:406) at org.apache.catalina.connector.CoyoteOutputStream.write(CoyoteOutputStream.java:97) at com.fasterxml.jackson.core.json.UTF8JsonGenerator._flushBuffer(UTF8JsonGenerator.java:2039) at com.fasterxml.jackson.core.json.UTF8JsonGenerator.flush(UTF8JsonGenerator.java:1051) at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:953)
首先Broken pipe 斷開的管道這個異常是允許在程序中出現。因為該異常是在客戶端瀏覽器向服務端發起請求后,未等到服務端進行正常響應就關閉瀏覽器或頁面,可以通過優化服務端接口處理性能來減少此類問題的產生,也有前輩推薦修改tomcat配置文件來減少此問題出現(不建議這么做)。
我們項目中問題的第二個特性:問題偶現
通過以上的代碼分析,已經確認我們項目中問題偶現的原因是由於客戶端向服務端發起請求后,不待服務端正常返回直接關閉瀏覽器或頁面,從而出現Broken pipe 異常,最終導致getOutputStream() has already been called for this response 異常的產生。
我們項目中問題的第一個特性:未同時使用response.getOutputStream() 或 response.getWriter() 代碼
由於Broken pipe異常的存在,成功讓我縮小了定位問題的范圍,經過排查發現項目中使用了SpringMvc的統一錯誤處理器,具體代碼如下所示:
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; public class ExceptionResolver extends AbstractHandlerExceptionResolver { @Override protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { /** * 配置不同的異常白名單 -> 跳轉到指定的錯誤頁 * java.lang.Exception -> error/404 * java.lang.Throwable -> error/404 */ String viewName = getErrorView(ex, request); if (viewName != null) { ModelAndView mav = new ModelAndView(); mav.setViewName(viewName); return mav; }else { return null; } } }
通過以上代碼就可以明確的分析出,我們項目中問題特性一的原因,由於在服務端接口處理完成后已正常返回,但是不幸遭遇Broken pipe異常,同時由於Broken pipe異常被ExceptionResolver 統一異常處理器捕獲,再次返回到了error/404 錯誤頁,最終導致在程序中出現錯誤異常,由於進行了兩次Response
解決方案(臨時解決方案):
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; public class ExceptionResolver extends AbstractHandlerExceptionResolver { @Override protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if ("org.apache.catalina.connector.ClientAbortException".equals(ex.getClass().getName())) { return null; } /** * 配置不同的異常白名單 -> 跳轉到指定的錯誤頁 * java.lang.Exception -> error/404 * java.lang.Throwable -> error/404 */ String viewName = getErrorView(ex, request); if (viewName != null) { ModelAndView mav = new ModelAndView(); mav.setViewName(viewName); return mav; }else { return null; } } }
估計每個人都會發現,筆記寫到這里,感覺並未把問題完全描述清楚,沒錯是這樣的,你沒有感覺錯。
由於我依舊存在不確定因素,就是在SpringMVC中在Controller接口中進行返回ModelAndView和 @ResponseBody 時,到底哪個使用了response.getOutputStream() ,而又是哪個使用了response.getWriter() 。
五、參考資料
https://stackoverflow.com/questions/21039471/spring-getoutputstream-has-already-been-called-for-this-response
https://www.iteye.com/problems/91763
https://bbs.csdn.net/topics/390341340