Spring cloud zuul的SendResponseFilter做了什么

Spring cloud zull 的SendResponseFilter主要工作是将代理请求获取的reponse写入当前response,发送回客户端。以下是源代码:

public class SendResponseFilter extends ZuulFilter {

   private static final Log log = LogFactory.getLog(SendResponseFilter.class);

   private static DynamicBooleanProperty INCLUDE_DEBUG_HEADER = DynamicPropertyFactory
         .getInstance()
         .getBooleanProperty(ZuulConstants.ZUUL_INCLUDE_DEBUG_HEADER, false);

   private static DynamicIntProperty INITIAL_STREAM_BUFFER_SIZE = DynamicPropertyFactory
         .getInstance()
         .getIntProperty(ZuulConstants.ZUUL_INITIAL_STREAM_BUFFER_SIZE, 8192);

   private static DynamicBooleanProperty SET_CONTENT_LENGTH = DynamicPropertyFactory
         .getInstance()
         .getBooleanProperty(ZuulConstants.ZUUL_SET_CONTENT_LENGTH, false);
   private boolean useServlet31 = true;

   public SendResponseFilter() {
      super();
      // To support Servlet API 3.0.1 we need to check if setcontentLengthLong exists
      try {
         HttpServletResponse.class.getMethod("setContentLengthLong");
      } catch(NoSuchMethodException e) {
         useServlet31 = false;
      }
   }

   private ThreadLocal buffers = new ThreadLocal() {
      @Override
      protected byte[] initialValue() {
         return new byte[INITIAL_STREAM_BUFFER_SIZE.get()];
      }
   };
   
   @Override
   public String filterType() {
      return POST_TYPE;
   }

   @Override
   public int filterOrder() {
      return SEND_RESPONSE_FILTER_ORDER;
   }

   @Override
   public boolean shouldFilter() {
      RequestContext context = RequestContext.getCurrentContext();
      return context.getThrowable() == null
            && (!context.getZuulResponseHeaders().isEmpty()
               || context.getResponseDataStream() != null
               || context.getResponseBody() != null);
   }

   @Override
   public Object run() {
      try {
         addResponseHeaders();
         writeResponse();
      }
      catch (Exception ex) {
         ReflectionUtils.rethrowRuntimeException(ex);
      }
      return null;
   }

   private void writeResponse() throws Exception {
      RequestContext context = RequestContext.getCurrentContext();
      // there is no body to send
      if (context.getResponseBody() == null
            && context.getResponseDataStream() == null) {
         return;
      }
      HttpServletResponse servletResponse = context.getResponse();
      if (servletResponse.getCharacterEncoding() == null) { // only set if not set
         servletResponse.setCharacterEncoding("UTF-8");
      }
      OutputStream outStream = servletResponse.getOutputStream();
      InputStream is = null;
      try {
         if (RequestContext.getCurrentContext().getResponseBody() != null) {
            String body = RequestContext.getCurrentContext().getResponseBody();
            writeResponse(
                  new ByteArrayInputStream(
                        body.getBytes(servletResponse.getCharacterEncoding())),
                  outStream);
            return;
         }
         boolean isGzipRequested = false;
         final String requestEncoding = context.getRequest()
               .getHeader(ZuulHeaders.ACCEPT_ENCODING);

         if (requestEncoding != null
               && HTTPRequestUtils.getInstance().isGzipped(requestEncoding)) {
            isGzipRequested = true;
         }
         is = context.getResponseDataStream();
         InputStream inputStream = is;
         if (is != null) {
            if (context.sendZuulResponse()) {
               // if origin response is gzipped, and client has not requested gzip,
               // decompress stream
               // before sending to client
               // else, stream gzip directly to client
               if (context.getResponseGZipped() && !isGzipRequested) {
                  // If origin tell it's GZipped but the content is ZERO bytes,
                  // don't try to uncompress
                  final Long len = context.getOriginContentLength();
                  if (len == null || len > 0) {
                     try {
                        inputStream = new GZIPInputStream(is);
                     }
                     catch (java.util.zip.ZipException ex) {
                        log.debug(
                              "gzip expected but not "
                                    + "received assuming unencoded response "
                                    + RequestContext.getCurrentContext()
                                    .getRequest().getRequestURL()
                                    .toString());
                        inputStream = is;
                     }
                  }
                  else {
                     // Already done : inputStream = is;
                  }
               }
               else if (context.getResponseGZipped() && isGzipRequested) {
                  servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");
               }
               writeResponse(inputStream, outStream);
            }
         }
      }
      finally {
         /**
         * Closing the wrapping InputStream itself has no effect on closing the underlying tcp connection since it's a wrapped stream. I guess for http
         * keep-alive. When closing the wrapping stream it tries to reach the end of the current request, which is impossible for infinite http streams. So
         * instead of closing the InputStream we close the HTTP response.
         *
         * @author Johannes Edmeier
         */
         try {
            Object zuulResponse = RequestContext.getCurrentContext()
                  .get("zuulResponse");
            if (zuulResponse instanceof Closeable) {
               ((Closeable) zuulResponse).close();
            }
            outStream.flush();
            // The container will close the stream for us
         }
         catch (IOException ex) {
         log.warn("Error while sending response to client: " + ex.getMessage());
         }
      }
   }

