本文章会对getWriter() has already been called for this response异常在SpringBoot下出现的情况进行分析和解决。 如果你只是想看解决方案,请直接拖到文章最后。
在把tomcat应用升级到SpringBoot后,部分http接口出现了getWriter() has already been called for this response异常。但是异常报出的很模糊,下面贴出异常:
java.lang.IllegalStateException: getWriter() has already been called for this response at org.apache.catalina.connector.Response.getOutputStream(Response.java:590) at org.apache.catalina.connector.ResponseFacade.getOutputStream(ResponseFacade.java:194) at javax.servlet.ServletResponseWrapper.getOutputStream(ServletResponseWrapper.java:100) at org.springframework.boot.web.servlet.support.ErrorPageFilter$ErrorWrapperResponse.getOutputStream(ErrorPageFilter.java:371) at javax.servlet.ServletResponseWrapper.getOutputStream(ServletResponseWrapper.java:100) at org.springframework.session.web.http.OnCommittedResponseWrapper.getOutputStream(OnCommittedResponseWrapper.java:139) at org.springframework.http.server.ServletServerHttpResponse.getBody(ServletServerHttpResponse.java:83) at com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter.writeInternal(FastJsonHttpMessageConverter.java:330) at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:227) at com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter.write(FastJsonHttpMessageConverter.java:244) at javax.servlet.http.HttpServlet.service(HttpServlet.java:661) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882) at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at com.howbuy.cms.base.filter.CheckLoginFilter.doFilter(CheckLoginFilter.java:157) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.session.web.http.SessionRepositoryFilter.doFilterInternal(SessionRepositoryFilter.java:151) at org.springframework.session.web.http.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:86) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:128) at org.springframework.boot.web.servlet.support.ErrorPageFilter.access$000(ErrorPageFilter.java:66) at org.springframework.boot.web.servlet.support.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:103) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:121) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:118) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81) at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:650) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:800) at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:806) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1498) at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:748)字面意思很明显,getWriter()已经被调用。 并且能看到的有用信息只是fastJson。(因为我的tomcat应用还包含sitemesh,urlwriter等,该异常也会出现在sitemesh与urlwriter上,并不一定是fastJson)
通过本地调试,却并没有复现出该问题(奇怪是产线一直出现)。起初怀疑是并发问题,但细一想应该不会。这个问题只有在升级后才出现。
进而怀疑是不是因为SpringMVC的版本问题(升级前该应用的springMvc版本为2.5.3,升级后为5.1.8)? 将应用去掉SpringBoot, 采用SpringMVC方式 (Spring版本依然为5.1.8),无法重现。
自此,该问题陷入泥潭(因为本地无法复现,无法追踪)
很幸运,在对一个文件上传接口进行测试时,该问题在本地终于出现。 下面贴出代码(已删除不必要的代码):
@ResponseBody @RequestMapping("/fileupload/new.htm") protected Map<String, Object> handleRequestInternal(HttpServletRequest request, HttpServletResponse response) { Map<String, Object> resultMap = new HashMap<String, Object>(); PrintWriter printWriter = null; try { printWriter = response.getWriter(); ... } catch (IOException e) { LOGGER.error("[ fileUpload ][error] -> 上传失败"); resultMap.put("success", false); resultMap.put("msg", "上传失败"); } finally { } return resultMap;; }因为在升级后,以前的代码并没有过多的更改。 可以发现,这个接口本来是要返回json的,但因为历史原因,写法采用的是response.getWriter().write()的方式,并且升级过程中,没有完全删除response.getWriter()。而在升级后,增加了fastJson的配置。 下面贴出fastJson配置:
@ControllerAdvice public class JsonpConverter extends FastJsonHttpMessageConverter implements ResponseBodyAdvice { @Override public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { HttpServletRequest servletRequest = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest(); String callback = servletRequest.getParameter("callback"); if(StringUtils.isNotBlank(callback)){ JSONPObject jsonp = new JSONPObject(callback); jsonp.addParameter(o); HttpServletResponse response = ((ServletServerHttpResponse) serverHttpResponse).getServletResponse(); PrintWriter pw = null; try { pw = response.getWriter(); pw.write(jsonp.toJSONString()); } catch (IOException e) { e.printStackTrace(); }finally { if(pw != null){ pw.flush(); pw.close(); } } } return o; } }这里采用的也是response.getWriter()。 而报错原因就是getWriter()应被调用。我通过去掉文件上传接口中的response.getWriter()后本地不在出现该异常。
对于fastJson,这个配置是以前沿用下来的,为什么以前可以,现在不可以了呢?
通过源码追踪,我在tomcat-embed-core-9.0.24.jar下的Response类中的getOutputStream()方法中发现了这么一段代码:
public ServletOutputStream getOutputStream() throws IOException { if (this.usingWriter) { throw new IllegalStateException(sm.getString("coyoteResponse.getOutputStream.ise")); } else { this.usingOutputStream = true; if (this.outputStream == null) { this.outputStream = new CoyoteOutputStream(this.outputBuffer); } return this.outputStream; } }我能肯定,我的业务代码和fastjson代码并没有改变(文件上传中的response.getWriter()已经删除,只保留fastjson配置中的response.getWriter())。 实际调用的response.getOutputSteam,并且注意,代码中有个if判断usingWriter, 而usingWriter被设置为true的地方只有一个:
public PrintWriter getWriter() throws IOException { if (this.usingOutputStream) { throw new IllegalStateException(sm.getString("coyoteResponse.getWriter.ise")); } else { if (ENFORCE_ENCODING_IN_GET_WRITER) { this.setCharacterEncoding(this.getCharacterEncoding()); } this.usingWriter = true; this.outputBuffer.checkConverter(); if (this.writer == null) { this.writer = new CoyoteWriter(this.outputBuffer); } return this.writer; } }可以看到这两个方法,互相判断,应该是为了保证调用的resposne写方法一致,要不都用getWriter(),要不都用getOutputStream()。而我并没有使用getOutputStream(),那就只能说SpringBoot中底层代码用的getOutputStream。 那我理解就是新的Spring已经不希望你使用getWriter()了么?
总之,无论现在的Spring处于什么原因,造成了这个问题,只要避免了代码中存在response.getWriter()写法就能避免该问题。尽可能的采用response.getOutputStream()来输出。 如果应用中包含的有fastJson, siteMesh, urlWriter等插件,都要更改response.getWriter()为response.getOutputStream()