路由器和過濾器:Zuul【譯】

原文鏈接:http://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.0.1.RELEASE/single/spring-cloud-netflix.html#_router_and_filter_zuul

8. 路由器和過濾器:Zuul

路由是微服務(wù)架構(gòu)不可或缺的一部分。 例如,/可以映射到您的Web應(yīng)用程序,/api /users映射到用戶服務(wù),/api/shop映射到商店服務(wù)。 Zuul是Netflix的基于JVM的路由器和服務(wù)器端負(fù)載均衡器。

Netflix使用Zuul進(jìn)行以下操作:

  • 認(rèn)證
  • Insights
  • 壓力測(cè)試
  • Canary測(cè)試
  • 動(dòng)態(tài)路由
  • 服務(wù)遷移
  • 負(fù)載脫落
  • 安全
  • 靜態(tài)響應(yīng)處理
  • 主動(dòng)/主動(dòng)流量管理

Zuul的規(guī)則引擎允許規(guī)則和過濾器基本上以任何JVM語言編寫,內(nèi)置支持Java和Groovy。

配置屬性zuul.max.host.connections已被兩個(gè)新屬性zuul.host.maxTotalConnectionszuul.host.maxPerRouteConnections取代,它們分別默認(rèn)為200和20。

所有路由的默認(rèn)Hystrix隔離模式(ExecutionIsolationStrategy)都是SEMAPHORE。 如果首選隔離模式,則可以將zuul.ribbonIsolationStrategy更改為THREAD

8.1 如何引入Zuul

要在項(xiàng)目中包含Zuul,請(qǐng)使用組ID為org.springframework.cloud的啟動(dòng)器和spring-cloud-starter-netflix-zuul的工件ID。 有關(guān)使用當(dāng)前Spring Cloud Release Train設(shè)置構(gòu)建系統(tǒng)的詳細(xì)信息,請(qǐng)參閱Spring Cloud Project頁面

8.2 嵌入式Zuul反向代理

Spring Cloud創(chuàng)建了一個(gè)嵌入式Zuul代理,以簡(jiǎn)化UI應(yīng)用程序想要對(duì)一個(gè)或多個(gè)后端服務(wù)進(jìn)行代理調(diào)用的常見用例的開發(fā)。 此功能對(duì)于用戶界面代理其所需的后端服務(wù)非常有用,從而無需為所有后端獨(dú)立管理CORS和身份驗(yàn)證問題。

要啟用它,請(qǐng)使用@EnableZuulProxy注釋Spring Boot主類。 這樣做會(huì)導(dǎo)致本地呼叫轉(zhuǎn)發(fā)到適當(dāng)?shù)姆?wù)。 按照慣例,具有用戶ID的服務(wù)從位于/users的代理接收請(qǐng)求(帶有前綴剝離)。 代理使用功能區(qū)來定位要通過發(fā)現(xiàn)轉(zhuǎn)發(fā)的實(shí)例。 所有請(qǐng)求都在hystrix命令中執(zhí)行,因此Hystrix指標(biāo)中會(huì)出現(xiàn)故障。 電路打開后,代理不會(huì)嘗試聯(lián)系該服務(wù)。

Zuul啟動(dòng)器不包含發(fā)現(xiàn)客戶端,因此,對(duì)于基于服務(wù)ID的路由,您還需要在類路徑中提供其中一個(gè)(Eureka是一種選擇)。

要跳過自動(dòng)添加的服務(wù),請(qǐng)將zuul.ignored-services設(shè)置為服務(wù)ID模式列表。 如果服務(wù)被忽略但仍包含在顯式配置的路由映射中的模式匹配,則它就不會(huì)被忽略的,如以下示例所示:

application.yml.

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

在前面的示例中,users外,將忽略所有服務(wù)。

要擴(kuò)充或更改代理路由,可以添加外部配置,如下所示:

application.yml.