   private void writeResponse(InputStream zin, OutputStream out) throws Exception {
      byte[] bytes = buffers.get();
      int bytesRead = -1;
      while ((bytesRead = zin.read(bytes)) != -1) {
         out.write(bytes, 0, bytesRead);
      }
   }

   private void addResponseHeaders() {
      RequestContext context = RequestContext.getCurrentContext();
      HttpServletResponse servletResponse = context.getResponse();
      if (INCLUDE_DEBUG_HEADER.get()) {
         @SuppressWarnings("unchecked")
         List rd = (List) context.get(ROUTING_DEBUG_KEY);
         if (rd != null) {
            StringBuilder debugHeader = new StringBuilder();
            for (String it : rd) {
               debugHeader.append("[[[" + it + "]]]");
            }
            servletResponse.addHeader(X_ZUUL_DEBUG_HEADER, debugHeader.toString());
         }
      }
      List> zuulResponseHeaders = context.getZuulResponseHeaders();
      if (zuulResponseHeaders != null) {
         for (Pair it : zuulResponseHeaders) {
            servletResponse.addHeader(it.first(), it.second());
         }
      }
      // Only inserts Content-Length if origin provides it and origin response is not
      // gzipped
      if (SET_CONTENT_LENGTH.get()) {
         Long contentLength = context.getOriginContentLength();
         if ( contentLength != null && !context.getResponseGZipped()) {
            if(useServlet31) {
               servletResponse.setContentLengthLong(contentLength);
            } else {
               //Try and set some kind of content length if we can safely convert the Long to an int
               if (isLongSafe(contentLength)) {
                  servletResponse.setContentLength(contentLength.intValue());
               }
            }
         }
      }
   }

   private boolean isLongSafe(long value) {
      return value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE;
   }

}

filterType:
post

filterOrder:
1000,是post阶段最后执行的过滤器

shouldFilter:

public boolean shouldFilter() {
​      RequestContext context = RequestContext.getCurrentContext();
​      return context.getThrowable() == null
​            && (!context.getZuulResponseHeaders().isEmpty()
​               || context.getResponseDataStream() != null
​               || context.getResponseBody() != null);
   }

该过滤器会检查请求上下文中是否包含请求响应相关的头信息(zuulResponseHeaders)、响应数据流(responseDataStream)或是响应体(responseBody),只有在包含它们其中一个且没有异常抛出的时候才会执行处理逻辑。

run:
主要做了两件事
1、添加响应头
addResponseHeaders();

