Spring Cloud Zuul 那些你不知道的功能點

本文摘自於 《Spring Cloud微服務 入門 實戰與進階》 一書。 #1. /routes 端點web

當@EnableZuulProxy與Spring Boot Actuator配合使用時,Zuul會暴露一個路由管理端點/routes。spring

藉助這個端點,能夠方便、直觀地查看以及管理Zuul的路由。api

將全部端點都暴露出來,增長下面的配置:bash

management.endpoints.web.exposure.include=*
複製代碼

訪問 http://localhost:2103/actuator/routes 能夠顯示全部路由信息:app

{
  "/cxytiandi/**": "http://cxytiandi.com", 
  "/hystrix-api/**": "hystrix-feign-demo", 
  "/api/**": "forward:/local", 
  "/hystrix-feign-demo/**": "hystrix-feign-demo"
}
複製代碼

2. /filters 端點

/fliters端點會返回Zuul中全部過濾器的信息。能夠清楚的瞭解Zuul中目前有哪些過濾器,哪些被禁用了等詳細信息。框架

訪問 http://localhost:2103/actuator/filters 能夠顯示全部過濾器信息:ide

{
  "error": [
    {
      "class": "com.cxytiandi.zuul_demo.filter.ErrorFilter", 
      "order": 100, 
      "disabled": false, 
      "static": true
    }
  ], 
  "post": [
    {
      "class": "org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter", 
      "order": 1000, 
      "disabled": false, 
      "static": true
    }
  ], 
  "pre": [
    {
      "class": "com.cxytiandi.zuul_demo.filter.IpFilter", 
      "order": 1, 
      "disabled": false, 
      "static": true
    }
  ], 
  "route": [ 
    {
      "class": "org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter", 
      "order": 10, 
      "disabled": false, 
      "static": true
    }
  ]
}

複製代碼

3. 文件上傳

建立一個新的Maven項目zuul-file-demo,編寫一個文件上傳的接口,如代碼清單7-20所示。微服務

代碼清單 7-20 文件上傳接口post

@RestController
public class FileController {

  @PostMapping("/file/upload")
  public String fileUpload(@RequestParam(value = "file") MultipartFile file) throws IOException {
      byte[] bytes = file.getBytes();
      File fileToSave = new File(file.getOriginalFilename());
      FileCopyUtils.copy(bytes, fileToSave);
      return fileToSave.getAbsolutePath();
  }

}
複製代碼

將服務註冊到Eureka中,服務名稱爲zuul-file-demo,經過PostMan來上傳文件,如圖7-4所示學習

圖7-4

能夠看到接口正常返回了文件上傳以後的路徑,接下來咱們換一個大一點的文件,文件大小爲1.7MB。

圖7-5

能夠看到報錯了(如圖7-5所示),經過Zuul上傳文件,若是超過1M須要配置上傳文件的大小, Zuul和上傳的服務都要加上配置:

spring.servlet.multipart.max-file-size=1000Mb
spring.servlet.multipart.max-request-size=1000Mb
複製代碼

配置加完後從新上傳就能夠成功了,如圖7-6所示。

圖7-6

第二種解決辦法是在網關的請求地址前面加上/zuul,就能夠繞過Spring DispatcherServlet進行上傳大文件。

# 正常的地址
http://localhost:2103/zuul-file-demo/file/upload
# 繞過的地址
http://localhost:2103/zuul/zuul-file-demo/file/upload
複製代碼

經過加上/zuul前綴可讓Zuul服務不用配置文件上傳大小,可是接收文件的服務仍是須要配置文件上傳大小,不然文件仍是會上傳失敗。

在上傳大文件的時候,時間比較會比較長,這個時候須要設置合理的超時時間來避免超時。

ribbon.ConnectTimeout=3000
ribbon.ReadTimeout=60000
複製代碼

在Hystrix隔離模式爲線程下zuul.ribbon-isolation-strategy=thread,須要設置Hystrix超時時間。

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000
複製代碼

4. 請求響應信息輸出

系統在生產環境出現問題時,排查問題最好的方式就是查看日誌了,日誌的記錄儘可能詳細,這樣你才能快速定位問題。

下面帶你們學習如何在Zuul中輸出請求響應的信息來輔助咱們解決一些問題。

