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

前言

本文章会对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()

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容