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.maxTotalConnections和zuul.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
您可以使用regexmapper在serviceId和路由之間提供約定。 它使用正則表達(dá)式命名組從serviceId中提取變量并將它們注入路由模式,如以下示例所示:
ApplicationConfiguration.java.
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
上面的示例表示myusers-v1的serviceId映射到路由/v1/myusers/**。 接受任何正則表達(dá)式,但所有命名組必須同時(shí)出現(xiàn)在servicePattern和routePattern中。 如果servicePattern與serviceId不匹配,則使用默認(rèn)行為。 在前面的示例中,myusers的serviceId映射到/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。 要使用RestClient或okhttp3.OkHttpClient,請(qǐng)分別設(shè)置ribbon.restclient.enabled = true或ribbon.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-Cookie和Cookie設(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ì)/routes的POST強(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.ReadTimeout和ribbon.SocketTimeout功能區(qū)屬性配置這些超時(shí)。
如果通過指定URL配置了Zuul路由,則需要使用zuul.host.connect-timeout-millis和zuul.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í)際的HttpServletRequest和HttpServletResponse的信息都存儲(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: 使用ServletRequestDispatcher轉(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
OkHttpClientv3: 通過在類路徑上設(shè)置com.squareup.okhttp3:okhttp庫并設(shè)置ribbon.okhttp.enabled = true來啟用。 - Netflix Ribbon HTTP client: 通過設(shè)置
ribbon.restclient.enabled = true啟用。 此客戶端具有限制,包括它不支持PATCH方法,但它也具有內(nèi)置重試。
- Apache
-
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