1. 程式人生 > >springboot2.x之spring-cloud-starter-netflix-zuul

springboot2.x之spring-cloud-starter-netflix-zuul

spring cloud連載第三篇補充之Zuul

由於Zuul的內容較多所以單獨列出一篇來講。全是乾貨,如果學到東西的,動動小手給點個推薦^_^  謝謝!

1. Router and Filter: Zuul(路由和過濾:Zuul)

路由是微服務架構不缺少的一部分。例如“/”可能對映到web服務,“/api/users”對映到使用者管理服務,而“/api/shop”對映到採購服務。Zuul是Netflix中的一個基於JVM的路由器,也是一個服務端負載均衡器。

zuul有下列用途:

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

 

注意:

1)zuul.max.host.connections已經被zuul.host.maxTotalConnections(預設值200)和zuul.host.maxPerRouteConnections(預設值20)代替了。

2)Hystrix對所有理由的預設隔離模式是SEMAPHORE,可以通過zuul.ribbonIsolationStrategy改為THREAD。

1.1 How to Include Zuul(依賴)

1         <dependency>
2             <groupId>org.springframework.cloud</groupId>
3             <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
4         </dependency>

1.2 Embedded Zuul Reverse Proxy(反向代理)

Spring Cloud建立了一個內建Zuul代理來簡化開發,比如有一個UI應用想要使用代理呼叫後端的一個或者多個服務。這可以避免為後臺每個服務都配置CORS和許可權系統。

在spring boot的入口類上使用@EnableZuulProxy註解來開啟代理。代理使用Ribbon通過服務發現來定位後端服務例項。並且所有請求在 hystrix command中執行。所以當斷路器開啟時,代理將不會重試連線後端服務。

注意:Zuul starter不包含服務發現客戶端,所以想要使用服務發現功能,需要提供一個服務發現客戶端(比如Eureka)。

為了防止自動新增服務,可以設定zuul.ignored-services引數來避免。如果一個服務匹配到一個忽略表示式,並且又在路由對映中明確指定了,那麼它就不會被忽略。例如(application.yml):

