1. 程式人生 > >⑥SpringCloud 實戰:引入gateway元件,開啟閘道器路由功能

⑥SpringCloud 實戰:引入gateway元件,開啟閘道器路由功能

這是SpringCloud實戰系列中第4篇文章,瞭解前面第兩篇文章更有助於更好理解本文內容: [①SpringCloud 實戰:引入Eureka元件,完善服務治理](https://jinglingwang.cn/archives/eureka) [②SpringCloud 實戰:引入Feign元件,發起服務間呼叫](https://jinglingwang.cn/archives/feign) [③SpringCloud 實戰:使用 Ribbon 客戶端負載均衡](https://jinglingwang.cn/archives/ribbon) [](https://jinglingwang.cn/archives/feign)[④SpringCloud 實戰:引入Hystrix元件,分散式系統容錯](https://jinglingwang.cn/archives/hystrix) [⑤SpringCloud 實戰:引入Zuul元件,開啟閘道器路由](https://jinglingwang.cn/archives/zuul) ## 簡介 Spring Cloud Gateway 是 Spring Cloud 的一個子專案,該專案是基於 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開發的閘道器,它旨在為微服務架構提供一種簡單有效的統一的 API 路由管理方式。 Spring Cloud Gateway 作為 Spring Cloud 生態系統中的閘道器,目標是替代 Netflix Zuul,其不僅提供統一的路由方式,並且基於 Filter 鏈的方式提供了閘道器基本的功能,例如:安全,監控/指標,和限流。 ## 特性 Spring Cloud Gateway 具有如下特性: - 基於Spring Framework 5, Project Reactor 和 Spring Boot 2.0 進行構建; - 動態路由:能夠匹配任何請求屬性; - 可以對路由指定 Predicate(斷言)和 Filter(過濾器); - 整合Hystrix的斷路器功能; - 整合 Spring Cloud 服務發現功能; - 易於編寫的 Predicate(斷言)和 Filter(過濾器); - 請求限流功能; - 支援路徑重寫。 ## 實戰 Gateway ### 引入 gateway 1. 新建`jlw-gateway`專案 2. 引入gateway依賴 ```xml org.springframework.cloud spring-cloud-starter-gateway ``` 3. 加入Eureka註冊中心 4. 開啟服務註冊和發現 ```xml # gateway 服務埠 server: port: 9000 spring: cloud: gateway: discovery: locator: # 啟用DiscoveryClient閘道器整合的標誌 enabled: true # 服務小寫匹配 lower-case-service-id: true ``` 配置完上面,Gateway 就可以自動根據服務發現為每個服務建立router了,然後將已服務名開頭的請求路徑轉發到對應的服務。 ![](https://img2020.cnblogs.com/blog/709068/202012/709068-20201214162422545-1539217629.gif) ### Gateway Actuator API pom中引入spring-boot-starter-actuator相關依賴,然後配置檔案新增如下程式碼,開啟gateway相關的端點。 ```xml management: endpoints: web: exposure: #應該包含的端點ID,全部:* include: 'gateway' ``` 重啟專案,訪問[http://localhost:9000/actuator/gateway/routes](http://localhost:9000/actuator/gateway/routes)就可以檢視到配置的路由資訊了 ![](https://img2020.cnblogs.com/blog/709068/202012/709068-20201214162433135-1081415787.png) 更多gateway路由資訊介面展示如下圖: ![](https://img2020.cnblogs.com/blog/709068/202012/709068-20201214162442979-1148502899.png) ### 相關概念 - Route(路由): 路由是構建閘道器的基本模組,它由ID,目標URI,一系列的斷言和過濾器組成,如果斷言為true則匹配該路由; - Predicate(斷言): 指的是Java 8 的 Function Predicate。 輸入型別是Spring框架中的ServerWebExchange。 這使開發人員可以匹配HTTP請求中的所有內容,例如請求頭或請求引數。如果請求與斷言相匹配,則進行路由; - Filter(過濾器): 指的是Spring框架中GatewayFilter的例項,使用過濾器,可以在請求被路由前後對請求進行修改。 ### 路由基本配置 Gateway 提供了兩種不同的方式用於配置路由:一種是通過yml檔案來配置,另一種是通過Java Bean來配置 ①**使用配置檔案** ```xml spring: cloud: gateway: routes: - id: eureka-provider uri: lb://eureka-provider predicates: - Path=/api/ep/** filters: - StripPrefix=2 ``` 欄位含義解釋: - id 我們自定義的路由 ID,保持唯一 - uri 目標服務地址,大部分場景我們是轉發到某個服務上,配置`lb://eureka-provider`意思是請求要轉發到註冊中心的`eureka-provider`服務上。 - predicates 路由條件,接受一個引數,返回一個布林結果決定是否匹配。Gateway 為我們內建了多種路由條件,包括 Path、Cookie、Param、Header、Before、After 等等,開箱即用,當然我們也可以自己實現 predicates - filters 過濾規則,當請求經過 predicate 匹配成功後,執行 filter,我們可以使用它修改請求和響應,示例表示目標服務收到的 path 將會忽略2位路徑path。 **②使用Java Bean配置** 配置`RouteLocator`物件,程式碼示例如下: ```java @Bean public RouteLocator customRoutes(RouteLocatorBuilder builder){ return builder.routes() // 請求閘道器路徑包含 /api/ec/** 的都會被路由到eureka-client .route("eureka-client",r->
r.path("/api/ec/**") .filters(f->f.stripPrefix(2)) .uri("lb://eureka-client")) // 可以配置多個route .route("eureka-client2",r->r.path("/api/ec2/**") .filters(f->f.stripPrefix(2)) .uri("lb://eureka-client")) .build(); } ``` 以上配置後,通過[http://localhost:9000/api/ec/sayHello](http://localhost:9000/api/ec/sayHello) 或者[http://localhost:9000/api/ec2/sayHello](http://localhost:9000/api/ec2/sayHello) 都會被路由到`eureka-client`服務。 ## 路由規則:Predicate Spring Cloud Gateway 內建了很多 Predicates 工廠(可以通過訪問路徑`/actuator/gateway/routepredicates`檢視),這些 Predicates 工廠通過不同的 HTTP 請求引數來匹配,多個 Predicates 工廠可以組合使用。 按照其功能可以大致分為以下幾種不同 Predicate ### 1.通過請求時間匹配路由 1. 在`指定時間之後`的請求會匹配該路由:AfterRoutePredicateFactory ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client predicates: - After=2019-10-10T00:00:00+08:00[Asia/Shanghai] ``` 2. 在`指定時間之前`的請求會匹配該路由:BeforeRoutePredicateFactory ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client predicates: - Before=2019-10-10T00:00:00+08:00[Asia/Shanghai] ``` 3. 在`指定時間區間`內的請求會匹配該路由:`BetweenRoutePredicateFactory` ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client predicates: - Between=2019-10-01T00:00:00+08:00[Asia/Shanghai], 2019-10-10T00:00:00+08:00[Asia/Shanghai] ``` ### 2.**通過Cookie匹配路由** Cookie Route Predicate 可以接收兩個引數,一個是 Cookie name,一個是正則表示式。示例如下: ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client predicates: - Cookie=name, jinglingwang.cn ``` ### 3.通過 Header匹配路由 Header Route Predicate 和 Cookie Route Predicate 一樣,也是接收 2 個引數,一個 header 中屬性名稱和一個正則表示式,示例如下: ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client predicates: - Header=name, jinglingwang.cn ``` ### 4.**通過 Host 匹配路由** 該模式接收一個引數:主機列表,示例如下: ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client predicates: - Host=**.jinglingwang.cn,**.jinglingwang.com ``` ### 5.通過 Request Method 匹配路由 可以通過是 POST、GET、PUT、DELETE 等不同的請求方式來進行路由,示例程式碼如下: ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client predicates: - Method=GET,POST ``` ### 6.**通過請求路徑匹配路由** ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client predicates: - Path=/api/rc/** ``` ### 7.通過請求引數匹配路由 該模式有兩個引數:一個必需的param和一個可選的regexp(Java正則表示式),示例如下 ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client predicates: - QUERY=name,jingling* ``` ### 8.通過指定遠端地址匹配路由 ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client predicates: - RemoteAddr=192.168.1.1/24 #192.168.1.1是IP 24是子網掩碼 ``` 如果請求的遠端地址是192.168.1.10,則此路由匹配。 ### 9.**通過權重來匹配路由** 該模式有兩個引數:group和Weight(一個int值),示例如下: ```java spring: cloud: gateway: routes: - id: weight_high uri: http://localhost:8201 predicates: - Weight=group1, 8 - id: weight_low uri: http://localhost:8202 predicates: - Weight=group1, 2 ``` 以上表示有80%的請求會被路由到localhost:8201,20%會被路由到localhost:8202 ## 閘道器過濾器 路由過濾器允許以某種方式修改傳入的HTTP請求或傳出的HTTP響應。路由過濾器只能指定路由進行使用。Spring Cloud Gateway 內建了多種路由過濾器,他們都由GatewayFilter的工廠類來產生。 1. **新增請求Header過濾器** ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client filters: - AddRequestHeader=source, jinglingwang.cn ``` 上面的示例會為所有匹配的請求向下遊請求時在Header中新增`source=jinglingwang.cn` 2. **新增請求引數過濾器** ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client filters: - AddRequestParameter=source, jinglingwang.cn ``` 上面的示例會把`source=jinglingwang.cn`新增到下游的請求引數中 3. **新增響應頭過濾器** ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client filters: - AddResponseHeader=source, jinglingwang.cn ``` 上面的示例會把`source=jinglingwang.cn`新增到所有匹配請求的下游響應頭中。 4. **剔除重複的響應頭過濾器** ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client filters: - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin ``` DedupeResponseHeader過濾器也可接收可選策略引數,可接收引數值包括:RETAIN_FIRST (預設值,保留第一個值), RETAIN_LAST(保留最後一個值), and RETAIN_UNIQUE(保留所有唯一值,以它們第一次出現的順序保留)。 5. **開啟Hystrix斷路器功能的過濾器** 要開啟斷路器功能,我們需要在pom.xml中新增Hystrix的相關依賴:spring-cloud-starter-netflix-hystrix 然後新增相關服務降級的處理類: ```java @RestController public class FallbackController{ @GetMapping("/fallback") public Object fallback() { Map result = new HashMap<>(3); result.put("data","jinglingwang.cn"); result.put("message","Get request fallback!"); result.put("code",500); return result; } } ``` 新增配置 ```java spring: cloud: gateway: routes: - id: eureka-provider uri: lb://eureka-provider predicates: - Path=/api/ep/** filters: - StripPrefix=2 - name: Hystrix args: name: fallbackcmd fallbackUri: forward:/fallback ``` 6. **啟用resilience4j斷路器的過濾器** 要啟用Spring Cloud斷路器過濾器,需要引入依賴`spring-cloud-starter-circuitbreaker-reactor-resilience4j` ```java spring: cloud: gateway: routes: - id: ribbon-client uri: lb://ribbon-client filters: - CircuitBreaker=myCircuitBreaker ``` 還可以接受一個可選的fallbackUri引數: ```java spring: cloud: gateway: routes: - id: eureka-provider uri: lb://eureka-provider predicates: - Path=/api/ep/** filters: - StripPrefix=2 - name: CircuitBreaker args: name: myCircuitBreaker fallbackUri: forward:/fallback ``` 關閉eureka-provider服務,訪問[http://localhost:9000/api/ep/hello](https://jinglingwang.cn/archives/gateway)介面,出現降級處理資訊 ![](https://img2020.cnblogs.com/blog/709068/202012/709068-20201214162519343-1526067935.png) 上面的配置也可以用JAVA Bean的方式配置: ```java @Bean public RouteLocator customRoutes(RouteLocatorBuilder builder){ return builder.routes() .route("eureka-provider", r -> r.path("/api/ep/**") .filters(f->f.stripPrefix(2).circuitBreaker(c->c.setName("myCircuitBreaker").setFallbackUri("forward:/fallback"))) .uri("lb://eureka-provider")) .build(); } ``` **6.1 根據狀態碼使斷路器跳閘** 根據返回的狀態碼,決定斷路器是否要跳閘 ```java spring: cloud: gateway: routes: - id: eureka-provider uri: lb://eureka-provider predicates: - Path=/api/ep/** filters: - StripPrefix=2 - name: CircuitBreaker args: name: myCircuitBreaker fallbackUri: forward:/fallback statusCodes: - 500 - 'NOT_FOUND' ``` 或者JAVA Bean配置: ```java @Bean public RouteLocator customRoutes(RouteLocatorBuilder builder){ return builder.routes() .route("eureka-provider", r -> r.path("/api/ep/**") .filters(f->f.stripPrefix(2).circuitBreaker(c->c.setName("myCircuitBreaker").setFallbackUri("forward:/fallback").addStatusCode("500"))) .uri("lb://eureka-provider")) .build(); } ``` 其中`NOT_FOUND`是HttpStatus列舉的String表示形式 7. **增加路徑的過濾器** ```java spring: cloud: gateway: routes: - id: eureka-provider uri: lb://eureka-provider predicates: - Path=/api/ep/** filters: - PrefixPath=/mypath ``` 把`/mypath`作為所有匹配請求路徑的字首 8. **去掉路徑字首的過濾器** ```java spring: cloud: gateway: routes: - id: eureka-provider uri: lb://eureka-provider predicates: - Path=/api/ep/** filters: - StripPrefix=2 ``` 以上配置會忽略兩位路徑path,當訪問閘道器API `/api/ep/hello` 時,向eureka-provider發起`/hello`請求 9. **用於限流的過濾器** RequestRateLimiter 過濾器可以用於限流,RateLimiter實現來確定是否允許繼續當前請求。 如果不是,則返回HTTP 429—太多請求(預設)的狀態。 該過濾器採用可選的`keyResolver`引數和速率限制器特定的引數,keyResolver是一個實現KeyResolver介面的bean。在配置中,使用SpEL按名稱引用bean。#{@myKeyResolver}是引用名為myKeyResolver的bean的SpEL表示式。 **使用 Redis RateLimiter** 1. 引入redis依賴,配置好redis。 ```xml org.springframework.boot
spring-boot-starter-data-redis-reactive
``` 2. 新增配置,使用的演算法是令牌桶演算法。 ```xml spring: cloud: gateway: routes: - id: eureka-provider uri: lb://eureka-provider predicates: - Path=/api/ep/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 #允許使用者每秒處理多少個請求,而不丟棄任何請求。這是令牌桶的填充速率 redis-rate-limiter.burstCapacity: 20 #一個使用者在一秒鐘內允許做的最大請求數。這是令牌桶可以容納的令牌數。將該值設定為零會阻止所有請求。 redis-rate-limiter.requestedTokens: 1 #一個請求花費多少令牌 ``` 通過在“replenishRate”和“burstCapacity”中設定相同的值來實現穩定的速率。通過將burstCapacity設定為高於replenishRate,可以允許臨時爆發。 3. 配置KeyResolver JAVA 程式碼: ```java @Configuration public class RedisRateLimiterConfig{ @Bean KeyResolver userKeyResolver() { // 根據請求引數中的phone進行限流 return exchange ->
Mono.just(exchange.getRequest().getQueryParams().getFirst("phone")); } @Bean @Primary public KeyResolver ipKeyResolver() { // 根據訪問IP進行限流 return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); } } ``` 4. 配置RequestRateLimiter ```java spring: cloud: gateway: routes: - id: eureka-provider uri: lb://eureka-provider predicates: - Path=/api/ep/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1 #允許使用者每秒處理多少個請求,而不丟棄任何請求。這是令牌桶的填充速率 redis-rate-limiter.burstCapacity: 2 #一個使用者在一秒鐘內允許做的最大請求數。這是令牌桶可以容納的令牌數。將該值設定為零會阻止所有請求。 redis-rate-limiter.requestedTokens: 1 #一個請求花費多少令牌 key-resolver: "#{@ipKeyResolver}" ``` 多次請求,會返回狀態碼為429的錯誤 ![](https://img2020.cnblogs.com/blog/709068/202012/709068-20201214162604965-1579441066.png) 10. **用於重定向的過濾器** ```java spring: cloud: gateway: routes: - id: eureka-provider uri: lb://eureka-provider predicates: - Path=/api/ep/** filters: - StripPrefix=2 - RedirectTo=302, https://jinglingwang.cn #302 重定向到https://jinglingwang.cn ``` 效果圖如下: ![](https://img2020.cnblogs.com/blog/709068/202012/709068-20201214162634057-2031996932.png) 11. **用於重試的過濾器** ```java spring: cloud: gateway: routes: - id: eureka-provider uri: lb://eureka-provider predicates: - Path=/api/ep/** filters: - StripPrefix=2 - name: Retry args: retries: 3 statuses: BAD_GATEWAY methods: GET,POST backoff: firstBackoff: 10ms maxBackoff: 50ms factor: 2 basedOnPreviousValue: false ``` 引數解釋: - retries:應該嘗試的重試次數。 - statuses:應該重試的HTTP狀態程式碼,HttpStatus。 - methods:應該重試的HTTP方法,HttpMethod。 - series:要重試的狀態碼,Series。 - exceptions:應該重試的引發異常的列表。 - backoff:為重試配置的指數補償 12. **用於限制請求大小的過濾器** ```java spring: cloud: gateway: routes: - id: eureka-provider uri: lb://eureka-provider predicates: - Path=/api/ep/** filters: - StripPrefix=2 - name: RequestSize args: maxSize: 5MB ``` 當請求大小大於允許的限制時,閘道器會限制請求到達下游服務。maxSize引數後跟一個可選的資料單位,如“KB節”或“MB”,預設是“B”。 上面的配置如果超過限制會出現以下提示: `errorMessage:Request size is larger than permissible limit. Request size is 6.0 MB where permissible limit is 5.0 MB` ## 配置http超時時間 我們可以為所有路由配置Http超時(響應和連線),並且為每個特定路由配置單獨的超時時間 **全域性的超時時間配置:** ```java spring: cloud: gateway: httpclient: connect-timeout: 1000 response-timeout: 5s ``` **特定路由配置超時時間** ```java spring: cloud: gateway: routes: - id: eureka-provider uri: lb://eureka-provider predicates: - Path=/api/ep/** metadata: response-timeout: 2000 connect-timeout: 1000 ``` 或者使用JAVA Bean的方式配置: ```java @Bean public RouteLocator customRoutes(RouteLocatorBuilder builder){ return builder.routes() .route("eureka-provider", r -> r.path("/api/ep/**") .filters(f->f.stripPrefix(2) .requestRateLimiter(rate->rate.setKeyResolver(ipKeyResolver)) .circuitBreaker(c->c.setName("myCircuitBreaker").setFallbackUri("forward:/fallback").addStatusCode("500").addStatusCode("NOT_FOUND"))) .uri("lb://eureka-provider") .metadata(RESPONSE_TIMEOUT_ATTR, 200) .metadata(CONNECT_TIMEOUT_ATTR, 200)) .build(); } ``` ## 跨域配置 ```java spring: cloud: gateway: globalcors: # 全域性跨域配置 cors-configurations: '[/**]': allowedOrigins: "jinglingwang.cn" allowedMethods: - GET add-to-simple-url-handler-mapping: true ``` 上面的配置,對於所有Get請求,允許來自`jinglingwang.cn`的跨域請求。 ## 自定義 Route Predicate Factories 通過一個名為`AbstractRoutePredicateFactory`的抽象類來進行擴充套件,示例程式碼: ```java public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory { public MyRoutePredicateFactory(){ super(Config.class); } @Override public Predicate apply(Config config){ return new GatewayPredicate() { @Override public boolean test(ServerWebExchange exchange) { String host = exchange.getRequest().getHeaders().getFirst("Host"); return "jinglingwang.cn".equalsIgnoreCase(host); } @Override public String toString() { return String.format("host: name=%s ", config.host); } }; } public static class Config { //自定義過濾器的配置屬性 @NotEmpty private String host; } } ``` ## 自定義 GatewayFilter Factories 要寫GatewayFilter,必須實現GatewayFilterFactory,可以通過擴充套件一個名為AbstractGatewayFilterFactory的抽象類來進行。 ```java @Component public class AddHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory{ public AddHeaderGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config){ return (exchange, chain) -> { // 如果要構建“前置”過濾器,則需要在呼叫chain.filter之前處理 ServerHttpRequest request = exchange.getRequest().mutate() .header("source", "jinglingwang.cn").build(); //使用構建器來處理請求 return chain.filter(exchange.mutate().request(request).build()); }; } public static class Config { //Put the configuration properties for your filter here } } ``` ### 配置屬性 要檢視所有與 Spring Cloud 閘道器相關的配置屬性列表,請參見[附錄](https://docs.spring.io/spring-cloud-gateway/docs/3.0.0-M3/reference/html/append