Netflix-Zuul網關說明文檔

Netflix-Zuul

一. 簡介

路由是微服務架構中的一個組成部分。
例如【/】可能是你映射到web應用的路徑,【/api/user】可能是你映射到user服務的路徑,【/api/shop】可能是
映射到shop服務的路徑。
Zuul是一款出自於Netflix基於JVM的服務器端的負載均衡器。

Netflix在用Zuul做這些事情:

  • 身份驗證(Authentication)
  • Insights
  • 壓力測試(Stress Testing)
  • 金絲雀測試(Canary Testing)
  • 動態路由(Dynamic Routing)
  • 服務遷移(Service Migration)
  • 減載(Load Shedding)
  • 安全(Security)
  • 靜態響應處理(Static Response handling)
  • 流量控制(Active/Active traffic management)

二. 集成Zuul反向代理

  • Spring Cloud 已經建立了Zuul代理,用來適用前端應用程序想要通過代理來訪問一個或者多個後端服務。
    這個特點對於用戶接口代理訪問到後端服務非常有用。避免了跨域問題並且可以使認證獨立於後端服務。

  • 如果要啓用Zuul反向代理,可以在Spring Boot應用程序的主類中添加註解@EnableZuulProxy。 這樣做
    可以使本地的調用請求路由到相應的服務。按照慣例,一個users服務從代理那裏接收/users(使用前綴剝離服務)
    的請求Zuul代理使用Ribbon通過服務發現來定位具體的服務。所有的請求在hystrix command中執行,所以失敗
    的請求可以在Hystrix監控中獲取到。一旦斷路器打開,Zuul代理將不會嘗試連接該服務。

    注:Zuul starter 沒有包含服務發現客戶端,因此,爲了實現基於服務id的路由,最好提供服務發現的客戶端
    (Eureka是其中之一的選擇)

  • 如果服務要跳過Zuul的反向代理,可在屬性【zuul.ignored-services】中設置跳過的服務ID列表。
    如果某個服務和需要跳過的服務列表匹配,但是又明確的設置在了路由map中,該服務將不會跳過Zuul的代理。
    下述例子就是這樣的效果:
    application.yml

      zuul:
        ignoredServices: '*'
        routes:
          users: /myusers/**
    

    上述例子中,所有的服務都將跳過Zuul的代理,除了users服務。

  • 如果要添加或者改變代理路由,可以將配置信息配置成如下所示:
    application.yml

      zuul:
        routes:
          users: /myusers/**
    

    上述配置意味着如果HTTP請求/myusers將會轉發到users服務(例:/myusers/101將會轉發到users服務的/101)

  • 如果想要通過路由獲取到更好的粒度控制,則可以獨立的指定path和serviceId,配置如下:
    application.yml

     zuul:
      routes:
        users:
          path: /myusers/**
          serviceId: users_service
    

    上述配置意味着如果HTTP請求/myusers將會轉發到serviceId爲users_service的服務中去。
    此場景中必須指定ant-style風格的格式。所以/myusers/*只匹配一級路徑,/myusers/**可匹配多級路徑。

  • 後臺服務的位置可以通過serviceId(通過服務發現獲取)指定,也可以通過url指定,配置如下:
    application.yml

      zuul:
        routes:
          users:
            path: /myusers/**
            url: http://example.com/users_service
    
  • 上述所有的這些路由例子都不會以HystrixCommand的方式執行,有多個url的時候也不會使用Ribbon做負載均衡。
    如果要實現這些目標,可以指定一個靜態服務的list,配置如下:
    application.yml

      zuul:
        routes:
          echo:
            path: /myusers/**
            serviceId: myusers-service
            stripPrefix: true
      
      hystrix:
        command:
          myusers-service:
            execution:
              isolation:
                thread:
                  timeoutInMilliseconds: ...
      
      myusers-service:
        ribbon:
          NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
          listOfServers: http://example1.com,http://example2.com
          ConnectTimeout: 1000
          ReadTimeout: 3000
          MaxTotalHttpConnections: 500
          MaxConnectionsPerHost: 100
    

    另一種方式是指定一個服務路由並且爲serviceId配置一個Ribbon客戶端(這樣做需要在Ribbon中禁止Eureka的支持),配置如下:
    application.yml

      zuul:
        routes:
          users:
            path: /myusers/**
            serviceId: users
      
      ribbon:
        eureka:
          enabled: false
      
      users:
        ribbon:
          listOfServers: example.com,google.com
    
  • 也可以使用正則表達式在serviceId和如路由之間指定一個規則。從serviceId中提取變量並且將它們注入到路由中,進而使用有規律的表達式來定義groups。例子如下:
    ApplicationConfiguration.java

      @Bean
      public PatternServiceRouteMapper serviceRouteMapper() {
          return new PatternServiceRouteMapper(
              "(?<name>^.+)-(?<version>v.+$)",
              "${version}/${name}");
      }
    

    上述例子意味着serviceId爲myusers-v1的服務將指向地址/v1/myusers/**.任何正則表達式都可被接受,但是任何被命名的groups必須存在於servicePattern和routePattern中。如果servicePattern不能匹配serviceId,將使用默認的操作。上述例子中,myusers的serviceId映射到了/myusers/**的路由中(沒有檢測到版本號)。這個功能默認是禁用的,只有在服務被發現時纔有這個功能。

  • 增加一個映射的前綴,可以設置zuul.prefix屬性值,例如/api。默認情況下,Zuul在請求轉發前會將這個前綴丟棄(可以通過設置zuul.stripPrefix=false來關掉這個功能)。也可以在個別的路由中特定服務中關掉丟棄映射前綴的功能。例如:
    application.yml

     zuul:
      routes:
        users:
          path: /myusers/**
          stripPrefix: false
    

    注:zuul.stripPrefix只有在設置了zuul.prefix時纔會起作用。它不會對路由中設置的path產生任何影響。
    在上述例子中,請求/myusers/101將轉發到users服務的/myusers/101路徑中。

  • 事實上,zuul.routes中的屬性會綁定到ZuulProperties的對象中。如果你去看那個對象的屬性,裏面有一個retryable的標誌。如果設置那個標誌爲true,則Ribbon的客戶端會自動重試失敗的請求。當你需要修改Ribbon客戶端的重試參數配置時你也可以將這個標記設置爲true。
    默認情況下,X-Forward-Host頭部屬性會加到轉發的請求中。如果要關閉這個屬性,可設置zuul.addProxyHeaders=false.
    默認情況下,前綴路徑在轉發時將丟棄,並且後臺請求中會增加X-Forward-Prefix頭部屬性(如上述例子中的/myusers)。

  • 如果設置默認的路由路徑(/),被@EnableZuulProxy註解的應用程序將類似一個獨立的服務器。例如,zuul.route.home:/將會路由home服務的/**路徑。

  • 如果需要將忽略路徑控制到更好的顆粒度,可以指定一些特別的pattern。這些pattern將會在處理路由位置開始時處理,這意味着前綴應該包含在pattenr中以保證匹配。忽略的模式跨越所有服務並取代任何其他路由規範。下面的例子將展示如何創建一個忽略pattern:
    application.yml

     zuul:
      ignoredPatterns: /**/admin/**
      routes:
        users: /myusers/**
    

    上面的例子意味着請求如/myusers/101將轉發到users服務的/101路徑中,然而如果請求路徑中包含/admin/將不會被轉發。

  • 如果你需要保證路由的順序,你需要使用YAML文件。使用properties文件將丟失順序訪問的功能。下面的例子就是這樣一個YAML文件:
    application.yml

       zuul:
        routes:
          users:
            path: /myusers/**
          legacy:
            path: /**  
    

    如果使用properties文件,legacy的路徑有可能變成優先於users的路徑處理,最後導致users服務不可達。

三. Zuul Http Client

Zuul默認使用的HTTP client是 Apache HTTP client而不是丟棄的Ribbon的RestClient。如果要使用RestClient或者okhttp3.OkHttpClient,分別設置 ribbon.restclient.enabled=true 或者 ribbon.okhttp.enabled=true即可。如果你想自定義Apache HTTP client或者OK HTTP client,則需要提供ClosableHttpClient或者OkHttpClient的bean類型。

四. Cookies和敏感頭部信息

  • 在同一個系統的不同服務之間你可以共享頭部信息,但是可能你不想將某些敏感頭部信息泄露給下游的外部服務器。這種情況下可以指定忽略的頭部信息作爲路由配置的一部分。Cookies扮演了一個特殊的角色,因爲它們能在瀏覽器中很好的定義語義,並且經常當做處理過的敏感信息使用。如果你的代理的消費者是瀏覽器,對於用戶來說給下游服務使用的Cookies可能是個問題,因爲它們都被搞得亂七八糟(所有的下游服務認爲Cookies都是來自於一個地方)。

  • 如果你充分的設計了你的服務(比如,只有一個下游服務設置了Cookies),你也許可以使他們從後端服務一直流轉到調用者。當然,如果你的代理設置了Cookies並且你所有的後端服務都是同一個系統的一部分,可以很自然和簡單的共享他們(例如使用Spring Session讓他們維持在共享的狀態)。除此之外, 對於調用者來說,下游服務隊任何cookie的get和set操作似乎都沒有多大用處,所以推薦至少將Set-Cookie和Cookie放入屬於域的一部分的路由的敏感頭部中。即使該路由屬於域的一部分,在讓cookie流轉與服務和代理之前,請仔細考慮那將意味着什麼。

    每個路由的敏感頭部可以通過逗號隔開字符串來設置,下面的例子就是這樣一個YAML文件:
    application.yml

       zuul:
        routes:
          users:
            path: /myusers/**
            sensitiveHeaders: Cookie,Set-Cookie,Authorization
            url: https://downstream
    

    這個是sensitiveHeaders的默認值,你可以不需要設置它除非你想要一個和默認值不一樣的值。
    這個在Spring Cloud Netflix 1.1中修改的。(在1.0中,用戶無法控制頭部,並且所有的cookies都流向兩個方向。).

    sensitiveHeaders是黑名單,並且默認不爲空。因此,如果要讓Zuul發送所有的頭部(除了忽略掉的),你必須指定設置一個空的列表。如果你想傳遞cookie或者authorization頭部給你的後臺服務這麼做是必要的。下面的例子展示瞭如何使用sensitiveHeaders:
    application.yml

      zuul:
        routes:
          users:
            path: /myusers/**
            sensitiveHeaders:
            url: https://downstream
    

    你也可以通過zuul.sensitiveHeaders來設置sensitive headers。如果sensitiveHeaders設置在一個路由中,那麼它將會覆蓋全局的sensitiveHeaders設置。

五. 忽略頭部

在附加在路由頭部的敏感頭部信息中,你可以設置一個全局屬性叫zuul.ignoredHeaders,在和下游服務交互的時候爲request和response丟棄敏感頭部信息。默認情況下,如果spring Security 不在classpath中,這個屬性值是空的。否則,會被初始化成大家都知道被spring security指定的安全頭部。在某種情況下,假設下游服務也可以添加這些敏感頭部,但是我們又需要從代理中獲取這些敏感頭部;當spring security在classpath中的情況下,爲了不丟棄這些安全頭部信息,可以設置zuul.ignoredHeadersfalse。如果您在Spring security 中禁用了HTTP安全響應頭,並且希望下游服務提供值,那麼這樣做很有用。

六. 管理節點

默認情況下,如果你使用了@EnableZuulProxy註解,你就啓用了以下兩個附加的endpoints:

  • Routes
  • Filters

1. 路由節點(Routes Endpoint)

  • 使用/routes(GET)會返回被映射的路由列表:
    GET /routes
{
  /stores/**: "http://localhost:8081"
}
  • 其他的詳細路由信息可以通過在GET /routes後增加?fromat=details參數來獲取,可以獲取到如下的詳細信息:
    GET /routes/details
{
  "/stores/**": {
    "id": "stores",
    "fullPath": "/stores/**",
    "location": "http://localhost:8081",
    "path": "/**",
    "prefix": "/stores",
    "retryable": false,
    "customSensitiveHeaders": false,
    "prefixStripped": true
  }
}
  • 使用POST方式提交/routes會強制刷新存在的路由(比如說,當路由被改變的時候)。你可以使用以下配置禁用這項功能。
endpoints.routes.enabled=false

2. 過濾節點 (Filter Endpoint)

  • 用GET方式使用路徑/filters請求filters的endpoint將會按類型一直返回帶有詳細信息的Zuul filters map, 在map中你可以按類型獲取每個類型的filters列表。

七. 窒息模式和局部跳轉(Strangulation patterns and Local forwards)

  • 當你需要遷移一個已經存在的應用或者API到原有的節點的時候,一般的處理方式是慢慢的用一種不同的實現方式慢慢替換它們。
    這種場景下,Zuul代理是一個很有用的工具,因爲你可以使用它將客戶端的一部分請求轉發到原有的節點,而另外一部分請求轉發到新的節點上。
    下面的例子展示了這種“窒息”模式的詳細配置:
    application.yml
      zuul:
      routes:
          first:
            path: /first/**
            url: http://first.example.com
          second:
            path: /second/**
            url: forward:/second
          third:
            path: /third/**
            url: forward:/3rd
          legacy:
            path: /**
            url: http://legacy.example.com
    

上面的例子中,我們“窒息”了【legacy】應用,它映射了所有不滿足的其他匹配規則的請求。

  • 路徑/first/**被提取到了一個外部url的新的服務中。

  • 路徑/second/**被轉發到了本地服務中(比如,一個普通的Spring @RequestMapping)。

  • 路徑/third/**也被轉發到了一個有不同前綴的服務中 (例:/third/foo跳轉到了/3rd/foo)。

    PS: 上述例子中被忽略的路徑不是真的被忽略了,只是沒有通過Zuul轉發而已。(所以它們仍能跳轉到本地服務中)

八. 通過Zuul上傳文件

  • 如果你使用了@EnableZuulProxy註解,你也可以使用代理路徑來上傳文件,只要文件足夠的小。
    大文件的話這裏有一個可替代的路徑可以繞過Spring 的 DispatcherServlet(避免multipart 處理)就是"/zuul/*"。也就是說,如果你配置了路由zuul.routes.customers=/customers/**, 你就可以提交大文件給/zuul/customers/*。servlet路徑是取道zuul.servletPath通過的。如果代理如有通過一個Ribbon負載均衡器,非常大的文件必須提高timeout的設置,例子如下:
    application.yml
    hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
    ribbon:
      ConnectTimeout: 3000
      ReadTimeout: 60000
    
  • 注意,流式處理大文件,在請求中你需要塊編碼(chunked encoding)(有些瀏覽器不是默認這麼處理的),例子如下:
    $ curl -v -H "Transfer-Encoding: chunked" \
      -F "[email protected]" localhost:9999/zuul/simple/file
    

九. 查詢字符串編碼

  • 當處理正在進來的請求時,查詢參數(query params)將被解碼,所以在Zuul filters中如果有需要才被修改。然後它們將被重新編碼,在路由過濾器(route filters)中重建後端請求。如果是用Javascript的encodeURIComponent方法來編碼,結果可能和原始的輸入有所不同。在大多數情況下,這沒有問題,不過有些web服務器對編碼比較複雜的查詢參數比較挑剔。

  • 強制執行查詢字符串的原始編碼,可以往ZuulProperties中設置一個特殊的標記,這樣查詢字符串就可以與HttpServletRequest::getQueryString方法一起使用。 例子如下:
    application.yml

    zuul:
    forceOriginalQueryStringEncoding: true
    

    PS:這個特殊標記只在SimpleHostRoutingFilter中有效果。因此你也丟失了使用RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters)輕鬆重寫查詢參數的能力,因爲查詢字符串現在直接從原始的HttpServletRequest中獲取。

十. 請求URI編碼(Request URI Encoding)

  • 當處理正在進來的請求時,請求URI將在匹配路由前進行解碼。此後將在路由過濾器(route filters)中重建後端請求的時候被重新編碼。如果你的URI中包含"/"這將出現一些不可預知的行爲。

  • 如果要使用原始的請求URI(request URI),可以往ZuulProperties中設置一個特殊的標記, 這樣查詢字符串就可以與HttpServletRequest::getRequstURI方法一起使用。 例子如下:
    application.yml

    zuul:
      decodeUrl: false
    

    PS:如果你使用RequestContext屬性覆蓋了請求URI,並且這個標記被設置成了false,請求內容中的url將不會被編碼。確認URL已經被編碼了將會是你的責任。

十一.平滑嵌入Zuul

  • 如果你使用@EnableZuulServer(而不是@EnableZuulProxy),你仍然能運行Zuul server,而無需代理或選擇性地打開代理平臺的某些部分。應用中的任何Zuul filter類型的將被自動裝載(與@EnableZuulProxy相同),但沒有任何代理過濾器將被自動裝載。

  • 在這個例子中,裝入Zuul server中歐冠的路由仍然使用"zuul.routes.*"配置指定,但是已經沒有服務發現和代理。因此,“serviceId”和“url”配置將被忽略,以下示例將“/api/**”中的所有路徑映射到Zuul的過濾器鏈路中:
    application.yml

     zuul:
      routes:
        api: /api/**
    

十二. 禁用Zuul Filters

  • Spring cloud版本的Zuul在proxy和server模式中自動啓用了一些ZuulFilter。可以查看Zuul filters包從而得知你可以啓用哪些filter。如果你想禁用某個Zuul filter,設置zuul.<SimpleClassName>.<filterType>.disable=true即可。

十三. 爲路由提供Hystrix Fallbacks

  • 在Zuul的路由中服務鏈路被中斷,你可以創建一個Fallbackprovider類型的bean來提供一個備用的響應(fallback response)。在這個bean中,你需要指定回退路由的id,並且提供一個ClientHttpResponse作爲回退返回。下面的例子是一個比較簡單的FallbackProvider的實現:

    class MyFallbackProvider implements FallbackProvider {
    
      @Override
      public String getRoute() {
          return "customers";
      }
    
      @Override
      public ClientHttpResponse fallbackResponse(String route, final Throwable cause) {
          if (cause instanceof HystrixTimeoutException) {
              return response(HttpStatus.GATEWAY_TIMEOUT);
          } else {
              return response(HttpStatus.INTERNAL_SERVER_ERROR);
          }
      }
    
      private ClientHttpResponse response(final HttpStatus status) {
          return new ClientHttpResponse() {
              @Override
              public HttpStatus getStatusCode() throws IOException {
                  return status;
              }
    
              @Override
              public int getRawStatusCode() throws IOException {
                  return status.value();
              }
    
              @Override
              public String getStatusText() throws IOException {
                  return status.getReasonPhrase();
              }
    
              @Override
              public void close() {
              }
    
              @Override
              public InputStream getBody() throws IOException {
                  return new ByteArrayInputStream("fallback".getBytes());
              }
    
              @Override
              public HttpHeaders getHeaders() {
                  HttpHeaders headers = new HttpHeaders();
                  headers.setContentType(MediaType.APPLICATION_JSON);
                  return headers;
              }
          };
      }
    }
    
  • 下面的例子將展示怎樣的路由配置可以和上述例子匹配:

    zuul:
    routes:
      customers: /customers/**
    
  • 如果你想爲所有的路由提供一個默認的fallback,你可以創建一個FallbackProvider類型的bean,並且在getRoute方法中返回*或者null, 如下所示:

    class MyFallbackProvider implements FallbackProvider {
      @Override
      public String getRoute() {
          return "*";
      }
    
      @Override
      public ClientHttpResponse fallbackResponse(String route, Throwable throwable) {
          return new ClientHttpResponse() {
              @Override
              public HttpStatus getStatusCode() throws IOException {
                  return HttpStatus.OK;
              }
    
              @Override
              public int getRawStatusCode() throws IOException {
                  return 200;
              }
    
              @Override
              public String getStatusText() throws IOException {
                  return "OK";
              }
    
              @Override
              public void close() {
    
              }
    
              @Override
              public InputStream getBody() throws IOException {
                  return new ByteArrayInputStream("fallback".getBytes());
              }
    
              @Override
              public HttpHeaders getHeaders() {
                  HttpHeaders headers = new HttpHeaders();
                  headers.setContentType(MediaType.APPLICATION_JSON);
                  return headers;
              }
          };
      }
    }
    

十四. Zuul 超時

如果你想配置通過Zuul的socket和代理請求的超時時間,你有兩個選項可供配置:

  • 如果Zuul使用了服務發現,你需要配置Ribbon的ribbon.ReadTimeoutribbon.SocketTimeout屬性。
  • 如果你配置使用Zuul的方式是指定url的方式 ,你需要配置zuul.host.connect-timeout-millisuul.host.socket-timeout-millis

十五. 重寫Location頭部

如果Zuul處理的是一個web應用程序,當web應用程序通過http狀態碼3XX跳轉時,你可能需要重寫Location頭部信息。否則,瀏覽器將重定向到Zuul的Url而不是web應用程序的url了。你可以設置一個LocationRewriteFilter爲Zuul的url重寫Location頭部。當然,它還添加了剝離的全局前綴和路由特定前綴。下面的例子展示了使用一個Spring Configuration文件來增加一個filter:

import org.springframework.cloud.netflix.zuul.filters.post.LocationRewriteFilter;
...

@Configuration
@EnableZuulProxy
public class ZuulConfig {
    @Bean
    public LocationRewriteFilter locationRewriteFilter() {
        return new LocationRewriteFilter();
    }
}

PS:請謹慎使用該filter,此filger作用在所有請求返回狀態碼爲3XX的Location頭部中,也許適用於所有的場景,比如重定向到外部的url中。

十六. 啓用跨域請求

默認情況下,Zuul路由所有的跨域請求(CORS)到下游服務中。如果你想要Zuul處理特定的跨域請求,你可以使用自定義的WebMvcConfigurerbean:

@Bean
public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/path-1/**")
                 .allowedOrigins("http://allowed-origin.com")
                 .allowedMethods("GET", "POST");
       }
    };
}
  • 在上面的例子中,我們允許http://allowed-origin.com的請求使用GET或者POST方式發送到以/path-1/開頭的服務節點中。你可以將你的CORS配置應用到具體的path表達式中,或者是爲整個應用程序準備的全局配置,使用/**表達式。
    你可以自定義的屬性有:
    • allowedOrigins
    • allowedMethods
    • allowedHeaders
    • exposedHeaders
    • allowCredentials
    • maxAge

十七. 指標監控

Zuul在路由請求時,將會爲任何在請求時可能發生的失敗提供監控指標。這些指標可以通過請求/actuator/metrics來獲取。這些指標將會有名字和這樣的格式:ZUUL::EXCEPTION:errorCause:statusCode.

十八. Zuul 開發者攻略

請查看Zuul Wiki

1. Zuul Servlet

Zuul被當做一個servlet來實現。在一般場景下,Zuul被嵌入到Spring的Dispatch機制。這讓Spring MVC在路由的控制之下,在這個場景中,Zuul緩衝所有請求。如果需要Zuul不緩衝請求(比如,大文件上傳),Servlet也可安裝在Spring Dispather之外。默認情況下,selvet的地址是/zuul,這個路徑可以使用zuul.servlet-path屬性來修改。

2. Zuul RequestContext

Zuul使用RequestContext在過濾器間傳遞數據。數據保存在爲每個request指定的ThreadLocal中。
有關路由請求的位置信息,errors和實際的HttpServletRequest和HttpServletResponse都保存在那裏。RequestContext繼承自ConCurrentHashMap,所以任何東西都可以保存在那裏。FilterContants包含了被安裝在SpringCloud Netflix中的所有過濾器的使用的keys(以後再談這些)。

3. @EnableZuulProxy vs. @EnableZuulServer

Spring Cloud Netflix 安裝了一些過濾器,依賴於啓用Zuul的註解。@EnableZuulProxy是@EnableZuulServer的超集。也就是說,@EnableZuulProxy包含了所有被@EnableZuulServer安裝的過濾器。在“proxy”的額外過濾器開啓了路由功能。如果你想要一個“空白”的Zuul,你應該使用@EnableZuulServer。

4. @EnableZuulServer Filters

@EnableZuulServer 創建了一個從Spring Boot配置文件中加載路由定義的SimpleRouteLocator。

下面的過濾器被裝載了(作爲普通的Spring Beans):

  • 前置過濾器(Pre filters):

    • ServletDetectionFilter: 可檢測任何通過Spring Dispatcher的請求。併爲其設置一個key爲FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY的boolean值。
    • FormBodyWrapperFilter: 爲下游請求解析和重編碼表單數據。
    • DebugFilter: 如果設置了調試請求的參數,設置equestContext.setDebugRouting()和 RequestContext.setDebugRequest()值爲true。
  • 路由過濾器(Route filters):

    • SendForwardFilter: 使用Servlet RequestDispatcher轉發請求。轉發位置將保存在RequestContext的屬性中,key爲FilterConstants.FORWARD_TO_KEY。這對於當前應用轉發的終端很有用。
  • 後置過濾器(Post filters):

    • SendResponseFilter: 從代理請求中寫入響應內容到當前的響應中。
  • Error filters:

    • SendErrorFilter: 如果RequestContext.getThrowable()不爲null,默認跳轉到/error路由中,可以通過修改error.path屬性來修改默認跳轉的路徑。

5. @EnabelZuulProxy Filters

創建一個DiscoveryClientRouteLocator用來從DiscoveryClient(比如說Eureka)以及properties文件中加載路由定義,這是一個爲從DiscoveryClient的每個ServiceId創建的路由。如果新的服務加入進來,路由將會被刷新。

此外,爲了讓過濾器被更早的發現,裝載了以下的過濾器(作爲普通的Spring Beans):

  • 前置過濾器(Pre filters):

    • PreDecorationFilter:決定路由到哪裏和怎麼路由,依賴於被支持的RouteLocator。它當然也爲下游的請求設置了各種各樣的proxy-related headers。
  • 路由過濾器(Route filters):

    • RibbonRoutingFilter: 使用 Ribbon, Hystrix, 和 pluggable HTTP 客戶端發送請求,Service IDs將會放入到RequestContext屬性中,key爲FilterConstants.SERVICE_ID_KEY。此過濾器可使用不同的HTTP客戶端:
      • Apache HttpClient: 默認的客戶端。
      • Squareup OkHttpClient v3: 在com.squareup.okhttp3:okhttp library包在classpath的情況下,通過設置ribbon.okhttp.enabled=true啓用。
      • Netflix Ribbon HTTP client: 通過設置ribbon.restclient.enabled=true來啓用,此client有使用限制,包括不支持PATCH HTTP method, 但是仍然有內置的重試機制。
    • SimpleHostRoutingFilter: 通過Apache HttpClient發送到預定的URLs,URLs可以在RequestContext.getRouteHost()中找到。

6. 自定義Zuul filter 實例

大部分的怎麼編寫過濾器的實例在Sample Zuul Filters工程中可以找到。
在這個工程中當然也可以找到操縱請求或者響應內容的實例。

這部分包含以下實例:

  • 怎麼編寫 Pre Filter:
    前置過濾器把數據裝載在RequestContext中供下游的過濾器使用。主要的使用場景是爲路由過濾器設置必須的信息。下面是一個Zuul前置過濾器的例子:

    public class QueryParamPreFilter extends ZuulFilter {
      @Override
      public int filterOrder() {
      	return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
      }
    
      @Override
      public String filterType() {
      	return PRE_TYPE;
      }
    
      @Override
      public boolean shouldFilter() {
      	RequestContext ctx = RequestContext.getCurrentContext();
      	return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded
      			&& !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId
      }
      @Override
      public Object run() {
          RequestContext ctx = RequestContext.getCurrentContext();
      	HttpServletRequest request = ctx.getRequest();
      	if (request.getParameter("sample") != null) {
      	    // put the serviceId in `RequestContext`
      		ctx.put(SERVICE_ID_KEY, request.getParameter("foo"));
      	}
          return null;
      }
    }
    

    上述過濾器從request參數中填充SERVICE_ID_KEY。在實際應用中,你不應該做這種直接的映射,Service ID應該從sample中查找。
    現在SERVICE_ID_KEY被修改了,PreDecorationFilter將不會執行,RibbonRoutingFilter將會被執行。
    PS:如果你想路由到全路徑URL,可以調用ctx.setRouteHost(url)。想要修改路由過濾器的跳轉,可以設置REQUEST_URI_KEY的值。

  • 怎麼編寫 Route Filter:
    路由過濾器在前置過濾器執行之後執行,並且將請求轉發到相應的服務中。這裏的大部分工作是將請求和響應數據裝換爲客戶端要求的模型。下面是一個Zuul路由過濾器的例子:

    public class OkHttpRoutingFilter extends ZuulFilter {
      @Autowired
      private ProxyRequestHelper helper;
    
      @Override
      public String filterType() {
      	return ROUTE_TYPE;
      }
    
      @Override
      public int filterOrder() {
      	return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1;
      }
    
      @Override
      public boolean shouldFilter() {
      	return RequestContext.getCurrentContext().getRouteHost() != null
      			&& RequestContext.getCurrentContext().sendZuulResponse();
      }
    
      @Override
      public Object run() {
      	OkHttpClient httpClient = new OkHttpClient.Builder()
      			// customize
      			.build();
    
      	RequestContext context = RequestContext.getCurrentContext();
      	HttpServletRequest request = context.getRequest();
    
      	String method = request.getMethod();
    
      	String uri = this.helper.buildZuulRequestURI(request);
    
      	Headers.Builder headers = new Headers.Builder();
      	Enumeration<String> headerNames = request.getHeaderNames();
      	while (headerNames.hasMoreElements()) {
      		String name = headerNames.nextElement();
      		Enumeration<String> values = request.getHeaders(name);
    
      		while (values.hasMoreElements()) {
      			String value = values.nextElement();
      			headers.add(name, value);
      		}
      	}
    
      	InputStream inputStream = request.getInputStream();
    
      	RequestBody requestBody = null;
      	if (inputStream != null && HttpMethod.permitsRequestBody(method)) {
      		MediaType mediaType = null;
      		if (headers.get("Content-Type") != null) {
      			mediaType = MediaType.parse(headers.get("Content-Type"));
      		}
      		requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream));
      	}
    
      	Request.Builder builder = new Request.Builder()
      			.headers(headers.build())
      			.url(uri)
      			.method(method, requestBody);
    
      	Response response = httpClient.newCall(builder.build()).execute();
    
      	LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();
    
      	for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {
      		responseHeaders.put(entry.getKey(), entry.getValue());
      	}
    
      	this.helper.setResponse(response.code(), response.body().byteStream(),
      			responseHeaders);
      	context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running
      	return null;
      }
    }
    

    上述過濾器將Servlet請求信息轉換成OkHttp3的請求信息,執行HTTP請求,並且將OKHttp3的響應信息轉換成Servlet的響應信息。

  • 怎麼編寫 Post Filter:
    後置過濾器一般操作請求的響應信息。下面的例子在請求頭部信息中增加了一個UUID作爲X-Sample頭部值:

    public class AddResponseHeaderFilter extends ZuulFilter {
      @Override
      public String filterType() {
      	return POST_TYPE;
      }
    
      @Override
      public int filterOrder() {
      	return SEND_RESPONSE_FILTER_ORDER - 1;
      }
    
      @Override
      public boolean shouldFilter() {
      	return true;
      }
    
      @Override
      public Object run() {
      	RequestContext context = RequestContext.getCurrentContext();
      	HttpServletResponse servletResponse = context.getResponse();
      	servletResponse.addHeader("X-Sample", UUID.randomUUID().toString());
      	return null;
      }
    }
    

    PS:其他的操作,比如轉換和加工請求響應內容,要複雜的多,計算量也大得多。

7. Zuul的Error是怎麼工作的

如果一個異常在Zuul過濾器的任何生命週期內被拋出,error過濾器將被執行。SendErrorFilter只有在RequestContext.getThrowable()不會null的時候纔會被執行,這時將會在設置指定的javax.servlet.error.*屬性到請求中,並且將請求轉發到SpringBoot的error page中。

8. Zuul應用程序上下文加載

Zuul內部使用Ribbon調用遠程的URLs。默認情況下,SpringCloud第一次調用的Ribbon客戶端的時候使用懶加載的方式。可以通過設置以下的配置來修改這種行爲,配置之後的結果就是在程序啓動時就會加載相關的Ribbon上下文:

zuul:
  ribbon:
    eager-load:
      enabled: true

參考文檔:

  1. 官方文檔:
    https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-netflix.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章