private void addResponseHeaders() {
​      RequestContext context = RequestContext.getCurrentContext();
​      HttpServletResponse servletResponse = context.getResponse();
​      if (INCLUDE_DEBUG_HEADER.get()) {
​         @SuppressWarnings("unchecked")
​         List rd = (List) context.get(ROUTING_DEBUG_KEY);
​         if (rd != null) {
​            StringBuilder debugHeader = new StringBuilder();
​            for (String it : rd) {
​               debugHeader.append("[[[" + it + "]]]");
​            }
​            servletResponse.addHeader(X_ZUUL_DEBUG_HEADER, debugHeader.toString());
​         }
​      }
​      List> zuulResponseHeaders = context.getZuulResponseHeaders();
​      if (zuulResponseHeaders != null) {
​         for (Pair it : zuulResponseHeaders) {
​            servletResponse.addHeader(it.first(), it.second());
​         }
​      }
​      // Only inserts Content-Length if origin provides it and origin response is not
​      // gzipped
​      if (SET_CONTENT_LENGTH.get()) {
​         Long contentLength = context.getOriginContentLength();
​         if ( contentLength != null && !context.getResponseGZipped()) {
​            if(useServlet31) {
​               servletResponse.setContentLengthLong(contentLength);
​            } else {
​               //Try and set some kind of content length if we can safely convert the Long to an int
​               if (isLongSafe(contentLength)) {
​                  servletResponse.setContentLength(contentLength.intValue());
​               }
​            }
​         }
​      }
   }

ZUUL_INCLUDE_DEBUG_HEADER主要涉及到zuul.include-debug-header的配置,如果为true,且请求中带有debug=true或zuul.debug.request
配置为true,则 debug信息将会添加到X-Zuul-Debug-Header响应header中,你可以将它作为信息返回给网关调用方,也可以通过调用com.netflix.zuul.context.Debug.getRoutingDebug().自己打印出来,方法可参考:
https://stackoverflow.com/questions/43910195/where-can-i-find-the-debug-information-in-zuul

接着是把zuulResponseHeaders放入到当前response中,那zuulResponseHeaders原先是怎么放到context中的呢,以我们使用SimpleHostRoutingFilter为例子:
其内部执行:

CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,
​      headers, params, requestEntity);
setResponse(response);

sendResponse:

private void setResponse(HttpResponse response) throws IOException {
   RequestContext.getCurrentContext().set("zuulResponse", response);
   this.helper.setResponse(response.getStatusLine().getStatusCode(),
​         response.getEntity() == null ? null : response.getEntity().getContent(),
​         revertHeaders(response.getAllHeaders()));
}

this.helper.setResponse:

public void setResponse(int status, InputStream entity,
​      MultiValueMap headers) throws IOException {
   RequestContext context = RequestContext.getCurrentContext();
   context.setResponseStatusCode(status);
   if (entity != null) {
​      context.setResponseDataStream(entity);
   }

   HttpHeaders httpHeaders = new HttpHeaders();
   for (Entry> header : headers.entrySet()) {
​      List values = header.getValue();
​      for (String value : values) {
​         httpHeaders.add(header.getKey(), value);
​      }
   }
   boolean isOriginResponseGzipped = false;
   if (httpHeaders.containsKey(CONTENT_ENCODING)) {
​      List collection = httpHeaders.get(CONTENT_ENCODING);
​      for (String header : collection) {
​         if (HTTPRequestUtils.getInstance().isGzipped(header)) {
​            isOriginResponseGzipped = true;
​            break;
​         }
​      }
   }
   context.setResponseGZipped(isOriginResponseGzipped);

   for (Entry> header : headers.entrySet()) {
​      String name = header.getKey();
​      for (String value : header.getValue()) {
​         context.addOriginResponseHeader(name, value);
​         if (name.equalsIgnoreCase(CONTENT_LENGTH)) {
​            context.setOriginContentLength(value);
​         }
​         if (isIncludedHeader(name)) {
​            context.addZuulResponseHeader(name, value);
​         }
​      }
   }
}

如上述代码所示,根据转发时获取的origin response,将相关信息设置到context中,以备后续使用,这些信息包括:responseStatusCode(响应状态码)、responseDataStream(响应输入流)、responseGZipped(原始响应是否Gzipped)、originResponseHeaders(原始响应头)、originContentLength(原始响应实体长度)、zuulResponseHeaders(从originResponseHeaders中过滤了部分header信息,具体看下面isIncludedHeader方法)