zuul:
  routes:
    users: /myusers/**

前面的示例意味著對(duì)/myusers的HTTP調(diào)用被轉(zhuǎn)發(fā)到用戶服務(wù)(例如/myusers/101被轉(zhuǎn)發(fā)到/101)。

要對(duì)路由進(jìn)行更細(xì)粒度的控制,可以單獨(dú)指定路徑和serviceId,如下所示:

application.yml.

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

前面的示例意味著對(duì)/myusers的HTTP調(diào)用將轉(zhuǎn)發(fā)到users_service服務(wù)。 路徑必須具有可以指定為ant樣式模式的路徑,因此/myusers/*僅匹配一個(gè)級(jí)別,但/myusers/**是分層匹配的。

后端的位置可以指定為serviceId(用于發(fā)現(xiàn)服務(wù))或url(用于物理位置),如以下示例所示:

application.yml.

zuul:
  routes:
    users:
      path: /myusers/**
      url: http://example.com/users_service

這些簡(jiǎn)單的url-routes不會(huì)作為HystrixCommand執(zhí)行,也不會(huì)使用Ribbon對(duì)多個(gè)URL進(jìn)行負(fù)載均衡。 要實(shí)現(xiàn)這些目標(biāo),您可以使用靜態(tài)服務(wù)器列表指定serviceId,如下所示:

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

另一種方法是指定服務(wù)路由并為serviceId配置Ribbon客戶端(這樣做需要在Ribbon中禁用Eureka支持 - 請(qǐng)參閱上面的更多信息),如以下示例所示:

application.yml.

zuul:
  routes:
    users:
      path: /myusers/**
      serviceId: users
 
ribbon:
  eureka:
    enabled: false
 
users:
  ribbon:
    listOfServers: example.com,google.com

您可以使用regexmapperserviceId和路由之間提供約定。 它使用正則表達(dá)式命名組從serviceId中提取變量并將它們注入路由模式,如以下示例所示:

ApplicationConfiguration.java.

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

上面的示例表示myusers-v1serviceId映射到路由/v1/myusers/**。 接受任何正則表達(dá)式,但所有命名組必須同時(shí)出現(xiàn)在servicePatternroutePattern中。 如果servicePatternserviceId不匹配,則使用默認(rèn)行為。 在前面的示例中,myusersserviceId映射到/myusers/**路由(未檢測(cè)到版本)。 默認(rèn)情況下禁用此功能,僅適用于已發(fā)現(xiàn)的服務(wù)。

要為所有映射添加前綴,請(qǐng)將zuul.prefix設(shè)置為值,例如/api。 默認(rèn)情況下,在轉(zhuǎn)發(fā)請(qǐng)求之前,會(huì)從請(qǐng)求中刪除代理前綴(您可以使用zuul.stripPrefix = false關(guān)閉此行為)。 您還可以關(guān)閉從各個(gè)路由中剝離特定于服務(wù)的前綴,如以下示例所示:

application.yml.

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

zuul.stripPrefix僅適用于zuul.prefix中設(shè)置的前綴。 它對(duì)給定路由的path中定義的前綴沒有任何影響。

在前面的示例中,對(duì)/myusers/101的請(qǐng)求將轉(zhuǎn)發(fā)到用戶服務(wù)上的/myusers/101。

zuul.routes條目實(shí)際上綁定到ZuulProperties類型的對(duì)象。 如果查看該對(duì)象的屬性,可以看到它還具有retryable的標(biāo)志。 將該標(biāo)志設(shè)置為true以使Ribbon客戶端自動(dòng)重試失敗的請(qǐng)求。 當(dāng)您需要修改使用功能區(qū)客戶端配置的重試操作的參數(shù)時(shí),也可以將該標(biāo)志設(shè)置為true。

默認(rèn)情況下,X-Forwarded-Host標(biāo)頭會(huì)添加到轉(zhuǎn)發(fā)的請(qǐng)求中。 要將其關(guān)閉,請(qǐng)?jiān)O(shè)置zuul.addProxyHeaders = false。 默認(rèn)情況下,前綴路徑被剝離,對(duì)后端的請(qǐng)求選擇X-Forwarded-Prefix標(biāo)頭(前面顯示的示例中為/myusers)。

如果設(shè)置默認(rèn)路由(/),則具有@EnableZuulProxy的應(yīng)用程序可以充當(dāng)獨(dú)立服務(wù)器。 例如,zuul.route.home:/會(huì)將所有流量(/ **)路由到home服務(wù)。

如果需要更細(xì)粒度的忽略,則可以指定要忽略的特定模式。 這些模式在路徑定位過程開始時(shí)進(jìn)行評(píng)估,這意味著前綴應(yīng)包含在模式中以保證匹配。 忽略的模式跨越所有服務(wù)并取代任何其他路由規(guī)范。 以下示例顯示如何創(chuàng)建忽略的模式:

application.yml.

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

上述示例表示所有呼叫(例如/myusers/101)都轉(zhuǎn)發(fā)到用戶服務(wù)上的/101。 但是,包括/admin/的呼叫無法解決。

如果您需要保留其訂單的路由,則需要使用YAML文件,因?yàn)槭褂脤傩晕募r(shí)排序會(huì)丟失。 以下示例顯示了這樣的YAML文件:

application.yml.

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

如果您要使用屬性文件,則legacy路徑可能最終位于users路徑前面,從而導(dǎo)致users路徑無法訪問。

8.3 Zuul Http客戶端

Zuul使用的默認(rèn)HTTP客戶端現(xiàn)在由Apache HTTP Client支持,而不是不推薦使用的Ribbon RestClient。 要使用RestClientokhttp3.OkHttpClient,請(qǐng)分別設(shè)置ribbon.restclient.enabled = trueribbon.okhttp.enabled = true。 如果要自定義Apache HTTP客戶端或OK HTTP客戶端,請(qǐng)?zhí)峁?code>ClosableHttpClient或OkHttpClient類型的bean。

8.4 Cookie和敏感標(biāo)題

您可以在同一系統(tǒng)中的服務(wù)之間共享標(biāo)頭,但您可能不希望敏感標(biāo)頭向下游泄漏到外部服務(wù)器。 您可以在路由配置中指定忽略的標(biāo)頭列表。 Cookie起著特殊的作用,因?yàn)樗鼈冊(cè)跒g覽器中具有良好定義的語義,并且它們始終被視為敏感。 如果您的代理的消費(fèi)者是瀏覽器,那么下游服務(wù)的cookie也會(huì)給用戶帶來問題,因?yàn)樗鼈兌蓟祀s起來(所有下游服務(wù)看起來都來自同一個(gè)地方)。

如果您對(duì)服務(wù)的設(shè)計(jì)非常小心(例如,如果只有一個(gè)下游服務(wù)設(shè)置了cookie),您可以讓它們從后端一直流到調(diào)用者。 此外,如果您的代理設(shè)置了cookie并且所有后端服務(wù)都是同一系統(tǒng)的一部分,那么簡(jiǎn)單地共享它們就很自然(例如,使用Spring Session將它們鏈接到某個(gè)共享狀態(tài))。 除此之外,由下游服務(wù)設(shè)置的任何cookie都可能對(duì)調(diào)用者沒用,因此建議您(至少)將Set-CookieCookie設(shè)置為不屬于您的域的路由的敏感標(biāo)頭。 即使是屬于您域名的路由,也要在讓cookie和代理之間流動(dòng)之前仔細(xì)考慮它的含義。

可以將敏感標(biāo)頭配置為每個(gè)路由的逗號(hào)分隔列表,如以下示例所示:

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

這是sensitiveHeaders的默認(rèn)值,因此除非您希望它不同,否則無需進(jìn)行設(shè)置。 這是Spring Cloud Netflix 1.1中的新功能(在1.0中,用戶無法控制標(biāo)題,并且所有Cookie都在兩個(gè)方向上流動(dòng))。

sensitiveHeaders是黑名單,默認(rèn)不為空。 因此,要使Zuul發(fā)送所有標(biāo)頭(ignore的標(biāo)頭除外),您必須將其明確設(shè)置為空列表。 如果要將cookie或授權(quán)標(biāo)頭傳遞到后端,則必須這樣做。 以下示例顯示了如何使用sensitiveHeaders

application.yml.

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

您還可以通過設(shè)置zuul.sensitiveHeaders來設(shè)置敏感標(biāo)頭。 如果在路由上設(shè)置了sensitiveHeaders,它將覆蓋全局sensitiveHeaders設(shè)置。

8.5 忽略標(biāo)題

除路由敏感標(biāo)頭外,您還可以為與下游服務(wù)交互期間應(yīng)丟棄的值(請(qǐng)求和響應(yīng))設(shè)置名為zuul.ignoredHeaders的全局值。 默認(rèn)情況下,如果Spring Security不在類路徑中,則它們?yōu)榭铡?否則,它們被初始化為一組眾所周知的“安全”頭文件(例如,涉及緩存),如Spring Security所指定的那樣。 在這種情況下的假設(shè)是下游服務(wù)也可能添加這些頭,但我們想要代理的值。 要在Spring Security位于類路徑時(shí)不丟棄這些眾所周知的安全標(biāo)頭,可以將zuul.ignoreSecurityHeaders設(shè)置為false。 如果您在Spring Security中禁用了HTTP安全響應(yīng)標(biāo)頭并希望下游服務(wù)提供的值,那么這樣做會(huì)非常有用。

8.6 管理端點(diǎn)

默認(rèn)情況下,如果將@EnableZuulProxy與Spring Boot Actuator一起使用,則可以啟用另外兩個(gè)端點(diǎn):

  • 路由
  • 過濾

8.6.1 路由結(jié)點(diǎn)

/routes的路由端點(diǎn)的GET返回映射路由的列表:

GET /routes.

{
  /stores/**: "http://localhost:8081"
}

可以通過將?format = details查詢字符串添加到/routes來請(qǐng)求其他路由詳細(xì)信息。 這樣做會(huì)產(chǎn)生以下輸出:

GET /routes/details.

{
  "/stores/**": {
    "id": "stores",
    "fullPath": "/stores/**",
    "location": "http://localhost:8081",
    "path": "/**",
    "prefix": "/stores",
    "retryable": false,
    "customSensitiveHeaders": false,
    "prefixStripped": true
  }
}

對(duì)/routesPOST強(qiáng)制刷新現(xiàn)有路由(例如,當(dāng)服務(wù)目錄中有更改時(shí))。 您可以通過將endpoints.routes.enabled設(shè)置為false來禁用此端點(diǎn)。

路由應(yīng)自動(dòng)響應(yīng)服務(wù)目錄中的更改,但POST/routes是一種強(qiáng)制更改立即發(fā)生的方法。

8.6.2 過濾結(jié)點(diǎn)

對(duì)/filters處的過濾器端點(diǎn)的GET按類型返回Zuul過濾器的映射。 對(duì)于映射中的每種過濾器類型,您將獲得該類型的所有過濾器及其詳細(xì)信息的列表。

8.7 扼殺模式和本地前鋒

遷移現(xiàn)有應(yīng)用程序或API時(shí)的一種常見模式是“扼殺”舊端點(diǎn),慢慢用不同的實(shí)現(xiàn)替換它們。 Zuul代理是一個(gè)有用的工具,因?yàn)槟梢允褂盟鼇硖幚韥碜耘f端點(diǎn)的客戶端的所有流量,但將一些請(qǐng)求重定向到新的端點(diǎn)。

以下示例顯示“扼殺”方案的配置詳細(xì)信息:

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

在前面的示例中,我們扼殺了“遺留”應(yīng)用程序,該應(yīng)用程序映射到與其他模式之一不匹配的所有請(qǐng)求。 /first/**中的路徑已被提取到具有外部URL的新服務(wù)中。 轉(zhuǎn)發(fā)/second/**中的路徑,以便可以在本地處理它們(例如,使用正常的Spring @RequestMapping)。 /third/**中的路徑也被轉(zhuǎn)發(fā)但具有不同的前綴(/third/foo被轉(zhuǎn)發(fā)到/3rd/foo)。

忽略的模式不會(huì)被完全忽略,它們只是不由代理處理(因此它們也可以在本地有效轉(zhuǎn)發(fā))。

8.8 通過Zuul上傳文件

如果您使用@EnableZuulProxy,您可以使用代理路徑上傳文件,只要文件很小,它就可以工作。 對(duì)于大型文件,有一個(gè)替代路徑繞過/zuul/*中的Spring DispatcherServlet(以避免多部分處理)。 換句話說,如果你有zuul.routes.customers = /customers/**,那么你可以將大文件POST/zuul/customers/*。 servlet路徑通過zuul.servletPath外部化。 如果代理路由引導(dǎo)您完成功能區(qū)負(fù)載平衡器,則極大文件也需要提升超時(shí)設(shè)置,如以下示例所示:

application.yml.

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000

請(qǐng)注意,要使用大型文件進(jìn)行流式處理,您需要在請(qǐng)求中使用分塊編碼(默認(rèn)情況下某些瀏覽器不會(huì)這樣做),如以下示例所示:

$ curl -v -H "Transfer-Encoding: chunked" \
    -F "file=@mylarge.iso" localhost:9999/zuul/simple/file

8.9 查詢字符串編碼

處理傳入請(qǐng)求時(shí),將對(duì)查詢參數(shù)進(jìn)行解碼,以便它們可用于Zuul過濾器中的可能修改。 然后對(duì)它們進(jìn)行重新編碼,在路由過濾器中重建后端請(qǐng)求。 如果(例如)它是使用Javascript的encodeURIComponent()方法編碼的,則結(jié)果可能與原始輸入不同。 雖然這在大多數(shù)情況下不會(huì)引起任何問題,但某些Web服務(wù)器可能會(huì)因復(fù)雜查詢字符串的編碼而變得挑剔。

要強(qiáng)制查詢字符串的原始編碼,可以將特殊標(biāo)志傳遞給ZuulProperties,以便使用HttpServletRequest::getQueryString方法將查詢字符串視為原樣,如以下示例所示:

application.yml.

zuul:
  forceOriginalQueryStringEncoding: true

此特殊標(biāo)志僅適用于SimpleHostRoutingFilter。 此外,您無法使RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters)輕松覆蓋查詢參數(shù),因?yàn)椴樵冏址F(xiàn)在直接在原始HttpServletRequest上獲取。

8.10 普通嵌入式Zuul

如果使用@EnableZuulServer(而不是@EnableZuulProxy),您還可以運(yùn)行Zuul服務(wù)器,而無需代理或有選擇地切換代理平臺(tái)的某些部分。 您添加到ZuulFilter類型的應(yīng)用程序的任何bean都會(huì)自動(dòng)安裝(與@EnableZuulProxy一樣),但不會(huì)自動(dòng)添加任何代理過濾器。

application.yml.

zuul:
  routes:
    api: /api/**

8.11 禁用Zuul過濾器

Zuul for Spring Cloud在代理和服務(wù)器模式下都默認(rèn)啟用了許多ZuulFilter bean。 有關(guān)可以啟用的過濾器列表,請(qǐng)參閱Zuul過濾器包。 如果要禁用一個(gè),請(qǐng)?jiān)O(shè)置zuul.<SimpleClassName>.<filterType>.disable = true。 按照慣例,filters后的包是Zuul過濾器類型。 例如,要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter,請(qǐng)?jiān)O(shè)置zuul.SendResponseFilter.post.disable = true。

8.12 為路由提供Hystrix后備

當(dāng)Zuul中給定路徑的電路跳閘時(shí),您可以通過創(chuàng)建FallbackProvider類型的bean來提供回退響應(yīng)。 在此bean中,您需要指定回退所針對(duì)的路由ID,并提供ClientHttpResponse作為回退返回。 以下示例顯示了一個(gè)相對(duì)簡(jiǎn)單的FallbackProvider實(shí)現(xiàn):

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

以下示例顯示了上一個(gè)示例的路由配置可能如何顯示:

zuul:
  routes:
    customers: /customers/**

如果要為所有路由提供默認(rèn)回退,可以創(chuàng)建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;
            }
        };
    }
}

8.13 Zuul超時(shí)

如果要為通過Zuul代理的請(qǐng)求配置套接字超時(shí)和讀取超時(shí),則有兩種選擇,具體取決于您的配置:

  • 如果Zuul使用服務(wù)發(fā)現(xiàn),則需要使用ribbon.ReadTimeoutribbon.SocketTimeout功能區(qū)屬性配置這些超時(shí)。

如果通過指定URL配置了Zuul路由,則需要使用zuul.host.connect-timeout-milliszuul.host.socket-timeout-millis。

8.14 重寫Location標(biāo)頭

如果Zuul面向Web應(yīng)用程序,則當(dāng)Web應(yīng)用程序通過HTTP狀態(tài)代碼3XX重定向時(shí),您可能需要重新寫入Location標(biāo)頭。 否則,瀏覽器會(huì)重定向到Web應(yīng)用程序的URL而不是Zuul URL。 您可以配置LocationRewriteFilter Zuul過濾器以將Location標(biāo)頭重新寫入Zuul的URL。 它還會(huì)添加剝離的全局和路由特定前綴。 以下示例使用Spring配置文件添加過濾器:

import org.springframework.cloud.netflix.zuul.filters.post.LocationRewriteFilter;
...
 
@Configuration
@EnableZuulProxy
public class ZuulConfig {
    @Bean
    public LocationRewriteFilter locationRewriteFilter() {
        return new LocationRewriteFilter();
    }
}

小心
仔細(xì)使用此過濾器。 過濾器作用于所有3XX響應(yīng)代碼的Location標(biāo)頭,這可能不適用于所有情況,例如將用戶重定向到外部URL時(shí)。

8.15 Metrics

對(duì)于路由請(qǐng)求時(shí)可能發(fā)生的任何故障,Zuul將在Actuator指標(biāo)端點(diǎn)下提供指標(biāo)。 可以通過點(diǎn)擊/actuator/metrics來查看這些指標(biāo)。 度量標(biāo)準(zhǔn)的名稱格式為ZUUL::EXCEPTIONerrorCause:statusCode。

8.16 Zuul開發(fā)人員指南

有關(guān)Zuul如何工作的一般概述,請(qǐng)參閱Zuul Wiki。

8.16.1 Zuul Servlet

Zuul是作為Servlet實(shí)現(xiàn)的。 對(duì)于一般情況,Zuul嵌入到Spring Dispatch機(jī)制中。 這讓Spring MVC可以控制路由。 在這種情況下,Zuul緩沖請(qǐng)求。 如果需要在沒有緩沖請(qǐng)求的情況下通過Zuul(例如,對(duì)于大型文件上載),Servlet也會(huì)安裝在Spring Dispatcher之外。 默認(rèn)情況下,servlet的地址為/zuul。 可以使用zuul.servlet-path屬性更改此路徑。

8.16.2 Zuul RequestContext

為了在過濾器之間傳遞信息,Zuul使用RequestContext。 它的數(shù)據(jù)保存在特定于每個(gè)請(qǐng)求的ThreadLocal中。 有關(guān)在哪里路由請(qǐng)求,錯(cuò)誤以及實(shí)際的HttpServletRequestHttpServletResponse的信息都存儲(chǔ)在那里。 RequestContext擴(kuò)展了ConcurrentHashMap,因此任何東西都可以存儲(chǔ)在上下文中。 FilterConstants包含Spring Cloud Netflix安裝的過濾器使用的密鑰(稍后將詳細(xì)介紹)。

8.16.3 @EnableZuulProxy vs. @EnableZuulServer

Spring Cloud Netflix安裝了許多過濾器,具體取決于使用哪個(gè)注釋來啟用Zuul。 @EnableZuulProxy@EnableZuulServer的超集。 換句話說,@EnableZuulProxy包含@EnableZuulServer安裝的所有過濾器。 “代理”中的其他過濾器啟用路由功能。 如果你想要一個(gè)“空白”Zuul,你應(yīng)該使用@EnableZuulServer。

8.16.4 @EnableZuulServer過濾

@EnableZuulServer創(chuàng)建一個(gè)SimpleRouteLocator,用于從Spring Boot配置文件加載路由定義。

安裝了以下過濾器(與普通的Spring Bean一樣):

  • 預(yù)過濾器:
    • ServletDetectionFilter: 檢測(cè)請(qǐng)求是否通過Spring Dispatcher。 使用FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY的鍵設(shè)置布爾值。
    • FormBodyWrapperFilter: 解析表單數(shù)據(jù)并為下游請(qǐng)求重新編碼。
    • DebugFilter: 如果設(shè)置了debug 請(qǐng)求參數(shù),則將RequestContext.setDebugRouting()RequestContext.setDebugRequest()設(shè)置為true
  • 路由過濾器:
    • SendForwardFilter: 使用Servlet RequestDispatcher轉(zhuǎn)發(fā)請(qǐng)求。 轉(zhuǎn)發(fā)位置存儲(chǔ)在RequestContext屬性FilterConstants.FORWARD_TO_KEY中。 這對(duì)于轉(zhuǎn)發(fā)到當(dāng)前應(yīng)用程序中的端點(diǎn)非常有用。
  • 后置過濾器:
    • SendResponseFilter: 將代理請(qǐng)求的響應(yīng)寫入當(dāng)前響應(yīng)。
  • 錯(cuò)誤過濾器:
    • SendErrorFilter: 如果RequestContext.getThrowable()不為null,則轉(zhuǎn)發(fā)到/error(默認(rèn)情況下)。 您可以通過設(shè)置error.path屬性來更改默認(rèn)轉(zhuǎn)發(fā)路徑(/error)。

8.16.5 @EnableZuulProxy過濾

創(chuàng)建DiscoveryClientRouteLocator,用于從DiscoveryClient(例如Eureka)以及屬性加載路徑定義。 為DiscoveryClient中的每個(gè)serviceId創(chuàng)建一個(gè)路由。 添加新服務(wù)后,將刷新路由。

除了前面描述的過濾器之外,還安裝了以下過濾器(與普通的Spring Bean一樣):

  • 預(yù)過濾器:
    • PreDecorationFilter: 確定路由的位置和方式,具體取決于提供的RouteLocator。 它還為下游請(qǐng)求設(shè)置各種與代理相關(guān)的標(biāo)頭。
  • 路由過濾器:
    • RibbonRoutingFilter: 使用Ribbon,Hystrix和可插入HTTP客戶端發(fā)送請(qǐng)求。 服務(wù)ID位于RequestContext屬性FilterConstants.SERVICE_ID_KEY中。 此過濾器可以使用不同的HTTP客戶端:
      • Apache HttpClient: 默認(rèn)客戶端。
      • Squareup OkHttpClient v3: 通過在類路徑上設(shè)置com.squareup.okhttp3:okhttp庫并設(shè)置ribbon.okhttp.enabled = true來啟用。
      • Netflix Ribbon HTTP client: 通過設(shè)置ribbon.restclient.enabled = true啟用。 此客戶端具有限制,包括它不支持PATCH方法,但它也具有內(nèi)置重試。
    • SimpleHostRoutingFilter:

8.16.6 自定義Zuul過濾器示例

下面的大多數(shù)“如何寫”示例包括Sample Zuul Filters項(xiàng)目。 還有一些操作該存儲(chǔ)庫中的請(qǐng)求或響應(yīng)主體的示例。

本節(jié)包括以下示例:

如何編寫預(yù)過濾器

預(yù)過濾器在RequestContext中設(shè)置數(shù)據(jù),以便在下游過濾器中使用。 主要用例是設(shè)置路由過濾器所需的信息。 以下示例顯示了Zuul預(yù)過濾器:

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

前面的過濾器從sample請(qǐng)求參數(shù)填充SERVICE_ID_KEY。 在實(shí)踐中,您不應(yīng)該進(jìn)行這種直接映射。 相反,應(yīng)該從sample的值中查找服務(wù)ID。

現(xiàn)在已填充SERVICE_ID_KEY,PreDecorationFilter不會(huì)運(yùn)行并且RibbonRoutingFilter會(huì)運(yùn)行。

如果要路由到完整URL,請(qǐng)改為調(diào)用ctx.setRouteHost(url)

要修改路由過濾器轉(zhuǎn)發(fā)的路徑,請(qǐng)?jiān)O(shè)置REQUEST_URI_KEY

如何編寫路由過濾器

路由過濾器在預(yù)過濾器之后運(yùn)行并向其他服務(wù)發(fā)出請(qǐng)求。 這里的大部分工作是將請(qǐng)求和響應(yīng)數(shù)據(jù)轉(zhuǎn)換為客戶端所需的模型。 以下示例顯示了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請(qǐng)求信息轉(zhuǎn)換為OkHttp3請(qǐng)求信息,執(zhí)行HTTP請(qǐng)求,并將OkHttp3響應(yīng)信息轉(zhuǎn)換為Servlet響應(yīng)。

如何編寫后置過濾器

后置過濾器通常會(huì)操縱響應(yīng)。 以下過濾器添加隨機(jī)UUID作為X-Sample標(biāo)頭:

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

其他操作(例如轉(zhuǎn)換響應(yīng)體)要復(fù)雜得多且計(jì)算量大。

8.16.7 Zuul錯(cuò)誤如何工作

如果在Zuul過濾器生命周期的任何部分期間拋出異常,則執(zhí)行錯(cuò)誤過濾器。 僅當(dāng)RequestContext.getThrowable()不為null時(shí),才會(huì)運(yùn)行SendErrorFilter。 然后,它在請(qǐng)求中設(shè)置特定的javax.servlet.error.*屬性,并將請(qǐng)求轉(zhuǎn)發(fā)到Spring Boot錯(cuò)誤頁面。

8.16.8 Zuul Eager應(yīng)用程序上下文加載

Zuul內(nèi)部使用Ribbon來調(diào)用遠(yuǎn)程URL。 默認(rèn)情況下,Spring Cloud在第一次調(diào)用時(shí)會(huì)延遲加載Ribbon客戶端。 可以使用以下配置更改Zuul的此行為,這會(huì)導(dǎo)致在應(yīng)用程序啟動(dòng)時(shí)急切加載與子功能區(qū)相關(guān)的應(yīng)用程序上下文。 以下示例顯示如何啟用預(yù)先加載:

application.yml.

zuul:
  ribbon:
    eager-load:
      enabled: true
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容