熟悉Zuul的朋友都知道,Zuul中有4種類型過濾器,每種都有特定的使用場景,要想記錄響應數據,那麼必須是在請求路由到了具體的服務以後,返回了纔有數據,這種需求就適合用post過濾器來實現了。如代碼清單7-21所示。

代碼清單 7-21 Zull獲取請求信息

HttpServletRequest req = (HttpServletRequest)RequestContext.getCurrentContext().getRequest();
System.err.println("REQUEST:: " + req.getScheme() + " " + req.getRemoteAddr() + ":" + req.getRemotePort());
StringBuilder params = new StringBuilder("?");
// 獲取URL參數
Enumeration<String> names = req.getParameterNames();
if( req.getMethod().equals("GET") ) {
   while (names.hasMoreElements()) {
         String name = (String) names.nextElement();
         params.append(name);
         params.append("=");
         params.append(req.getParameter(name));
         params.append("&");
   }
}
        
if (params.length() > 0) {
    params.delete(params.length()-1, params.length());
}
        
System.err.println("REQUEST:: > " + req.getMethod() + " " + req.getRequestURI() + params + " " + req.getProtocol());
Enumeration<String> headers = req.getHeaderNames();
while (headers.hasMoreElements()) {
      String name = (String) headers.nextElement();
      String value = req.getHeader(name);
      System.err.println("REQUEST:: > " + name + ":" + value);
}
        
final RequestContext ctx = RequestContext.getCurrentContext();
// 獲取請求體參數
if (!ctx.isChunkedRequestBody()) {
    ServletInputStream inp = null;
    try {
         inp = ctx.getRequest().getInputStream();
         String body = null;
         if (inp != null) {
            body = IOUtils.toString(inp);
            System.err.println("REQUEST:: > " + body);  
         } catch (IOException e) {
                e.printStackTrace();
         }
    }
}
複製代碼

輸出效果以下:

獲取響應內容第一種方式,如代碼清單7-22所示。

代碼清單 7-22 獲取響應內容(一)

try {
     Object zuulResponse = RequestContext.getCurrentContext().get("zuulResponse");
     if (zuulResponse != null) {
          RibbonHttpResponse resp = (RibbonHttpResponse) zuulResponse;
          String body = IOUtils.toString(resp.getBody());
          System.err.println("RESPONSE:: > " + body);
          resp.close();
          RequestContext.getCurrentContext().setResponseBody(body);
     }
} catch (IOException e) {
     e.printStackTrace();
}
複製代碼

獲取響應內容第二種方式,如代碼清單7-23所示。

代碼清單 7-23 獲取響應內容(二)

InputStream stream = RequestContext.getCurrentContext().getResponseDataStream();
try {
      if (stream != null) {
          String body = IOUtils.toString(stream);
          System.err.println("RESPONSE:: > " + body);
          RequestContext.getCurrentContext().setResponseBody(body);
      }    
} catch (IOException e) {
      e.printStackTrace();
}
複製代碼

爲何上面兩種方式能夠取到響應內容?

在RibbonRoutingFilter或者SimpleHostRoutingFilter中能夠看到下面一段代碼,如代碼清單7-24所示。

代碼清單 7-24 響應內容獲取源碼

public Object run() {
    RequestContext context = RequestContext.getCurrentContext();
    this.helper.addIgnoredHeaders();
    try {
        RibbonCommandContext commandContext = buildCommandContext(context);
        ClientHttpResponse response = forward(commandContext);
        setResponse(response);
        return response;
    }
    catch (ZuulException ex) {
        throw new ZuulRuntimeException(ex);
    }
    catch (Exception ex) {
        throw new ZuulRuntimeException(ex);
    }
}
複製代碼

forward()方法對服務調用,拿到響應結果,經過setResponse()方法進行響應的設置,如代碼清單7-25所示。

代碼清單 7-25 setResponse(一)

protected void setResponse(ClientHttpResponse resp) throws ClientException, IOException {
    RequestContext.getCurrentContext().set("zuulResponse", resp);
    this.helper.setResponse(resp.getStatusCode().value(),
    resp.getBody() == null ? null : resp.getBody(), resp.getHeaders());
}
複製代碼

上面第一行代碼就能夠解釋咱們的第一種獲取的方法,這邊直接把響應內容加到了RequestContext中。

第二種方式的解釋就在helper.setResponse的邏輯裏面了,如代碼清單7-26所示。

