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.ymlzuul: ignoredServices: '*' routes: users: /myusers/**
上述例子中,所有的服務都將跳過Zuul的代理,除了users服務。
-
如果要添加或者改變代理路由,可以將配置信息配置成如下所示:
application.ymlzuul: routes: users: /myusers/**
上述配置意味着如果HTTP請求/myusers將會轉發到users服務(例:/myusers/101將會轉發到users服務的/101)
-
如果想要通過路由獲取到更好的粒度控制,則可以獨立的指定path和serviceId,配置如下:
application.ymlzuul: routes: users: path: /myusers/** serviceId: users_service
上述配置意味着如果HTTP請求/myusers將會轉發到serviceId爲users_service的服務中去。
此場景中必須指定ant-style風格的格式。所以/myusers/*只匹配一級路徑,/myusers/**可匹配多級路徑。 -
後臺服務的位置可以通過serviceId(通過服務發現獲取)指定,也可以通過url指定,配置如下:
application.ymlzuul: routes: users: path: /myusers/** url: http://example.com/users_service
-
上述所有的這些路由例子都不會以HystrixCommand的方式執行,有多個url的時候也不會使用Ribbon做負載均衡。
如果要實現這些目標,可以指定一個靜態服務的list,配置如下:
application.ymlzuul: 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.ymlzuul: 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.ymlzuul: 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.ymlzuul: ignoredPatterns: /**/admin/** routes: users: /myusers/**
上面的例子意味着請求如/myusers/101將轉發到users服務的/101路徑中,然而如果請求路徑中包含/admin/將不會被轉發。
-
如果你需要保證路由的順序,你需要使用YAML文件。使用properties文件將丟失順序訪問的功能。下面的例子就是這樣一個YAML文件:
application.ymlzuul: 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.ymlzuul: 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.ymlzuul: 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.ignoredHeaders
爲false
。如果您在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.ymlzuul: 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.ymlhystrix.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.ymlzuul: forceOriginalQueryStringEncoding: true
PS:這個特殊標記只在SimpleHostRoutingFilter中有效果。因此你也丟失了使用
RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters)
輕鬆重寫查詢參數的能力,因爲查詢字符串現在直接從原始的HttpServletRequest中獲取。
十. 請求URI編碼(Request URI Encoding)
-
當處理正在進來的請求時,請求URI將在匹配路由前進行解碼。此後將在路由過濾器(route filters)中重建後端請求的時候被重新編碼。如果你的URI中包含"/"這將出現一些不可預知的行爲。
-
如果要使用原始的請求URI(request URI),可以往ZuulProperties中設置一個特殊的標記, 這樣查詢字符串就可以與HttpServletRequest::getRequstURI方法一起使用。 例子如下:
application.ymlzuul: 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.ymlzuul: 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.ReadTimeout
和ribbon.SocketTimeout
屬性。 - 如果你配置使用Zuul的方式是指定url的方式 ,你需要配置
zuul.host.connect-timeout-millis
和uul.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處理特定的跨域請求,你可以使用自定義的WebMvcConfigurer
bean:
@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()中找到。
- RibbonRoutingFilter: 使用 Ribbon, Hystrix, 和 pluggable HTTP 客戶端發送請求,Service IDs將會放入到RequestContext屬性中,key爲FilterConstants.SERVICE_ID_KEY。此過濾器可使用不同的HTTP客戶端:
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
參考文檔: