先说解决方案:使用 getOutputStream() 替换 getWriter() 来获取输出流。


错误描述

在 Spring Boot 应用中使用 HttpServletResponse 输出自定义内容时报错,错误如下:

Caused by: java.lang.IllegalStateException: getWriter() has already been called for this response
    at org.apache.catalina.connector.Response.getOutputStream(Response.java:547) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
    at org.apache.catalina.connector.ResponseFacade.getOutputStream(ResponseFacade.java:210) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
    at javax.servlet.ServletResponseWrapper.getOutputStream(ServletResponseWrapper.java:105) ~[tomcat-embed-core-9.0.29.jar:9.0.29]
    at org.springframework.http.server.ServletServerHttpResponse.getBody(ServletServerHttpResponse.java:84) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:255) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:104) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:295) ~[spring-webmvc-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:226) ~[spring-webmvc-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:82) ~[spring-web-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:124) ~[spring-webmvc-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:888) ~[spring-webmvc-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793) ~[spring-webmvc-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.2.2.RELEASE.jar:5.2.2.RELEASE]
    ... 25 common frames omitted

我的代码:

@Override
    public void handle(HttpServletRequest httpServletRequest,
                       HttpServletResponse httpServletResponse,
                       AccessDeniedException e) {
        JsonResult<String> result = JsonResult.error(e.getMessage());
        httpServletResponse.setHeader("Content-type", "application/json;charset=UTF-8");
        httpServletResponse.setCharacterEncoding("UTF-8");
        try (PrintWriter printWriter = httpServletResponse.getWriter()) {
            printWriter.write(JSONUtil.toJsonStr(result));
        } catch (IOException ex) {
            log.error("error:" + ex.getMessage());
        }
    }

错误分析

通过输出的错误中发现,异常来自 tomcat-embed-core-9.0.29.jar 下的 org.apache.catalina.connector.Response.getOutputStream() 方法,我们直接查看 org.apache.catalina.connector.Response.getOutputStream() 的源码:

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

 }

同时也发现了 org.apache.catalina.connector.Response.getWriter() 方法的定义:

@Override
 public PrintWriter getWriter()
     throws IOException {

     if (usingOutputStream) {
         throw new IllegalStateException
             (sm.getString("coyoteResponse.getWriter.ise"));
     }

     if (ENFORCE_ENCODING_IN_GET_WRITER) {
         /*
             * If the response's character encoding has not been specified as
             * described in <code>getCharacterEncoding</code> (i.e., the method
             * just returns the default value <code>ISO-8859-1</code>),
             * <code>getWriter</code> updates it to <code>ISO-8859-1</code>
             * (with the effect that a subsequent call to getContentType() will
             * include a charset=ISO-8859-1 component which will also be
             * reflected in the Content-Type response header, thereby satisfying
             * the Servlet spec requirement that containers must communicate the
             * character encoding used for the servlet response's writer to the
             * client).
             */
            setCharacterEncoding(getCharacterEncoding());
        }

        usingWriter = true;
        outputBuffer.checkConverter();
        if (writer == null) {
            writer = new CoyoteWriter(outputBuffer);
        }
        return writer;
    }

通过源码中分析,在 getOutputStream() 执行过程,会将 usingOutputStream 设为 true,并判断 usingWriter 是否为 true,是则抛异常。同理,在 getWriter() 执行过程,会将 usingWriter 设为 true,并判断 usingOutputStream 是否为 true,是则抛异常。这种写法无疑是表明要么使用 getOutputStream() ,要么使用 getWriter(),两者只能使用一个,不能同时使用。

结合我遇到的错误来看,我使用了 getWriter() 来输出内容,而 Spring Boot 自带的 Tomcat 默认使用了 getOutputStream() 来处理,使用 response 的输出流不一致导致抛异常。

如此看来,我们只要统一使用 getOutputStream() 来输出就可避免此问题。

修改代码:

    @Override
    public void handle(HttpServletRequest httpServletRequest,
                       HttpServletResponse httpServletResponse,
                       AccessDeniedException e) {
        JsonResult<String> result = JsonResult.error(e.getMessage());
        httpServletResponse.setHeader("Content-type", "application/json;charset=UTF-8");
        httpServletResponse.setCharacterEncoding("UTF-8");
        try (ServletOutputStream outputStream = httpServletResponse.getOutputStream()) {
            outputStream.print(JSONUtil.toJsonStr(result));
        } catch (IOException ex) {
            log.error("error:" + ex.getMessage());
        }
    }

重新启动项目,测试后发现已没有报错。至此,问题解决。

解决方案

使用 getOutputStream() 替换 getWriter() 来获取输出流。

参考文档

  1. SpringBoot getWriter() has already been called for this response异常分析
文章目录