1 zuul:
2   ignoredServices: '*'
3   routes:
4     users: /myusers/**

你可以單獨指定路徑和service ID,例如(application.yml):

1  zuul:
2   routes:
3     users:
4       path: /myusers/**
5       serviceId: users_service

其中path是一個ant風格的表示式,所以/myusers/*僅僅匹配一層目錄,而/myusers/**可以匹配任意多層級目錄。

其中後端服務的位置既可以使用serviceId也可以使用url(物理位置)指定。如下(application.yml):

1  zuul:
2   routes:
3     users:
4       path: /myusers/**
5       url: http://example.com/users_service

這些簡單的url路由不會作為HystrixCommand執行,也不會使用Ribbon負載均衡。如果要使用的話,可以指定一個伺服器列表的serviceId,如下:

application.yml.

複製程式碼

 1 zuul:
 2   routes:
 3     echo:
 4       path: /myusers/**
 5       serviceId: myusers-service
 6       stripPrefix: true
 7 
 8 hystrix:
 9   command:
10     myusers-service:
11       execution:
12         isolation:
13           thread:
14             timeoutInMilliseconds: ...
15 
16 myusers-service:
17   ribbon:
18     NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
19     listOfServers: http://example1.com,http://example2.com
20     ConnectTimeout: 1000
21     ReadTimeout: 3000
22     MaxTotalHttpConnections: 500
23     MaxConnectionsPerHost: 100

複製程式碼

另一個方法是指定一個服務路由並且為serviceId配置Ribbon客戶端(這麼做需要在Ribbon中禁用Eureka),如下:

application.yml.

複製程式碼

 1 zuul:
 2   routes:
 3     users:
 4       path: /myusers/**
 5       serviceId: users
 6 
 7 ribbon:
 8   eureka:
 9     enabled: false
10 
11 users:
12   ribbon:
13     listOfServers: example.com,google.com

複製程式碼

可以使用正則表示式來配置路由規則。如下:

ApplicationConfiguration.java.

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

在上面的例子中如果serviceId為myusers-v1那麼它將被對映到/v1/myusers/**。如果有一個serviceId不匹配,那麼將會使用預設規則。例如,在上面的例子中一個serviceId為myusers的服務將會對映到"/myusers/**"。

給所有對映新增字首可以使用zuul.prefix。預設情況下,請求被轉發前將會去除掉其中的代理字首(可以使用zuul.stripPrefix=false來改變預設行為)。也可以在單獨的一個路由中關閉去除服務指定字首的行為。如下:

application.yml. 

1  zuul:
2   routes:
3     users:
4       path: /myusers/**
5       stripPrefix: false

注意:zuul.stripPrefix只是針對zuul.prefix,對路由中的路徑不起作用。

在上面的例子中/myusers/101請求將會轉發為/myusers/101到users服務中。

zuul.routes是繫結在ZuulProperties物件上。在這個類裡可以看到retryable屬性,將它設定為true,可以在請求失敗時使用Ribbon客戶端重試。

預設情況下,X-Forwarded-Host將會新增到轉發的請求頭上,可以設定zuul.addProxyHeaders = false來關閉它。預設情況下,路徑中的字首將會被跳過,轉發的請求頭中將會有一個X-Forwarded-Prefix頭(例如上面例子中的/myusers)。

注意:如果你希望你配置的路由是有序的話,那你應該使用yml配置檔案,因為使用properties檔案時會丟失排序。

1.3 Zuul Http Client(Zuul HTTP客戶端)

Zuul的預設HTTP客戶端是Apache HTTP客戶端而不是已經過時的Ribbon的RestClient。如果要使用RestClient或者okhttp3.OkHttpClient,可以設定ribbon.restclient.enabled=true 或者 ribbon.okhttp.enabled=true。

如果你想自定義Apache HTTP client 或者 OK HTTP client,那麼需要提供一個ClosableHttpClient 或者 OkHttpClient型別的bean。

1.4 Cookies and Sensitive Headers(cookies和敏感頭部)

可以在路由配置中指定要忽略的頭部,如下:

application.yml.

1 zuul:
2   routes:
3     users:
4       path: /myusers/**
5       sensitiveHeaders: Cookie,Set-Cookie,Authorization
6       url: https://downstream

注意:上面的例子是sensitiveHeaders的預設值,這是在Spring Cloud Netflix 1.1版本新增的功能。(在1.0中,不能設定頭部並且所有cookie雙向流動)。

sensitiveHeaders是一個黑名單,預設不為空。因此要讓Zuul傳送所有頭部的話,需要明確指定sensitiveHeaders為空。如下:

application.yml. 

1  zuul:
2   routes:
3     users:
4       path: /myusers/**
5       sensitiveHeaders:
6       url: https://downstream

你也可以通過zuul.sensitiveHeader進行全域性設定。如果在一個路由上設定sensitiveHeaders的話將會覆蓋全域性設定。

1.5 Ignored Headers(忽略頭部)

除了路由敏感頭部以外,你還可以設定zuul.ignoredHeaders成那些在與下游服務互動時應該剔除的值。預設,Spring Security不在classpath上時,這些值是空的。否則他們被Spring Security初始化為一些常見的“安全”頭部。

這種情況下,下游的服務也可能設定這些頭部,但是我們想要的是代理中的值。如果Spring Security在classpath上時,為了不剔除這些安全頭部,可以設定zuul.ignoreSecurityHeaders=false。

1.6 Management Endpoints(管理端點)

如果你同時使用@EnableZuulProxy 和Spring Boot Actuator,那麼將會開啟兩個額外的端點:

  • Routes
  • Filters

1.6.1 Routes Endpoint(路由端點)

get請求/routes:

GET /routes. 

1 {
2   /stores/**: "http://localhost:8081"
3 }

如果想要得到路由的詳細資訊,在請求上新增?format=details查詢欄位。

GET /routes/details

複製程式碼

 1 {
 2   "/stores/**": {
 3     "id": "stores",
 4     "fullPath": "/stores/**",
 5     "location": "http://localhost:8081",
 6     "path": "/**",
 7     "prefix": "/stores",
 8     "retryable": false,
 9     "customSensitiveHeaders": false,
10     "prefixStripped": true
11   }
12 }

複製程式碼

/routes的POST方法將會強制重新整理路由資訊。

可以通過endpoints.routes.enabled=false來禁用這個端點。

注意:路由會自動根據服務目錄的改動來更新,但是POST請求/routes是一種立即強制更新的方法。

 

1.6.2 Filters Endpoint(過濾器端點)

/filters的GET請求將會返回過濾器型別列表。

1.7 Strangulation Patterns and Local Forwards(壓縮模式和本地轉發)

當遷移一個老的應用或者API時,需要慢慢把它的訪問端點替換成新的實現。Zuul會是一個很有用的代理,因為你可以使用它處理所有來自客戶端老的端點的流量並且重定向一些請求到新的實現上。如下:

application.yml. 

複製程式碼

 1  zuul:
 2   routes:
 3     first:
 4       path: /first/**
 5       url: http://first.example.com
 6     second:
 7       path: /second/**
 8       url: forward:/second
 9     third:
10       path: /third/**
11       url: forward:/3rd
12     legacy:
13       path: /**
14       url: http://legacy.example.com

複製程式碼

其中,forward:開頭的url將會轉發到本地。

 

1.8 Uploading Files through Zuul(通過Zuul上傳檔案)

如果你使用@EnableZuulProxy,那麼可以通過代理路徑來上傳一些小檔案,對於大檔案有一個可以繞過Spring DispatcherServlet的路徑“/zuul/*”,換句話說,如果zuul.routes.customers=/customers/*,那麼可以

通過傳送POST請求到/zuul/customers/*。servlet路徑是通過zuul.servletPath外部化的。如果代理路由使用Ribbon,尤其大檔案需要提高超時時間。如下:

application.yml. 

1 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
2 ribbon:
3   ConnectTimeout: 3000
4   ReadTimeout: 60000

1.9 Query String Encoding(查詢欄位編碼)

當處理請求時,查詢欄位將會被解碼,這樣就可以在Zuul過濾器中修改他們。然後在過濾器中再重新編碼後傳送請求給後端。如果使用Javascrip的encodeURIComponent()方法,那麼結果可能會和原始輸入不同。這在大多數情況下不會有問題,但是一些web伺服器對於複雜查詢欄位的編碼要求還是很挑剔的。

為了強制查詢字串的原始編碼,可以向ZuulProperties傳遞一個特殊的標誌,以便將查詢字串作為HttpServletRequest::getQueryString方法使用。

application.yml. 

1  zuul:
2   forceOriginalQueryStringEncoding: true

注意:這個特殊的標誌只對SimpleHostRoutingFilter有效,另外可以通過RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters)來覆蓋查詢字串。

1.10 Plain Embedded Zuul(純內建Zuul)

如果使用@EnableZuulServer (而不是@EnableZuulProxy),可以啟動一個Zuul伺服器但是沒有任何代理。任何ZuulFilter型別的bean會自動安裝,但是沒有任何代理過濾器會被自動新增。

這種情況下,Zuul伺服器的路由依然可以通過"zuul.routes.*"來配置,但是沒有服務發現和代理。因此,"serviceId" 和 "url"會被忽略掉。在下面的例子中所有"/api/**"中的路徑都會被對映到Zuul的過濾器鏈。

application.yml. 

1  zuul:
2   routes:
3     api: /api/**

1.11 Disable Zuul Filters(禁用Zuul過濾器)

在代理和伺服器模式,spring cloud Zuul都會預設註冊一些ZuulFilter。可以通過zuul.<SimpleClassName>.<filterType>.disable=true來禁用指定的過濾器。

按照慣例,包名中filters的後面就是filterType。例如,如果要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter,可以設定zuul.SendResponseFilter.post.disable=true。

1.12 Providing Hystrix Fallbacks For Routes(為路由提供Hystrix降級服務)

當Zuul的路由迴路出現問題時,可以通過一個FallbackProvider型別的bean來提供降級服務。在這個bean中,需要指定路由的ID,並且提供一個ClientHttpResponse。下面的例子提供了一個相對簡單的FallbackProvider的實現。

複製程式碼

 1 class MyFallbackProvider implements FallbackProvider {
 2 
 3     @Override
 4     public String getRoute() {
 5         return "customers";
 6     }
 7 
 8     @Override
 9     public ClientHttpResponse fallbackResponse(String route, final Throwable cause) {
10         if (cause instanceof HystrixTimeoutException) {
11             return response(HttpStatus.GATEWAY_TIMEOUT);
12         } else {
13             return response(HttpStatus.INTERNAL_SERVER_ERROR);
14         }
15     }
16 
17     private ClientHttpResponse response(final HttpStatus status) {
18         return new ClientHttpResponse() {
19             @Override
20             public HttpStatus getStatusCode() throws IOException {
21                 return status;
22             }
23 
24             @Override
25             public int getRawStatusCode() throws IOException {
26                 return status.value();
27             }
28 
29             @Override
30             public String getStatusText() throws IOException {
31                 return status.getReasonPhrase();
32             }
33 
34             @Override
35             public void close() {
36             }
37 
38             @Override
39             public InputStream getBody() throws IOException {
40                 return new ByteArrayInputStream("fallback".getBytes());
41             }
42 
43             @Override
44             public HttpHeaders getHeaders() {
45                 HttpHeaders headers = new HttpHeaders();
46                 headers.setContentType(MediaType.APPLICATION_JSON);
47                 return headers;
48             }
49         };
50     }
51 }

複製程式碼

下面的例子是對應上面例子的路由配置:

1 zuul:
2   routes:
3     customers: /customers/**

如果要對所有路由提供一個預設降級服務,可以建立一個FallbackProvider型別的bean,然後在getRoute方法中返回“*”或者null。如下:

複製程式碼

 1 class MyFallbackProvider implements FallbackProvider {
 2     @Override
 3     public String getRoute() {
 4         return "*";
 5     }
 6 
 7     @Override
 8     public ClientHttpResponse fallbackResponse(String route, Throwable throwable) {
 9         return new ClientHttpResponse() {
10             @Override
11             public HttpStatus getStatusCode() throws IOException {
12                 return HttpStatus.OK;
13             }
14 
15             @Override
16             public int getRawStatusCode() throws IOException {
17                 return 200;
18             }
19 
20             @Override
21             public String getStatusText() throws IOException {
22                 return "OK";
23             }
24 
25             @Override
26             public void close() {
27 
28             }
29 
30             @Override
31             public InputStream getBody() throws IOException {
32                 return new ByteArrayInputStream("fallback".getBytes());
33             }
34 
35             @Override
36             public HttpHeaders getHeaders() {
37                 HttpHeaders headers = new HttpHeaders();
38                 headers.setContentType(MediaType.APPLICATION_JSON);
39                 return headers;
40             }
41         };
42     }
43 }

複製程式碼

1.13 Zuul Timeouts(Zuul的超時時間)

如果想要為通過Zuul代理的請求設定socket超時時間和讀取超時時間,你有兩個選項,基於配置:

1)如果Zuul使用服務發現,則配置ribbon.ReadTimeout和ribbon.SocketTimeout;

2)如果路由是通過URL指定的,那麼需要配置zuul.host.connect-timeout-millis和zuul.host.socket-timeout-millis

1.14 Rewriting the Location header(重寫頭部Location欄位)

如果Zuul在一個web應用前面,那麼你需要重寫Location頭部當你的web應用通過HTTP狀態碼3XX重定向。否則,瀏覽器會重定向到web應用的URL而不是Zuul的URL。

可以通過配置一個LocationRewriteFilter型別的Zuul過濾器來重寫Location頭部到Zuul的URL。它還恢復了刪除的全域性字首和特定於路由的字首。如下:

複製程式碼

 1 import org.springframework.cloud.netflix.zuul.filters.post.LocationRewriteFilter;
 2 ...
 3 
 4 @Configuration
 5 @EnableZuulProxy
 6 public class ZuulConfig {
 7     @Bean
 8     public LocationRewriteFilter locationRewriteFilter() {
 9         return new LocationRewriteFilter();
10     }
11 }

複製程式碼

注意:要非常小心使用這個過濾器,因為它會作用於所有響應碼為3XX的Location頭部,這可能在某些場合不適合。比如要重定向到一個外部地址。

1.15 Metrics(度量)

Zuul在Actuator metrics端點下提供metrics,當路由請求出現失敗時。可以通過/actuator/metrics端點檢視。metrics名稱的格式為ZUUL::EXCEPTION:errorCause:statusCode。

1.16 Zuul Developer Guide(Zuul開發指南)

1.16.1 The Zuul Servlet

Zuul的實現是一個Servlet。通常情況下,Zuul是嵌入到Spring分發機制中的。Spring MVC會掌控路由。這種情況下,Zuul會快取請求。如果有一種情況是穿過Zuul但是不要快取(例如大檔案的上傳),這時可以使用一種獨立於Spring分發器的外部Servlet。預設情況,這個Servlet的地址是/zuul。也可以通過zuul.servlet-path屬性來修改。

1.16.2 Zuul RequestContext

Zuul使用RequestContext在不同的過濾器中傳遞資訊。它的資料儲存在特定於每個請求的ThreadLocal中.它儲存的資訊有:路由請求到何處,錯誤,HttpServletRequest 和 HttpServletResponse。

RequestContext繼承ConcurrentHashMap,所以它可以儲存任何資訊。FilterConstants儲存了那些被過濾器使用的key。

1.16.3 @EnableZuulProxy vs. @EnableZuulServer

Spring Cloud Netflix安裝了一系列過濾器,安裝了哪些過濾器依賴於你使用哪種註解來開啟Zuul的。@EnableZuulProxy是@EnableZuulServer的超集。換句話說,@EnableZuulProxy包含了@EnableZuulServer中的過濾器。

在“proxy”模式中的額外過濾器開啟了路由功能。所以如果想要一個“空白”的Zuul,就使用@EnableZuulServer。

1.16.4 @EnableZuulServer Filters

@EnableZuulServer建立一個SimpleRouteLocator(它從Spring Boot配置檔案中載入路由定義)。

安裝的過濾器(作為普通的spring bean):

1)Pre filters:

ServletDetectionFilter:檢測請求是否是通過Spring Dispatcher,設定FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY的布林值。

FormBodyWrapperFilter:解析表單資料,並且為下游請求重新編碼這些資料。

DebugFilter:如果請求引數中設定了debug,則RequestContext.setDebugRouting()和RequestContext.setDebugRequest()都設定為true。

2)Route filters:

SendForwardFilter:使用RequestDispatcher轉發請求。轉發地址儲存在RequestContext的FilterConstants.FORWARD_TO_KEY屬性中。

3)Post filters:

SendResponseFilter:將代理請求的響應寫入到當前的響應中。

4)Error filters:

SendErrorFilter:如果RequestContext.getThrowable()不是null,則轉發到/error(預設)。也可以error.path設定轉發路徑。

 

1.16.5 @EnableZuulProxy Filters

建立一個 DiscoveryClientRouteLocator(從DiscoveryClient(例如Eureka)和配置檔案中載入路由定義)。為每個服務發現客戶端中的serviceId都會建立一個路由。當有新服務新增時,路由就會重新整理。

除了上面的過濾器外,還有額外的過濾器:

1)Pre filters:

PreDecorationFilter:根據提供的RouteLocator來決定如何路由,並且路由到何處。並且為下游服務設定了一些代理相關的頭部。

2)Route filters:

RibbonRoutingFilter:使用Ribbon、Hystrix和可插入的HTTP客戶端傳送請求。服務ID儲存在RequestContext的FilterConstants.SERVICE_ID_KEY鍵中。

這個過濾器可以使用不同的HTTP客戶端:

1️⃣Apache HttpClient:預設客戶端

2️⃣Squareup OkHttpClient v3:新增com.squareup.okhttp3:okhttp依賴,並且設定ribbon.okhttp.enabled=true。

3️⃣Netflix Ribbon HTTP client:設定ribbon.restclient.enabled=true。但是這個客戶端有一些限制,它不支援PATCH方法,但是有內建的重試機制。

SimpleHostRoutingFilter:通過Apache HttpClient向預定的url傳送請求。URL在RequestContext.getRouteHost()中。

1.16.6 Custom Zuul Filter Examples(自定義Zuul過濾器示例)

下面大多數的例子都包括在Sample Zuul Filters專案中。這個專案中還包含了一些如果修改請求或者響應的訊息體的例子。

How to Write a Pre Filter

Pre filters為在RequestContext中設定資料,給下游的過濾器使用。主要用途就設定一些route過濾器需要的資訊。如下:

複製程式碼

 1 public class QueryParamPreFilter extends ZuulFilter {
 2     @Override
 3     public int filterOrder() {
 4         return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
 5     }
 6 
 7     @Override
 8     public String filterType() {
 9         return PRE_TYPE;
10     }
11 
12     @Override
13     public boolean shouldFilter() {
14         RequestContext ctx = RequestContext.getCurrentContext();
15         return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded
16                 && !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId
17     }
18     @Override
19     public Object run() {
20         RequestContext ctx = RequestContext.getCurrentContext();
21         HttpServletRequest request = ctx.getRequest();
22         if (request.getParameter("sample") != null) {
23             // put the serviceId in `RequestContext`
24             ctx.put(SERVICE_ID_KEY, request.getParameter("foo"));
25         }
26         return null;
27     }
28 }

複製程式碼

上面的過濾器使用sample請求引數填充SERVICE_ID_KEY。實際上不應該做這種直接對映,Service ID應該從sample的值中查詢。

現在,SERVICE_ID_KEY已經被填充,所以PreDecorationFilter將不會執行,RibbonRoutingFilter會執行。

注意:如果想路由到一個完整的URL,呼叫ctx.setRouteHost(url)。

要修改路由過濾器轉發到的路徑,請設定REQUEST_URI_KEY。

How to Write a Route Filter

Route filters在pre filters後執行。它轉發請求到其他服務。這裡的大部分工作是將請求和響應資料轉換到客戶機所需的模型。

複製程式碼

 1 public class OkHttpRoutingFilter extends ZuulFilter {
 2     @Autowired
 3     private ProxyRequestHelper helper;
 4 
 5     @Override
 6     public String filterType() {
 7         return ROUTE_TYPE;
 8     }
 9 
10     @Override
11     public int filterOrder() {
12         return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1;
13     }
14 
15     @Override
16     public boolean shouldFilter() {
17         return RequestContext.getCurrentContext().getRouteHost() != null
18                 && RequestContext.getCurrentContext().sendZuulResponse();
19     }
20 
21     @Override
22     public Object run() {
23         OkHttpClient httpClient = new OkHttpClient.Builder()
24                 // customize
25                 .build();
26 
27         RequestContext context = RequestContext.getCurrentContext();
28         HttpServletRequest request = context.getRequest();
29 
30         String method = request.getMethod();
31 
32         String uri = this.helper.buildZuulRequestURI(request);
33 
34         Headers.Builder headers = new Headers.Builder();
35         Enumeration<String> headerNames = request.getHeaderNames();
36         while (headerNames.hasMoreElements()) {
37             String name = headerNames.nextElement();
38             Enumeration<String> values = request.getHeaders(name);
39 
40             while (values.hasMoreElements()) {
41                 String value = values.nextElement();
42                 headers.add(name, value);
43             }
44         }
45 
46         InputStream inputStream = request.getInputStream();
47 
48         RequestBody requestBody = null;
49         if (inputStream != null && HttpMethod.permitsRequestBody(method)) {
50             MediaType mediaType = null;
51             if (headers.get("Content-Type") != null) {
52                 mediaType = MediaType.parse(headers.get("Content-Type"));
53             }
54             requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream));
55         }
56 
57         Request.Builder builder = new Request.Builder()
58                 .headers(headers.build())
59                 .url(uri)
60                 .method(method, requestBody);
61 
62         Response response = httpClient.newCall(builder.build()).execute();
63 
64         LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();
65 
66         for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {
67             responseHeaders.put(entry.getKey(), entry.getValue());
68         }
69 
70         this.helper.setResponse(response.code(), response.body().byteStream(),
71                 responseHeaders);
72         context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running
73         return null;
74     }
75 }

複製程式碼

上面的過濾器將Servlet請求資訊轉換到OkHttp3請求資訊中,並且傳送一個HTTP請求,然後將OkHttp3響應資訊轉換到Servlet響應資訊中。

How to Write a Post Filter

Post filters主要是用來修改響應。下面的例子中在響應頭中新增一個X-Sample頭部並且設定為UUID。

複製程式碼

 1 public class AddResponseHeaderFilter extends ZuulFilter {
 2     @Override
 3     public String filterType() {
 4         return POST_TYPE;
 5     }
 6 
 7     @Override
 8     public int filterOrder() {
 9         return SEND_RESPONSE_FILTER_ORDER - 1;
10     }
11 
12     @Override
13     public boolean shouldFilter() {
14         return true;
15     }
16 
17     @Override
18     public Object run() {
19         RequestContext context = RequestContext.getCurrentContext();
20         HttpServletResponse servletResponse = context.getResponse();
21         servletResponse.addHeader("X-Sample", UUID.randomUUID().toString());
22         return null;
23     }
24 }

複製程式碼

注意:其他操作,比如轉換響應體,則要複雜得多,計算量也大得多。

1.16.7 How Zuul Errors Work(Zuul錯誤)

Zuul過濾器的生命週期的任何階段出現異常,error過濾器將會執行。當RequestContext.getThrowable()不為null時,SendErrorFilter將會執行。它然後在請求中設定javax.servlet.error.*屬性,

然後將請求轉發到spring boot的錯誤頁面。

1.16.8 Zuul Eager Application Context Loading

Zuul內部使用Ribbon來請求遠端URL。預設,Ribbon客戶端在第一次被使用時才被Spring Cloud載入。可以通過下面的配置來改變預設行為。它會在啟動時初始化Ribbon相關的上下文。

application.yml. 

1 zuul:
2   ribbon:
3     eager-load:
4       enabled: true

1.17 Retrying Failed Requests(重試失敗請求)

Spring Cloud Netflix提供了許多傳送HTTP請求的方法。你可以使用RestTemplate, Ribbon, 或者 Feign。無論怎麼選擇建立HTTP請求,都有可能請求失敗。

當請求失敗時,你可能想要請求自動重試。當使用Sping Cloud Netflix時,你需要新增Spring Retry到classpath上。這樣RestTemplates, Feign, 和 Zuul會在請求失敗時

自動重試。

1.17.1 BackOff Policies(補償政策)

預設,在使用重試機制時是沒有補償政策的。如果你想配置一個補償政策,則需要建立一個LoadBalancedBackOffPolicyFactory型別的bean。它會為指定的服務建立一個BackOffPolicy。如下:

複製程式碼

 1 @Configuration
 2 public class MyConfiguration {
 3     @Bean
 4     LoadBalancedBackOffPolicyFactory backOffPolicyFactory() {
 5         return new LoadBalancedBackOffPolicyFactory() {
 6             @Override
 7             public BackOffPolicy createBackOffPolicy(String service) {
 8                 return new ExponentialBackOffPolicy();
 9             }
10         };
11     }
12 }

複製程式碼

1.17.2 Configuration(配置)

當你使用Ribbon和Spring Retry時,你可以通過配置某些Ribbon屬性來控制重試功能。例如,client.ribbon.MaxAutoRetriesclient.ribbon.MaxAutoRetriesNextServer, 和 client.ribbon.OkToRetryOnAllOperations。

注意:開啟client.ribbon.OkToRetryOnAllOperations的話將會包括重試POST請求,這樣會對伺服器資源有些影響,因為它會快取請求體資料。

另外,你可能希望對某些響應中的狀態碼進行重試請求。可通過設定clientName.ribbon.retryableStatusCodes。

1 clientName:
2   ribbon:
3     retryableStatusCodes: 404,502

你也可以建立一個LoadBalancedRetryPolicy型別的bean,並且實現retryableStatusCode方法。

1.17.2.1 Zuul

可以通過zuul.retryable設定為false來關閉zuul中的重試機制。也可以設定zuul.routes.routename.retryable為false來關閉某個指定路由上的重試機制。

1.18 HTTP Clients

Spring Cloud Netflix 會為Ribbon, Feign, 和 Zuul自動建立HTTP客戶端。你也可以提供你自己的HTTP客戶端。如果您使用的是Apache Http Cient,那麼可以建立型別為ClosableHttpClient的bean,或者如果您使用的是OK Http,則可以建立OkHttpClient。

注意:當您建立自己的HTTP客戶機時,您還負責為這些客戶機實現正確的連線管理策略。如果沒做好會導致資源管理問題。