public boolean isIncludedHeader(String headerName) {
   String name = headerName.toLowerCase();
   RequestContext ctx = RequestContext.getCurrentContext();
   if (ctx.containsKey(IGNORED_HEADERS)) {
​      Object object = ctx.get(IGNORED_HEADERS);
​      if (object instanceof Collection && ((Collection) object).contains(name)) {
​         return false;
​      }
   }
   switch (name) {
   case "host":
   case "connection":
   case "content-length":
   case "content-encoding":
   case "server":
   case "transfer-encoding":
   case "x-application-context":
​      return false;
   default:
​      return true;
   }
}

为什么要过滤这些以上这些字段呢,因为这些都是目标主机特定于代理网关的http头,而不是代理网关特定于客户端的http头,像host,serve、connection、content-encoding等,代理网关可能根据客户端请求信息,当前网络状态设置不一样的值。有些像content-length,后面作了特别处理。

最后是设置当前响应的content-length,SET_CONTENT_LENGTH对应的配置项是zuul.set-content-length,如果是true,并且目标主机提供Content-Length而且响应没有被gzipped压缩时,才插入Content-Length。为什么gzipped压缩时不传入呢,通过后面的run方法内容可知,如果原始response是经过gzip压缩,而网关client没有要求gzip压缩,则在发送给客户端之前先解压响应流,因此此时一旦设置了Content-Length, 便会导致实际的传输长度比Content-Length要长的情况,导致截断。

// if origin response is gzipped, and client has not requested gzip,
​               // decompress stream
​               // before sending to client
​               // else, stream gzip directly to client
​               if (context.getResponseGZipped() && !isGzipRequested) {
​                  // If origin tell it's GZipped but the content is ZERO bytes,
​                  // don't try to uncompress
​                  final Long len = context.getOriginContentLength();
​                  if (len == null || len > 0) {
​                     try {
​                        inputStream = new GZIPInputStream(is);
​                     }
​                     catch (java.util.zip.ZipException ex) {
​                        log.debug(
​                              "gzip expected but not "
​                                    + "received assuming unencoded response "
​                                    + RequestContext.getCurrentContext()
​                                    .getRequest().getRequestURL()
​                                    .toString());
​                        inputStream = is;
​                     }
​                  }
​                  else {
​                     // Already done : inputStream = is;
​                  }
​               }
​               else if (context.getResponseGZipped() && isGzipRequested) {
​                  servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");
​               }

可能有些童鞋不大清楚content-length的作用,在此作一个普及:

