美文网首页
Spring cloud zuul的SendResponseFi

Spring cloud zuul的SendResponseFi

作者: 九九派 | 来源:发表于2018-12-31 12:06 被阅读43次

    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<byte[]> buffers = new ThreadLocal<byte[]>() {
          @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<String> rd = (List<String>) 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<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
          if (zuulResponseHeaders != null) {
             for (Pair<String, String> 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<String> rd = (List<String>) 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<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
    ​      if (zuulResponseHeaders != null) {
    ​         for (Pair<String, String> 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<String, String> headers) throws IOException {
       RequestContext context = RequestContext.getCurrentContext();
       context.setResponseStatusCode(status);
       if (entity != null) {
    ​      context.setResponseDataStream(entity);
       }
    
       HttpHeaders httpHeaders = new HttpHeaders();
       for (Entry<String, List<String>> header : headers.entrySet()) {
    ​      List<String> values = header.getValue();
    ​      for (String value : values) {
    ​         httpHeaders.add(header.getKey(), value);
    ​      }
       }
       boolean isOriginResponseGzipped = false;
       if (httpHeaders.containsKey(CONTENT_ENCODING)) {
    ​      List<String> collection = httpHeaders.get(CONTENT_ENCODING);
    ​      for (String header : collection) {
    ​         if (HTTPRequestUtils.getInstance().isGzipped(header)) {
    ​            isOriginResponseGzipped = true;
    ​            break;
    ​         }
    ​      }
       }
       context.setResponseGZipped(isOriginResponseGzipped);
    
       for (Entry<String, List<String>> 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的SendResponseFi

          本文链接:https://www.haomeiwen.com/subject/qhowkqtx.html