SpringBoot getWriter() has already been called for this response異常分析

前言

本文章會(huì)對(duì)getWriter() has already been called for this response異常在SpringBoot下出現(xiàn)的情況進(jìn)行分析和解決。
如果你只是想看解決方案,請(qǐng)直接拖到文章最后。

問題分析

在把tomcat應(yīng)用升級(jí)到SpringBoot后,部分http接口出現(xiàn)了getWriter() has already been called for this response異常。但是異常報(bào)出的很模糊,下面貼出異常:

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()已經(jīng)被調(diào)用。 并且能看到的有用信息只是fastJson。(因?yàn)槲业膖omcat應(yīng)用還包含sitemesh,urlwriter等,該異常也會(huì)出現(xiàn)在sitemesh與urlwriter上,并不一定是fastJson)

通過本地調(diào)試,卻并沒有復(fù)現(xiàn)出該問題(奇怪是產(chǎn)線一直出現(xiàn))。起初懷疑是并發(fā)問題,但細(xì)一想應(yīng)該不會(huì)。這個(gè)問題只有在升級(jí)后才出現(xiàn)。

進(jìn)而懷疑是不是因?yàn)镾pringMVC的版本問題(升級(jí)前該應(yīng)用的springMvc版本為2.5.3,升級(jí)后為5.1.8)? 將應(yīng)用去掉SpringBoot, 采用SpringMVC方式 (Spring版本依然為5.1.8),無法重現(xiàn)。

自此,該問題陷入泥潭(因?yàn)楸镜責(zé)o法復(fù)現(xiàn),無法追蹤)

轉(zhuǎn)機(jī)-發(fā)現(xiàn)問題

很幸運(yùn),在對(duì)一個(gè)文件上傳接口進(jìn)行測試時(shí),該問題在本地終于出現(xiàn)。
下面貼出代碼(已刪除不必要的代碼):

@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;;
}

因?yàn)樵谏?jí)后,以前的代碼并沒有過多的更改。 可以發(fā)現(xiàn),這個(gè)接口本來是要返回json的,但因?yàn)闅v史原因,寫法采用的是response.getWriter().write()的方式,并且升級(jí)過程中,沒有完全刪除response.getWriter()。而在升級(jí)后,增加了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()。 而報(bào)錯(cuò)原因就是getWriter()應(yīng)被調(diào)用。我通過去掉文件上傳接口中的response.getWriter()后本地不在出現(xiàn)該異常。

問題再追蹤

對(duì)于fastJson,這個(gè)配置是以前沿用下來的,為什么以前可以,現(xiàn)在不可以了呢?

通過源碼追蹤,我在tomcat-embed-core-9.0.24.jar下的Response類中的getOutputStream()方法中發(fā)現(xiàn)了這么一段代碼:

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;
        }
    }

我能肯定,我的業(yè)務(wù)代碼和fastjson代碼并沒有改變(文件上傳中的response.getWriter()已經(jīng)刪除,只保留fastjson配置中的response.getWriter())。
實(shí)際調(diào)用的response.getOutputSteam,并且注意,代碼中有個(gè)if判斷usingWriter, 而usingWriter被設(shè)置為true的地方只有一個(gè):

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;
        }
    }

可以看到這兩個(gè)方法,互相判斷,應(yīng)該是為了保證調(diào)用的resposne寫方法一致,要不都用getWriter(),要不都用getOutputStream()。而我并沒有使用getOutputStream(),那就只能說SpringBoot中底層代碼用的getOutputStream。 那我理解就是新的Spring已經(jīng)不希望你使用getWriter()了么?

解決方案

總之,無論現(xiàn)在的Spring處于什么原因,造成了這個(gè)問題,只要避免了代碼中存在response.getWriter()寫法就能避免該問題。盡可能的采用response.getOutputStream()來輸出。
如果應(yīng)用中包含的有fastJson, siteMesh, urlWriter等插件,都要更改response.getWriter()response.getOutputStream()

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容