Content-Length指示出报文中的实体主体的字节大小,它主要为了检测出服务器崩溃导致的报文截尾,并对持久连接的多个报文进行正确分段。 如果主体进行了内容编码,它指示编码后的主体的字节长度。如果不是持久连接,那么不需要知道它正在读取的主体的长度,只需要读到服务器关闭主体连接为止,如果是持久连接,在服务器写主体前,必须知道它的大小并在Content-length中发送,若服务器动态创建内容,则发送前无法知道主体的长度,那怎么办呢?这时可以用transfer-encoding替代,在头部加入 Transfer-Encoding: chunked 之后,就代表这个报文采用了分块编码。这时,报文中的实体需要改为用一系列分块来传输。每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的 CRLF(\r\n),也不包括分块数据结尾的 CRLF。最后一个分块长度值必须为 0,对应的分块数据没有内容,表示实体结束。(详细可参考https://imququ.com/post/transfer-encoding-header-in-http.html)

那假如SET_CONTENT_LENGTH为false,既然SendResponseFilter中没看到设置Content-Length的代码,也没设置 transfer-encoding,那么是在哪里作处理的呢,调试代码可知,是在tomcat中处理的:

zuul 内部可以不对content length或Tansfer-Encoding进行设置,由org.apache.catalina.connector.ResponseFacade进行

调试代码,查看org.apache.catalina.connector.CoyoteOutputStream. close源码

@Override
public void close() throws IOException {
​    ob.close();
}

ob即outputbuffer,close时内部将执行doFlush:

protected void doFlush(boolean realFlush) throws IOException {

    if (suspended) {
        return;
    }
    
    try {
        doFlush = true;
        if (initial) {
            coyoteResponse.sendHeaders();
            initial = false;
        }
        if (cb.remaining() > 0) {
            flushCharBuffer();
        }
        if (bb.remaining() > 0) {
            flushByteBuffer();
        }
    } finally {
        doFlush = false;
    }
    
    if (realFlush) {
        coyoteResponse.action(ActionCode.CLIENT_FLUSH, null);
        // If some exception occurred earlier, or if some IOE occurred
        // here, notify the servlet with an IOE
        if (coyoteResponse.isExceptionPresent()) {
            throw new ClientAbortException(coyoteResponse.getErrorException());
        }
    }

}

Http11Processor的prepareResponse方法:
如果存在contentLength,则设置;如果没有,且响应码支持拥有实体,并且使用的是HTTP 1.1,持久连接(Connection: keep-alive),那么我们将使用Transfer-Encoding:chunk。

@Override
protected final void prepareResponse() throws IOException {

    boolean entityBody = true;
    contentDelimitation = false;
    
    OutputFilter[] outputFilters = outputBuffer.getFilters();
    
    if (http09 == true) {
        // HTTP/0.9
        outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]);
        outputBuffer.commit();
        return;
    }
    
    int statusCode = response.getStatus();
    if (statusCode < 200 || statusCode == 204 || statusCode == 205 ||
            statusCode == 304) {
        // No entity body
        outputBuffer.addActiveFilter
            (outputFilters[Constants.VOID_FILTER]);
        entityBody = false;
        contentDelimitation = true;
        if (statusCode == 205) {
            // RFC 7231 requires the server to explicitly signal an empty
            // response in this case
            response.setContentLength(0);
        } else {
            response.setContentLength(-1);
        }
    }
    
    MessageBytes methodMB = request.method();
    if (methodMB.equals("HEAD")) {
        // No entity body
        outputBuffer.addActiveFilter
            (outputFilters[Constants.VOID_FILTER]);
        contentDelimitation = true;
    }
    
    // Sendfile support
    if (endpoint.getUseSendfile()) {
        prepareSendfile(outputFilters);
    }
    
    // Check for compression
    boolean isCompressible = false;
    boolean useCompression = false;
    if (entityBody && (compressionLevel > 0) && sendfileData == null) {
        isCompressible = isCompressible();
        if (isCompressible) {
            useCompression = useCompression();
        }
        // Change content-length to -1 to force chunking
        if (useCompression) {
            response.setContentLength(-1);
        }
    }
    
    MimeHeaders headers = response.getMimeHeaders();
    // A SC_NO_CONTENT response may include entity headers
    if (entityBody || statusCode == HttpServletResponse.SC_NO_CONTENT) {
        String contentType = response.getContentType();
        if (contentType != null) {
            headers.setValue("Content-Type").setString(contentType);
        }
        String contentLanguage = response.getContentLanguage();
        if (contentLanguage != null) {
            headers.setValue("Content-Language")
                .setString(contentLanguage);
        }
    }
    
    long contentLength = response.getContentLengthLong();
    boolean connectionClosePresent = false;
    if (contentLength != -1) {
        headers.setValue("Content-Length").setLong(contentLength);
        outputBuffer.addActiveFilter
            (outputFilters[Constants.IDENTITY_FILTER]);
        contentDelimitation = true;
    } else {
        // If the response code supports an entity body and we're on
        // HTTP 1.1 then we chunk unless we have a Connection: close header
        connectionClosePresent = isConnectionClose(headers);
        if (entityBody && http11 && !connectionClosePresent) {
            outputBuffer.addActiveFilter
                (outputFilters[Constants.CHUNKED_FILTER]);
            contentDelimitation = true;
            headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED);
        } else {
            outputBuffer.addActiveFilter
                (outputFilters[Constants.IDENTITY_FILTER]);
        }
    }
    
    if (useCompression) {
        outputBuffer.addActiveFilter(outputFilters[Constants.GZIP_FILTER]);
        headers.setValue("Content-Encoding").setString("gzip");
    }
    // If it might be compressed, set the Vary header
    if (isCompressible) {
        // Make Proxies happy via Vary (from mod_deflate)
        MessageBytes vary = headers.getValue("Vary");
        if (vary == null) {
            // Add a new Vary header
            headers.setValue("Vary").setString("Accept-Encoding");
        } else if (vary.equals("*")) {
            // No action required
        } else {
            // Merge into current header
            headers.setValue("Vary").setString(
                    vary.getString() + ",Accept-Encoding");
        }
    }
    
    // Add date header unless application has already set one (e.g. in a
    // Caching Filter)
    if (headers.getValue("Date") == null) {
        headers.addValue("Date").setString(
                FastHttpDateFormat.getCurrentDate());
    }
    
    // FIXME: Add transfer encoding header
    
    if ((entityBody) && (!contentDelimitation)) {
        // Mark as close the connection after the request, and add the
        // connection: close header
        keepAlive = false;
    }
    
    // This may disabled keep-alive to check before working out the
    // Connection header.
    checkExpectationAndResponseStatus();
    
    // If we know that the request is bad this early, add the
    // Connection: close header.
    if (keepAlive && statusDropsConnection(statusCode)) {
        keepAlive = false;
    }
    if (!keepAlive) {
        // Avoid adding the close header twice
        if (!connectionClosePresent) {
            headers.addValue(Constants.CONNECTION).setString(
                    Constants.CLOSE);
        }
    } else if (!http11 && !getErrorState().isError()) {
        headers.addValue(Constants.CONNECTION).setString(Constants.KEEPALIVE);
    }
    
    // Add server header
    if (server == null) {
        if (serverRemoveAppProvidedValues) {
            headers.removeHeader("server");
        }
    } else {
        // server always overrides anything the app might set
        headers.setValue("Server").setString(server);
    }
    
    // Build the response header
    try {
        outputBuffer.sendStatus();
    
        int size = headers.size();
        for (int i = 0; i < size; i++) {
            outputBuffer.sendHeader(headers.getName(i), headers.getValue(i));
        }
        outputBuffer.endHeaders();
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        // If something goes wrong, reset the header buffer so the error
        // response can be written instead.
        outputBuffer.resetHeaderBuffer();
        throw t;
    }
    
    outputBuffer.commit();
}