代碼清單 7-26 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);
    }

    // .....
}
複製代碼

5. Zuul自帶的Debug功能

Zuul中自帶了一個DebugFilter,一開始我也沒明白這個DebugFilter有什麼用,看名稱很容易理解,用來調試的,但是你看它源碼幾乎沒什麼邏輯,就set了兩個值而已,如代碼清單7-27所示。

代碼清單 7-27 DebugFilter run方法

@Override
public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    ctx.setDebugRouting(true);
    ctx.setDebugRequest(true);
    return null;
}
複製代碼

要想讓這個過濾器執行就得研究下它的shouldFilter()方法,如代碼清單7-28所示。 代碼清單 7-28 DebugFilter shouldFilter 方法

@Override
public boolean shouldFilter() {
    HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
    if ("true".equals(request.getParameter(DEBUG_PARAMETER.get()))) {
      return true;
    }
    return ROUTING_DEBUG.get();
}
複製代碼

只要知足兩個條件中的任何一個就能夠開啓這個過濾器,第一個條件是請求參數中帶了某個參數=true就能夠開啓,這個參數名是經過下面的代碼獲取的,如代碼清單7-29所示。

代碼清單 7-29 DebugFilter啓用參數(一)

private static final DynamicStringProperty DEBUG_PARAMETER = DynamicPropertyFactory
      .getInstance().getStringProperty(ZuulConstants.ZUUL_DEBUG_PARAMETER, "debug");
複製代碼

DynamicStringProperty是Netflix的配置管理框架Archaius提供的API,能夠從配置中心獲取配置,因爲Netflix沒有開源Archaius的服務端,因此這邊用的就是默認值debug,若是你們想動態去獲取這個值的話能夠用攜程開源的Apollo來對接Archaius,這個在後面章節給你們講解。

能夠在請求地址後面追加debug=true來開啓這個過濾器,參數名稱debug也能夠在配置文件中進行覆蓋,用zuul.debug.parameter指定,不然就是從Archaius中獲取,沒有對接Archaius那就是默認值debug。

第二個條件代碼,如代碼清單7-30所示。

代碼清單 7-30 DebugFilter啓用參數(二)

private static final DynamicBooleanProperty ROUTING_DEBUG = DynamicPropertyFactory
      .getInstance().getBooleanProperty(ZuulConstants.ZUUL_DEBUG_REQUEST, false);
  
複製代碼

是經過配置zuul.debug.request來決定的,能夠在配置文件中配置zuul.debug.request=true開啓DebugFilter過濾器。

DebugFilter過濾器開啓後,並沒什麼效果,在run方法中只是設置了DebugRouting和DebugRequest兩個值爲true,因而繼續看源碼,發如今不少地方有這麼一段代碼,好比com.netflix.zuul.FilterProcessor.runFilters(String)中,如代碼清單7-31所示。

代碼清單 7-31 Debug信息添加

if (RequestContext.getCurrentContext().debugRouting()) {
    Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
複製代碼

當debugRouting爲true的時候就會添加一些Debug信息到RequestContext中。如今明白了DebugFilter中爲何要設置DebugRouting和DebugRequest兩個值爲true。

到這步後發現仍是很迷茫,通常咱們調試信息的話確定是用日誌輸出來的,日誌級別就是Debug,但這個Debug信息只是累加起來存儲到RequestContext中,沒有對使用者展現。

繼續看代碼吧,功夫不負有心人,在org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter.addResponseHeaders()這段代碼中看到了但願。如代碼清單7-32所示。

代碼清單 7-32 Debug信息設置響應

private void addResponseHeaders() {
     RequestContext context = RequestContext.getCurrentContext();
     HttpServletResponse servletResponse = context.getResponse();
     if (this.zuulProperties.isIncludeDebugHeader()) {
         @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());
         }
     }
}
複製代碼

核心代碼在於this.zuulProperties.isIncludeDebugHeader(),只有知足這個條件纔會把RequestContext中的調試信息做爲響應頭輸出,在配置文件中增長下面的配置便可:

zuul.include-debug-header=true
複製代碼

最後在請求的響應頭中能夠看到調試內容,如圖7-7所示。

圖7-7

本文摘自於 《Spring Cloud微服務 入門 實戰與進階》 一書。

相關文章
相關標籤/搜索