private static boolean isConnectionClose(MimeHeaders headers) {
​    MessageBytes connection = headers.getValue(Constants.CONNECTION);
​    if (connection == null) {
​        return false;
​    }
​    return connection.equals(Constants.CLOSE);
}

private void prepareSendfile(OutputFilter[] outputFilters) {
​    String fileName = (String) request.getAttribute(
​            org.apache.coyote.Constants.SENDFILE_FILENAME_ATTR);
​    if (fileName == null) {
​        sendfileData = null;
​    } else {
​        // No entity body sent here
​        outputBuffer.addActiveFilter(outputFilters[Constants.VOID_FILTER]);
​        contentDelimitation = true;
​        long pos = ((Long) request.getAttribute(
​                org.apache.coyote.Constants.SENDFILE_FILE_START_ATTR)).longValue();
​        long end = ((Long) request.getAttribute(
​                org.apache.coyote.Constants.SENDFILE_FILE_END_ATTR)).longValue();
​        sendfileData = socketWrapper.createSendfileData(fileName, pos, end - pos);
​    }
}

2、写入响应内容
writeResponse():

 private void writeResponse() throws Exception {
​      RequestContext context = RequestContext.getCurrentContext();
​      // there is no body to send
​      if (context.getResponseBody() == null
​            && context.getResponseDataStream() == null) {
​         return;
​      }
​      HttpServletResponse servletResponse = context.getResponse();
​      if (servletResponse.getCharacterEncoding() == null) { // only set if not set
​         servletResponse.setCharacterEncoding("UTF-8");
​      }
​      OutputStream outStream = servletResponse.getOutputStream();
​      InputStream is = null;
​      try {
​         if (RequestContext.getCurrentContext().getResponseBody() != null) {
​            String body = RequestContext.getCurrentContext().getResponseBody();
​            writeResponse(
​                  new ByteArrayInputStream(
​                        body.getBytes(servletResponse.getCharacterEncoding())),
​                  outStream);
​            return;
​         }
​         boolean isGzipRequested = false;
​         final String requestEncoding = context.getRequest()
​               .getHeader(ZuulHeaders.ACCEPT_ENCODING);

         if (requestEncoding != null
               && HTTPRequestUtils.getInstance().isGzipped(requestEncoding)) {
            isGzipRequested = true;
         }
         is = context.getResponseDataStream();
         InputStream inputStream = is;
         if (is != null) {
            if (context.sendZuulResponse()) {
               // if origin response is gzipped, and client has not requested gzip,
               // decompress stream
               // before sending to client
               // else, stream gzip directly to client
               if (context.getResponseGZipped() && !isGzipRequested) {
                  // If origin tell it's GZipped but the content is ZERO bytes,
                  // don't try to uncompress
                  final Long len = context.getOriginContentLength();
                  if (len == null || len > 0) {
                     try {
                        inputStream = new GZIPInputStream(is);
                     }
                     catch (java.util.zip.ZipException ex) {
                        log.debug(
                              "gzip expected but not "
                                    + "received assuming unencoded response "
                                    + RequestContext.getCurrentContext()
                                    .getRequest().getRequestURL()
                                    .toString());
                        inputStream = is;
                     }
                  }
                  else {
                     // Already done : inputStream = is;
                  }
               }
               else if (context.getResponseGZipped() && isGzipRequested) {
                  servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");
               }
               writeResponse(inputStream, outStream);
            }
         }
      }
      finally {
         /**
         * Closing the wrapping InputStream itself has no effect on closing the underlying tcp connection since it's a wrapped stream. I guess for http
         * keep-alive. When closing the wrapping stream it tries to reach the end of the current request, which is impossible for infinite http streams. So
         * instead of closing the InputStream we close the HTTP response.
         *
         * @author Johannes Edmeier
         */
         try {
            Object zuulResponse = RequestContext.getCurrentContext()
                  .get("zuulResponse");
            if (zuulResponse instanceof Closeable) {
               ((Closeable) zuulResponse).close();
            }
            outStream.flush();
            // The container will close the stream for us
         }
         catch (IOException ex) {
         log.warn("Error while sending response to client: " + ex.getMessage());
         }
      }
   }

   private void writeResponse(InputStream zin, OutputStream out) throws Exception {
​      byte[] bytes = buffers.get();
​      int bytesRead = -1;
​      while ((bytesRead = zin.read(bytes)) != -1) {
​         out.write(bytes, 0, bytesRead);
​      }
   }

由SimpleHostRoutingFilter可知,原始reponse获取的时候已将reponseBody或responseDataStream放入context中,它先判断是否存在responseBody,存在即写入输出流,直接返回,否则将responseDataStream写入,这里responseDataStream可能是一个压缩流,如果原始response是经过gzip压缩,而网关client没有要求gzip压缩,则在发送给客户端之前先解压响应流,否则就直接输出,并设置Content-Encoding:gzip头。

为了释放系统资源,前面通过route过滤器获取的reponse,即zuulResponse需要关闭,关闭其被包装InputStream也许只是为了保持http长连接,本身对底层tcp连接的关闭不起作用。因此,我们关闭HTTP响应而不是关闭InputStream。

先写到这里,有什么需要补充的请留言。

java达人

                                                   ID:drjava

                                             (长按或扫码识别)
image

你可能感兴趣的:(Spring cloud zuul的SendResponseFilter做了什么)