1.1 SpringCloud Gateway 簡介
SpringCloud Gateway 是 Spring Cloud 的一個全新項目,該項目是基於 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開發的網關,它旨在為微服務架構提供一種簡單有效的統一的 API 路由管理方式。
SpringCloud Gateway 作為 Spring Cloud 生態系統中的網關,目標是替代 Zuul,在Spring Cloud 2.0以上版本中,沒有對新版本的Zuul 2.0以上最新高性能版本進行集成,仍然還是使用的Zuul 2.0之前的非Reactor模式的老版本。而為了提升網關的性能,SpringCloud Gateway是基於WebFlux框架實現的,而WebFlux框架底層則使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway 的目標,不僅提供統一的路由方式,並且基於 Filter 鏈的方式提供了網關基本的功能,例如:安全,監控/指標,和限流。
提前聲明:Spring Cloud Gateway 底層使用了高性能的通信框架Netty。
1.2 SpringCloud Gateway 特征
SpringCloud官方,對SpringCloud Gateway 特征介紹如下:
(1)基於 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
(2)集成 Hystrix 斷路器
(3)集成 Spring Cloud DiscoveryClient
(4)Predicates 和 Filters 作用於特定路由,易於編寫的 Predicates 和 Filters
(5)具備一些網關的高級功能:動態路由、限流、路徑重寫
從以上的特征來說,和Zuul的特征差別不大。SpringCloud Gateway和Zuul主要的區別,還是在底層的通信框架上。
簡單說明一下上文中的三個術語:
(1)Filter(過濾器):
和Zuul的過濾器在概念上類似,可以使用它攔截和修改請求,並且對上游的響應,進行二次處理。過濾器為org.springframework.cloud.gateway.filter.GatewayFilter類的實例。
(2)Route(路由):
網關配置的基本組成模塊,和Zuul的路由配置模塊類似。一個Route模塊由一個 ID,一個目標 URI,一組斷言和一組過濾器定義。如果斷言為真,則路由匹配,目標URI會被訪問。
(3)Predicate(斷言):
這是一個 Java 8 的 Predicate,可以使用它來匹配來自 HTTP 請求的任何內容,例如 headers 或參數。斷言的輸入類型是一個 ServerWebExchange。
1.3 SpringCloud Gateway和架構
Spring在2017年下半年迎來了Webflux,Webflux的出現填補了Spring在響應式編程上的空白,Webflux的響應式編程不僅僅是編程風格的改變,而且對於一系列的著名框架,都提供了響應式訪問的開發包,比如Netty、Redis等等。
SpringCloud Gateway 使用的Webflux中的reactor-netty響應式編程組件,底層使用了Netty通訊框架。
1.3.1 SpringCloud Zuul的IO模型
Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是傳統的Servlet IO處理模型。
大家知道,servlet由servlet container進行生命周期管理。container啟動時構造servlet對象並調用servlet init()進行初始化;container關閉時調用servlet destory()銷毀servlet;container運行時接受請求,並為每個請求分配一個線程(一般從線程池中獲取空閑線程)然后調用service()。
弊端:servlet是一個簡單的網絡IO模型,當請求進入servlet container時,servlet container就會為其綁定一個線程,在並發不高的場景下這種模型是適用的,但是一旦並發上升,線程數量就會上漲,而線程資源代價是昂貴的(上線文切換,內存消耗大)嚴重影響請求的處理時間。在一些簡單的業務場景下,不希望為每個request分配一個線程,只需要1個或幾個線程就能應對極大並發的請求,這種業務場景下servlet模型沒有優勢。
所以Springcloud Zuul 是基於servlet之上的一個阻塞式處理模型,即spring實現了處理所有request請求的一個servlet(DispatcherServlet),並由該servlet阻塞式處理處理。所以Springcloud Zuul無法擺脫servlet模型的弊端。雖然Zuul 2.0開始,使用了Netty,並且已經有了大規模Zuul 2.0集群部署的成熟案例,但是,Springcloud官方已經沒有集成改版本的計划了。
1.3.2 Webflux 服務器
Webflux模式替換了舊的Servlet線程模型。用少量的線程處理request和response io操作,這些線程稱為Loop線程,而業務交給響應式編程框架處理,響應式編程是非常靈活的,用戶可以將業務中阻塞的操作提交到響應式框架的work線程中執行,而不阻塞的操作依然可以在Loop線程中進行處理,大大提高了Loop線程的利用率。官方結構圖:

Webflux雖然可以兼容多個底層的通信框架,但是一般情況下,底層使用的還是Netty,畢竟,Netty是目前業界認可的最高性能的通信框架。而Webflux的Loop線程,正好就是著名的Reactor 模式IO處理模型的Reactor線程,如果使用的是高性能的通信框架Netty,這就是Netty的EventLoop線程。
關於Reactor線程模型,和Netty通信框架的知識,是Java程序員的重要、必備的內功,個中的原理,具體請參見尼恩編著的《Netty、Zookeeper、Redis高並發實戰》一書,這里不做過多的贅述。
1.3.3 Spring Cloud Gateway的處理流程
客戶端向 Spring Cloud Gateway 發出請求。然后在 Gateway Handler Mapping 中找到與請求相匹配的路由,將其發送到 Gateway Web Handler。Handler 再通過指定的過濾器鏈來將請求發送到我們實際的服務執行業務邏輯,然后返回。過濾器之間用虛線分開是因為過濾器可能會在發送代理請求之前(“pre”)或之后(“post”)執行業務邏輯。
2 路由配置方式
2.1 基礎URI路由配置方式
如果請求的目標地址,是單個的URI資源路徑,配置文件示例如下:
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: url-proxy-1
uri: https://blog.csdn.net
predicates:
-Path=/csdn
各字段含義如下:
id:我們自定義的路由 ID,保持唯一
uri:目標服務地址
predicates:路由條件,Predicate 接受一個輸入參數,返回一個布爾值結果。該接口包含多種默認方法來將 Predicate 組合成其他復雜的邏輯(比如:與,或,非)。
上面這段配置的意思是,配置了一個 id 為 url-proxy-1的URI代理規則,路由的規則為:
當訪問地址http://localhost:8080/csdn/1.jsp時,
會路由到上游地址https://blog.csdn.net/1.jsp。
2.2 基於代碼的路由配置方式
轉發功能同樣可以通過代碼來實現,我們可以在啟動類 GateWayApplication 中添加方法 customRouteLocator() 來定制轉發規則。
package com.springcloud.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/csdn")
.uri("https://blog.csdn.net"))
.build();
}
}
我們在yaml配置文件中注銷掉相關路由的配置,重啟服務,訪問鏈接:http://localhost:8080/ csdn, 可以看到和上面一樣的頁面,證明我們測試成功。
上面兩個示例中 uri 都是指向了我的CSDN博客,在實際項目使用中可以將 uri 指向對外提供服務的項目地址,統一對外輸出接口。
2.3 和注冊中心相結合的路由配置方式
在uri的schema協議部分為自定義的lb:類型,表示從微服務注冊中心(如Eureka)訂閱服務,並且進行服務的路由。
一個典型的示例如下:
server:
port: 8084
spring:
cloud:
gateway:
routes:
-id: seckill-provider-route
uri: lb://seckill-provider
predicates:
- Path=/seckill-provider/**
-id: message-provider-route
uri: lb://message-provider
predicates:
-Path=/message-provider/**
application:
name: cloud-gateway
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:8888/eureka/
注冊中心相結合的路由配置方式,與單個URI的路由配置,區別其實很小,僅僅在於URI的schema協議不同。單個URI的地址的schema協議,一般為http或者https協議。
3 路由 匹配規則
Spring Cloud Gateway 的功能很強大,我們僅僅通過 Predicates 的設計就可以看出來,前面我們只是使用了 predicates 進行了簡單的條件匹配,其實 Spring Cloud Gataway 幫我們內置了很多 Predicates 功能。
Spring Cloud Gateway 是通過 Spring WebFlux 的 HandlerMapping 做為底層支持來匹配到轉發路由,Spring Cloud Gateway 內置了很多 Predicates 工廠,這些 Predicates 工廠通過不同的 HTTP 請求參數來匹配,多個 Predicates 工廠可以組合使用。

gateWay的主要功能之一是轉發請求,轉發規則的定義主要包含三個部分
| Route(路由) | 路由是網關的基本單元,由ID、URI、一組Predicate、一組Filter組成,根據Predicate進行匹配轉發。 | |
| Predicate(謂語、斷言) | 路由轉發的判斷條件,目前SpringCloud Gateway支持多種方式,常見如:Path、Query、Method、Header等,寫法必須遵循 key=vlue的形式 | |
| Filter(過濾器) | 過濾器是路由轉發請求時所經過的過濾邏輯,可用於修改請求、響應內容 |
其中Route和Predicate必須同時申明
例子:
//通過配置文件配置
spring:
cloud:
gateway:
routes:
- id: gate_route
uri: http://localhost:9023
predicates:
## 當請求的路徑為gate、rule開頭的時,轉發到http://localhost:9023服務器上
- Path=/gate/**,/rule/**
### 請求路徑前加上/app
filters:
- PrefixPath=/app
3.1 Predicate 斷言條件(轉發規則)介紹
Predicate 來源於 Java 8,是 Java 8 中引入的一個函數,Predicate 接受一個輸入參數,返回一個布爾值結果。該接口包含多種默認方法來將 Predicate 組合成其他復雜的邏輯(比如:與,或,非)。可以用於接口請求參數校驗、判斷新老數據是否有變化需要進行更新操作。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性實現了各種路由匹配規則,有通過 Header、請求參數等不同的條件來進行作為條件匹配到對應的路由。網上有一張圖總結了 Spring Cloud 內置的幾種 Predicate 的實現。
[
說白了 Predicate 就是為了實現一組匹配規則,方便讓請求過來找到對應的 Route 進行處理,接下來我們接下 Spring Cloud GateWay 內置幾種 Predicate 的使用。
- 轉發規則(predicates),假設 轉發uri都設定為http://localhost:9023
| 規則 | 實例 | 說明 |
|---|---|---|
| Path | - Path=/gate/,/rule/ | ## 當請求的路徑為gate、rule開頭的時,轉發到http://localhost:9023服務器上 |
| Before | - Before=2017-01-20T17:42:47.789-07:00[America/Denver] | 在某個時間之前的請求才會被轉發到 http://localhost:9023服務器上 |
| After | - After=2017-01-20T17:42:47.789-07:00[America/Denver] | 在某個時間之后的請求才會被轉發 |
| Between | - Between=2017-01-20T17:42:47.789-07:00[America/Denver],2017-01-21T17:42:47.789-07:00[America/Denver] | 在某個時間段之間的才會被轉發 |
| Cookie | - Cookie=chocolate, ch.p | 名為chocolate的表單或者滿足正則ch.p的表單才會被匹配到進行請求轉發 |
| Header | - Header=X-Request-Id, \d+ | 攜帶參數X-Request-Id或者滿足\d+的請求頭才會匹配 |
| Host | - Host=www.hd123.com | 當主機名為www.hd123.com的時候直接轉發到http://localhost:9023服務器上 |
| Method | - Method=GET | 只有GET方法才會匹配轉發請求,還可以限定POST、PUT等請求方式 |
1.5.2 通過請求參數匹配
Query Route Predicate 支持傳入兩個參數,一個是屬性名一個為屬性值,屬性值可以是正則表達式。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
-Query=smile
這樣配置,只要請求中包含 smile 屬性的參數即可匹配路由。
使用 curl 測試,命令行輸入:
curl localhost:8080?smile=x&id=2
經過測試發現只要請求匯總帶有 smile 參數即會匹配路由,不帶 smile 參數則不會匹配。
還可以將 Query 的值以鍵值對的方式進行配置,這樣在請求過來時會對屬性值和正則進行匹配,匹配上才會走路由。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
-Query=keep, pu.
這樣只要當請求中包含 keep 屬性並且參數值是以 pu 開頭的長度為三位的字符串才會進行匹配和路由。
使用 curl 測試,命令行輸入:
curl localhost:8080?keep=pub
測試可以返回頁面代碼,將 keep 的屬性值改為 pubx 再次訪問就會報 404,證明路由需要匹配正則表達式才會進行路由。
1.5.3 通過 Header 屬性匹配
Header Route Predicate 和 Cookie Route Predicate 一樣,也是接收 2 個參數,一個 header 中屬性名稱和一個正則表達式,這個屬性值和正則表達式匹配則執行。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Header=X-Request-Id, \d+
使用 curl 測試,命令行輸入:
curl http://localhost:8080 -H "X-Request-Id:88"
則返回頁面代碼證明匹配成功。將參數-H "X-Request-Id:88"改為-H "X-Request-Id:spring"再次執行時返回404證明沒有匹配。
1.5.4 通過 Cookie 匹配
Cookie Route Predicate 可以接收兩個參數,一個是 Cookie name ,一個是正則表達式,路由規則會通過獲取對應的 Cookie name 值和正則表達式去匹配,如果匹配上就會執行路由,如果沒有匹配上則不執行。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Cookie=sessionId, test
使用 curl 測試,命令行輸入:
curl http://localhost:8080 --cookie "sessionId=test"
則會返回頁面代碼,如果去掉--cookie "sessionId=test",后台匯報 404 錯誤。
1.5.5 通過 Host 匹配
Host Route Predicate 接收一組參數,一組匹配的域名列表,這個模板是一個 ant 分隔的模板,用.號作為分隔符。它通過參數中的主機地址作為匹配規則。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Host=**.baidu.com
使用 curl 測試,命令行輸入:
curl http://localhost:8080 -H "Host: www.baidu.com"
curl http://localhost:8080 -H "Host: md.baidu.com"
經測試以上兩種 host 均可匹配到 host_route 路由,去掉 host 參數則會報 404 錯誤。
1.5.6 通過請求方式匹配
可以通過是 POST、GET、PUT、DELETE 等不同的請求方式來進行路由。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Method=GET
使用 curl 測試,命令行輸入:
# curl 默認是以 GET 的方式去請求
測試返回頁面代碼,證明匹配到路由,我們再以 POST 的方式請求測試。
# curl 默認是以 GET 的方式去請求
curl -X POST http://localhost:8080
返回 404 沒有找到,證明沒有匹配上路由
1.5.7 通過請求路徑匹配
Path Route Predicate 接收一個匹配路徑的參數來判斷是否走路由。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: gateway-service
uri: http://ityouknow.com
order: 0
predicates:
-Path=/foo/{segment}
如果請求路徑符合要求,則此路由將匹配,例如:/foo/1 或者 /foo/bar。
使用 curl 測試,命令行輸入:
curl http://localhost:8080/foo/1
curl http://localhost:8080/foo/xx
curl http://localhost:8080/boo/xx
經過測試第一和第二條命令可以正常獲取到頁面返回值,最后一個命令報404,證明路由是通過指定路由來匹配。
1.5.8 通過請求 ip 地址進行匹配
Predicate 也支持通過設置某個 ip 區間號段的請求才會路由,RemoteAddr Route Predicate 接受 cidr 符號(IPv4 或 IPv6 )字符串的列表(最小大小為1),例如 192.168.0.1/16 (其中 192.168.0.1 是 IP 地址,16 是子網掩碼)。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- RemoteAddr=192.168.1.1/24
可以將此地址設置為本機的 ip 地址進行測試。
curl localhost:8080
如果請求的遠程地址是 192.168.1.10,則此路由將匹配。
1.5.10 組合使用
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Host=**.foo.org
- Path=/headers
- Method=GET
- Header=X-Request-Id, \d+
- Query=foo, ba.
- Query=baz
- Cookie=chocolate, ch.p
各種 Predicates 同時存在於同一個路由時,請求必須同時滿足所有的條件才被這個路由匹配。
一個請求滿足多個路由的斷言條件時,請求只會被首個成功匹配的路由轉發
3.2 過濾器規則(Filter)
過濾器規則(Filter)
| 過濾規則 | 實例 | 說明 |
|---|---|---|
| PrefixPath | - PrefixPath=/app | 在請求路徑前加上app |
| RewritePath | - RewritePath=/test, /app/test | 訪問localhost:9022/test,請求會轉發到localhost:8001/app/test |
| SetPath | SetPath=/app/{path} | 通過模板設置路徑,轉發的規則時會在路徑前增加app,{path}表示原請求路徑 |
| RedirectTo | 重定向 | |
| RemoveRequestHeader | 去掉某個請求頭信息 |
注:當配置多個filter時,優先定義的會被調用,剩余的filter將不會生效
PrefixPath
對所有的請求路徑添加前綴:
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- PrefixPath=/mypath
訪問/hello的請求被發送到https://example.org/mypath/hello。
RedirectTo
重定向,配置包含重定向的返回碼和地址:
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- RedirectTo=302, https://acme.org
RemoveRequestHeader
去掉某個請求頭信息:
spring:
cloud:
gateway:
routes:
- id: removerequestheader_route
uri: https://example.org
filters:
- RemoveRequestHeader=X-Request-Foo
去掉請求頭信息 X-Request-Foo
RemoveResponseHeader
去掉某個回執頭信息:
spring:
cloud:
gateway:
routes:
- id: removerequestheader_route
uri: https://example.org
filters:
- RemoveResponseHeader=X-Request-Foo
RemoveRequestParameter
去掉某個請求參數信息:
spring:
cloud:
gateway:
routes:
- id: removerequestparameter_route
uri: https://example.org
filters:
- RemoveRequestParameter=red
RewritePath
改寫路徑:
spring:
cloud:
gateway:
routes:
- id: rewrite_filter
uri: http://localhost:8081
predicates:
- Path=/test/**
filters:
- RewritePath=/where(?<segment>/?.*), /test(?<segment>/?.*)
/where/... 改成 test/...
使用代碼改下路徑
RouteLocatorBuilder.Builder builder = routeLocatorBuilder.routes();
builder
.route("path_rote_at_guigu", r -> r.path("/guonei")
.uri("http://news.baidu.com/guonei"))
.route("csdn_route", r -> r.path("/csdn")
.uri("https://blog.csdn.net"))
.route("blog3_rewrite_filter", r -> r.path("/blog3/**")
.filters(f -> f.rewritePath("/blog3/(?<segment>.*)", "/$\\{segment}"))
.uri("https://blog.csdn.net"))
.route("rewritepath_route", r -> r.path("/baidu/**")
.filters(f -> f.rewritePath("/baidu/(?<segment>.*)", "/$\\{segment}"))
.uri("http://www.baidu.com"))
.build();
SetPath
設置請求路徑,與RewritePath類似。
spring:
cloud:
gateway:
routes:
- id: setpath_route
uri: https://example.org
predicates:
- Path=/red/{segment}
filters:
- SetPath=/{segment}
如/red/blue的請求被轉發到/blue。
SetRequestHeader
設置請求頭信息。
spring:
cloud:
gateway:
routes:
- id: setrequestheader_route
uri: https://example.org
filters:
- SetRequestHeader=X-Request-Red, Blue
SetStatus
設置回執狀態碼。
spring:
cloud:
gateway:
routes:
- id: setstatusint_route
uri: https://example.org
filters:
- SetStatus=401
StripPrefix
跳過指定路徑。
spring:
cloud:
gateway:
routes:
- id: nameRoot
uri: https://nameservice
predicates:
- Path=/name/**
filters:
- StripPrefix=2
請求/name/blue/red會轉發到/red。
RequestSize
請求大小。
spring:
cloud:
gateway:
routes:
- id: request_size_route
uri: http://localhost:8080/upload
predicates:
- Path=/upload
filters:
- name: RequestSize
args:
maxSize: 5000000
超過5M的請求會返回413錯誤。
Default-filters
對所有請求添加過濾器。
spring:
cloud:
gateway:
default-filters:
- AddResponseHeader=X-Response-Default-Red, Default-Blue
- PrefixPath=/httpbin
3.3 通過代碼進行配置
通過代碼進行配置,將路由規則設置為一個Bean即可:
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/get")
.uri("http://httpbin.org"))
.route("host_route", r -> r.host("*.myhost.org")
.uri("http://httpbin.org"))
.route("rewrite_route", r -> r.host("*.rewrite.org")
.filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
.uri("http://httpbin.org"))
.route("hystrix_route", r -> r.host("*.hystrix.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd")))
.uri("http://httpbin.org"))
.route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
.filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
.uri("http://httpbin.org"))
.route("limit_route", r -> r
.host("*.limited.org").and().path("/anything/**")
.filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
.uri("http://httpbin.org"))
.build();
}
3.2 實現熔斷降級
為什么要實現熔斷降級?
在分布式系統中,網關作為流量的入口,因此會有大量的請求進入網關,向其他服務發起調用,其他服務不可避免的會出現調用失敗(超時、異常),失敗時不能讓請求堆積在網關上,需要快速失敗並返回給客戶端,想要實現這個要求,就必須在網關上做熔斷、降級操作。
為什么在網關上請求失敗需要快速返回給客戶端?
因為當一個客戶端請求發生故障的時候,這個請求會一直堆積在網關上,當然只有一個這種請求,網關肯定沒有問題(如果一個請求就能造成整個系統癱瘓,那這個系統可以下架了),但是網關上堆積多了就會給網關乃至整個服務都造成巨大的壓力,甚至整個服務宕掉。因此要對一些服務和頁面進行有策略的降級,以此緩解服務器資源的的壓力,以保證核心業務的正常運行,同時也保持了客戶和大部分客戶的得到正確的相應,所以需要網關上請求失敗需要快速返回給客戶端。
server.port: 8082
spring:
application:
name: gateway
redis:
host: localhost
port: 6379
password: 123456
cloud:
gateway:
routes:
- id: rateLimit_route
uri: http://localhost:8000
order: 0
predicates:
- Path=/test/**
filters:
- StripPrefix=1
- name: Hystrix
args:
name: fallbackCmdA
fallbackUri: forward:/fallbackA
hystrix.command.fallbackCmdA.execution.isolation.thread.timeoutInMilliseconds: 5000
這里的配置,使用了兩個過濾器:
(1)過濾器StripPrefix,作用是去掉請求路徑的最前面n個部分截取掉。
StripPrefix=1就代表截取路徑的個數為1,比如前端過來請求/test/good/1/view,匹配成功后,路由到后端的請求路徑就會變成http://localhost:8888/good/1/view。
(2)過濾器Hystrix,作用是通過Hystrix進行熔斷降級
當上游的請求,進入了Hystrix熔斷降級機制時,就會調用fallbackUri配置的降級地址。需要注意的是,還需要單獨設置Hystrix的commandKey的超時時間
fallbackUri配置的降級地址的代碼如下:
package org.gateway.controller;
import org.gateway.response.Response;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FallbackController {
@GetMapping("/fallbackA")
public Response fallbackA() {
Response response = new Response();
response.setCode("100");
response.setMessage("服務暫時不可用");
return response;
}
}
4 高級配置
4.1 分布式限流
從某種意義上講,令牌桶算法是對漏桶算法的一種改進,桶算法能夠限制請求調用的速率,而令牌桶算法能夠在限制調用的平均速率的同時還允許一定程度的突發調用。在令牌桶算法中,存在一個桶,用來存放固定數量的令牌。算法中存在一種機制,以一定的速率往桶中放令牌。每次請求調用需要先獲取令牌,只有拿到令牌,才有機會繼續執行,否則選擇選擇等待可用的令牌、或者直接拒絕。放令牌這個動作是持續不斷的進行,如果桶中令牌數達到上限,就丟棄令牌,所以就存在這種情況,桶中一直有大量的可用令牌,這時進來的請求就可以直接拿到令牌執行,比如設置qps為100,那么限流器初始化完成一秒后,桶中就已經有100個令牌了,這時服務還沒完全啟動好,等啟動完成對外提供服務時,該限流器可以抵擋瞬時的100個請求。所以,只有桶中沒有令牌時,請求才會進行等待,最后相當於以一定的速率執行。
在Spring Cloud Gateway中,有Filter過濾器,因此可以在“pre”類型的Filter中自行實現上述三種過濾器。但是限流作為網關最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory這個類,適用在Redis內的通過執行Lua腳本實現了令牌桶的方式。具體實現邏輯在RequestRateLimiterGatewayFilterFactory類中,lua腳本在如下圖所示的文件夾中:
首先在工程的pom文件中引入gateway的起步依賴和redis的reactive依賴,代碼如下:
配置如下:
server:
port: 8081
spring:
cloud:
gateway:
routes:
- id: limit_route
uri: http://httpbin.org:80/get
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@userKeyResolver}'
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 3
application:
name: cloud-gateway
redis:
host: localhost
port: 6379
database: 0
在上面的配置文件,指定程序的端口為8081,配置了 redis的信息,並配置了RequestRateLimiter的限流過濾器,該過濾器需要配置三個參數:
-
burstCapacity,令牌桶總容量。
-
replenishRate,令牌桶每秒填充平均速率。
-
key-resolver,用於限流的鍵的解析器的 Bean 對象的名字。它使用 SpEL 表達式根據#{@beanName}從 Spring 容器中獲取 Bean 對象。
這里根據用戶ID限流,請求路徑中必須攜帶userId參數
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
KeyResolver需要實現resolve方法,比如根據userid進行限流,則需要用userid去判斷。實現完KeyResolver之后,需要將這個類的Bean注冊到Ioc容器中。
如果需要根據IP限流,定義的獲取限流Key的bean為:
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
通過exchange對象可以獲取到請求信息,這邊用了HostName,如果你想根據用戶來做限流的話這邊可以獲取當前請求的用戶ID或者用戶名就可以了,比如:
如果需要根據接口的URI進行限流,則需要獲取請求地址的uri作為限流key,定義的Bean對象為:
@Bean
KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
通過exchange對象可以獲取到請求信息,這邊用了HostName,如果你想根據用戶來做限流的話這邊可以獲取當前請求的用戶ID或者用戶名就可以了,比如:
如果需要根據接口的URI進行限流,則需要獲取請求地址的uri作為限流key,定義的Bean對象為:
@Bean
KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
4.2 健康檢查配置
admin-client、actuator健康檢查配置,為之后的功能提供支持,此部分比較簡單,不再贅述,加入以下maven依賴和配置
maven依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件
spring:
application:
name: mas-cloud-gateway
boot:
admin:
client:
### 本地搭建的admin-server
url: http://localhost:8011
eureka:
client:
registerWithEureka: true
fetchRegistry: true
healthcheck:
enabled: true
serviceUrl:
defaultZone: http://localhost:6887/eureka/
enabled: true
feign:
sentinel:
enabled: true
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: ALWAYS
若轉發的目標地址為微服務中組件,不為具體ip:port形式的,應寫成lb://mas-openapi-service形式,目標地址會從注冊中心直接拉取
4.3 統一配置跨域請求:
現在的請求通過經過gateWay網關時,需要在網關統一配置跨域請求,需求所有請求通過
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowed-origins: "*"
allowed-headers: "*"
allow-credentials: true
allowed-methods:
- GET
- POST
- DELETE
- PUT
- OPTION
5 整合Nacos
maven依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>nacos_gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>nacos_gateway</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos dicovery-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>0.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
需要注意在Gateway服務中的pom.xml文件中不要存在這個jar
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
否則調用接口時會報以下錯誤因為gateway使用的是webflux,默認使用netty,所以從依賴中排除 tomcat相關的依賴
java.lang.ClassCastException: org.springframework.core.io.buffer.DefaultDataBufferFactory cannot be cast to org.springframework.core.io.buffer.NettyDataBufferFactory
at org.springframework.cloud.gateway.filter.NettyWriteResponseFilter.lambda$filter$1(NettyWriteResponseFilter.java:82) ~[spring-cloud-gateway-core-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:44) [reactor-core-3.2.12.RELEASE.jar:3.2.12.RELEASE]
錯誤2 是由於 spring-boot-starter-web 引起
服務發現配置:從Nacos獲取微服務提供者清單
server:
port: 8087
spring:
application:
name: nacos_gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
enabled: true #表明gateway開啟服務注冊和發現的功能,並且spring cloud gateway自動根據服務發現為每一個服務創建了一個router,這個router將以服務名開頭的請求路徑轉發到對應的服務
lower-case-service-id: true #是將請求路徑上的服務名配置為小寫(因為服務注冊的時候,向注冊中心注冊時將服務名轉成大寫的了
routes:
-id: apiuser
#
uri: lb://nacos-consumer-user
predicates:
# http://localhost:6601/user/user/users/2, 必須加上StripPrefix=1,否則訪問服務時會帶上user
- Path=/user/** # 轉發該路徑
#以下是配置例子
# - id: 163 #網關路由到網易官網
# uri: http://www.163.com/
# predicates:
- Path=/163/**
# - id: ORDER-SERVICE #網關路由到訂單服務order-service
# uri: lb://ORDER-SERVICE
# predicates:
# - Path=/ORDER-SERVICE/**
# - id: USER-SERVICE #網關路由到用戶服務user-service
# uri: lb://USER-SERVICE
# predicates:
# - Pach=/USER-SERVICE/**
nacos實現動態配置
使用nacos實現動態路由,以上兩種方式都是實現的靜態配置路徑,只能應對部分場景,接下來配置nacos實現動態配置以及配置的存儲,由於gateWay並沒有適配nacos,需要自定義監聽器:
@Component
@Slf4j
public class NacosDynamicRouteService implements ApplicationEventPublisherAware {
private String dataId = "gateway-router";
private String group = "DEFAULT_GROUP";
@Value("${spring.cloud.nacos.config.server-addr}")
private String serverAddr;
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher applicationEventPublisher;
private static final List<String> ROUTE_LIST = new ArrayList<>();
@PostConstruct
public void dynamicRouteByNacosListener() {
try {
ConfigService configService = NacosFactory.createConfigService(serverAddr);
configService.getConfig(dataId, group, 5000);
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
clearRoute();
try {
if (StringUtil.isNullOrEmpty(configInfo)) {//配置被刪除
return;
}
List<RouteDefinition> gatewayRouteDefinitions = JSONObject.parseArray(configInfo, RouteDefinition.class);
for (RouteDefinition routeDefinition : gatewayRouteDefinitions) {
addRoute(routeDefinition);
}
publish();
} catch (Exception e) {
log.error("receiveConfigInfo error" + e);
}
}
@Override
public Executor getExecutor() {
return null;
}
});
} catch (NacosException e) {
log.error("dynamicRouteByNacosListener error" + e);
}
}
private void clearRoute() {
for (String id : ROUTE_LIST) {
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
}
ROUTE_LIST.clear();
}
private void addRoute(RouteDefinition definition) {
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
ROUTE_LIST.add(definition.getId());
} catch (Exception e) {
log.error("addRoute error" + e);
}
}
private void publish() {
this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this.routeDefinitionWriter));
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
在nacos中增加一個規則:
[{
"filters": [],
"id": "baidu_route",
"order": 0,
"predicates": [{
"args": {
"pattern": "/baidu"
},
"name": "Path"
}],
"uri": "https://www.baidu.com"
}]
訪問網關的路由規則,能看到剛剛加入的規則,訪問http://localhost:9022/baidu時請求直接被轉發到百度的首頁了。

6 整合Swagger聚合微服務系統API文檔
有關源碼 具體請參見 瘋狂創客圈的 crazy-springcloud 腳手架
maven依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud-center-alibaba</artifactId>
<groupId>com.crazymaker.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.crazymaker.springcloud</groupId>
<artifactId>springcloud-gateway-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<name>springcloud-gateway-demo</name>
<packaging>jar</packaging>
<dependencies>
<!--gateway 網關依賴,內置webflux 依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--新增sentinel-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
</dependency>
<!-- nacos服務注冊發現依賴-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.version}</version>
</dependency>
<!-- nacos配置服務依賴-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-common</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>${swagger-ui.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.crazymaker.cloud.nacos.demo.gateway.starter.GatewayProviderApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4.1</version>
<configuration>
<descriptors>
<descriptor>src/main/assembly/assembly.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 添加docker-maven插件 -->
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.1.1</version>
<configuration>
<imageName>dockerlocal:5000/${project.artifactId}:${project.version}</imageName>
<baseImage>dockerlocal:5000/java</baseImage>
<entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]</entryPoint>
<dockerDirectory>docker</dockerDirectory>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</build>
</project>
配置文件
package com.crazymaker.cloud.nacos.demo.gateway.config;
import lombok.AllArgsConstructor;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassName SwaggerProvider
* @PackageName com.ruoyi.gateway.config
* @Description
* @Author daiz
* @Date 2019/8/16 10:04
* @Version 1.0
*/
@Component
@Primary
@AllArgsConstructor
public class SwaggerConfig implements SwaggerResourcesProvider
{
public static final String API_URI = "/v2/api-docs";
private final RouteLocator routeLocator;
private final GatewayProperties gatewayProperties;
@Override
public List<SwaggerResource> get()
{
/**
* 網關應用名稱,不需要在網關的swagger 上展示
*/
String appName = "springcloud-gateway";
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
// 取出gateway的route
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
// 結合配置的route-路徑(Path),和route過濾,只獲取有效的route節點
// 打開下面注釋可以自動掃描接入gateway的服務,為了演示,只掃描system
// gatewayProperties.getRoutes().stream().filter(routeDefinition ->
// routes.contains(routeDefinition.getId()))
gatewayProperties.getRoutes().stream()
.filter(route -> route.getUri().getHost() != null)
.filter(route -> !appName.equals(route.getUri().getHost()))
.forEach(routeDefinition -> routeDefinition.getPredicates().stream()
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
.forEach(predicateDefinition -> resources
.add(swaggerResource(routeDefinition.getId(), predicateDefinition.getArgs()
.get(NameUtils.GENERATED_NAME_PREFIX + "0").replace("/**", API_URI)))));
return resources;
}
private SwaggerResource swaggerResource(String name, String location)
{
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}
效果:

7 Gatway 網關的過濾器開發
7.1 過濾器的執行次序
Spring-Cloud-Gateway 基於過濾器實現,同 zuul 類似,有pre和post兩種方式的 filter,分別處理前置邏輯和后置邏輯。客戶端的請求先經過pre類型的 filter,然后將請求轉發到具體的業務服務,收到業務服務的響應之后,再經過post類型的 filter 處理,最后返回響應到客戶端。
過濾器執行流程如下,order 越大,優先級越低

分為全局過濾器和局部過濾器
- 全局過濾器:
對所有路由生效
2、接口用時統計
- 局部過濾器:
對指定路由生效
7.2定義全局過濾器
實現 GlobalFilter 和 Ordered,重寫相關方法,加入到spring容器管理即可,無需配置,全局過濾器對所有的路由都有效。
全局過濾器舉例:代碼如下:
package com.crazymaker.cloud.nacos.demo.gateway.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Configuration
public class FilterConfig
{
@Bean
@Order(-1)
public GlobalFilter a()
{
return new AFilter();
}
@Bean
@Order(0)
public GlobalFilter b()
{
return new BFilter();
}
@Bean
@Order(1)
public GlobalFilter c()
{
return new CFilter();
}
@Slf4j
public class AFilter implements GlobalFilter, Ordered
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
log.info("AFilter前置邏輯");
return chain.filter(exchange).then(Mono.fromRunnable(() ->
{
log.info("AFilter后置邏輯");
}));
}
// 值越小,優先級越高
// int HIGHEST_PRECEDENCE = -2147483648;
// int LOWEST_PRECEDENCE = 2147483647;
@Override
public int getOrder()
{
return HIGHEST_PRECEDENCE + 100;
}
}
@Slf4j
public class BFilter implements GlobalFilter, Ordered
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
log.info("BFilter前置邏輯");
return chain.filter(exchange).then(Mono.fromRunnable(() ->
{
log.info("BFilter后置邏輯");
}));
}
// 值越小,優先級越高
// int HIGHEST_PRECEDENCE = -2147483648;
// int LOWEST_PRECEDENCE = 2147483647;
@Override
public int getOrder()
{
return HIGHEST_PRECEDENCE + 200;
}
}
@Slf4j
public class CFilter implements GlobalFilter, Ordered
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
log.info("CFilter前置邏輯");
return chain.filter(exchange).then(Mono.fromRunnable(() ->
{
log.info("CFilter后置邏輯");
}));
}
// 值越小,優先級越高
// int HIGHEST_PRECEDENCE = -2147483648;
// int LOWEST_PRECEDENCE = 2147483647;
@Override
public int getOrder()
{
return HIGHEST_PRECEDENCE + 300;
}
}
}
7.3定義局部過濾器
步驟:
1 需要實現GatewayFilter, Ordered,實現相關的方法
2 加入到過濾器工廠,並且注冊到spring容器中。
3、在配置文件中進行配置,如果不配置則不啟用此過濾器規則。
局部過濾器舉例, 對請求頭部的 user-id 進行校驗,代碼如下:
1 需要實現GatewayFilter, Ordered,實現相關的方法
package com.crazymaker.cloud.nacos.demo.gateway.filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
//@Component
@Slf4j
public class UserIdCheckGateWayFilter implements GatewayFilter, Ordered
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
String url = exchange.getRequest().getPath().pathWithinApplication().value();
log.info("請求URL:" + url);
log.info("method:" + exchange.getRequest().getMethod());
/* String secret = exchange.getRequest().getHeaders().getFirst("secret");
if (StringUtils.isBlank(secret))
{
return chain.filter(exchange);
}*/
//獲取param 請求參數
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
//獲取header
String userId = exchange.getRequest().getHeaders().getFirst("user-id");
log.info("userId:" + userId);
if (StringUtils.isBlank(userId))
{
log.info("*****頭部驗證不通過,請在頭部輸入 user-id");
//終止請求,直接回應
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
// 值越小,優先級越高
// int HIGHEST_PRECEDENCE = -2147483648;
// int LOWEST_PRECEDENCE = 2147483647;
@Override
public int getOrder()
{
return HIGHEST_PRECEDENCE;
}
}
2 加入到過濾器工廠,並且注冊到spring容器中。
package com.crazymaker.cloud.nacos.demo.gateway.config;
import com.crazymaker.cloud.nacos.demo.gateway.filter.UserIdCheckGateWayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
@Component
public class UserIdCheckGatewayFilterFactory extends AbstractGatewayFilterFactory<Object>
{
@Override
public GatewayFilter apply(Object config)
{
return new UserIdCheckGateWayFilter();
}
}
3、在配置文件中進行配置,如果不配置則不啟用此過濾器規則。
- id: service_provider_demo_route_filter
uri: lb://service-provider-demo
predicates:
- Path=/filter/**
filters:
- RewritePath=/filter/(?<segment>.*), /provider/$\{segment}
- UserIdCheck
8 整合Sentinel完成流控和降級
maven依賴
使用Sentinel作為gateWay的限流、降級、系統保護工具
<!--alibaba 流量衛士-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>${sentinel.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.7.1</version>
</dependency>
配置文件
客戶端配置:在配置文件中增加下列配置,dashboard就可以輕松管理客戶端了,還有一種方式是在啟動時加入
spring:
cloud:
sentinel:
transport:
## VM
##-Djava.net.preferIPv4Stack=true -Dcsp.sentinel.dashboard.server=localhost:8080 -Dcsp.sentinel.api.port=8666 -Dproject.name=gateway -Dcsp.sentinel.app.type=1
dashboard: localhost:8880
port: 8880
限流規則通用配置
由於sentinel的工作原理其實借助於全局的filter進行請求攔截並計算出是否進行限流、熔斷等操作的,增加SentinelGateWayFilter配置
@Bean//攔截請求
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
sentinel 不僅支持通過硬代碼方式進行資源的申明,還能通過注解方式進行聲明,為了讓注解生效,還需要配置切面類SentinelResourceAspect
@Bean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
sentinel攔截包括了視圖、靜態資源等,需要配置viewResolvers以及攔截之后的異常,我們也可以自定義拋出異常的提示
public SentinelConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean//自定義異常
@Order(Ordered.HIGHEST_PRECEDENCE)
public ExceptionHandler sentinelGatewayBlockExceptionHandler() {
// Register the block exception handler for Spring Cloud Gateway.
return new ExceptionHandler(viewResolvers, serverCodecConfigurer);
}
自定義異常提示:當發生限流、熔斷異常時,會返回定義的提示信息。
/**
* 配置限流的異常處理器:SentinelGatewayBlockExceptionHandler
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandlerEX(viewResolvers, serverCodecConfigurer);
}
不需要額外的配置,sentinel就已經可以正常工作了
限流規則設置
1 資源定義:定義API組
2 定義限流規則
具體請參見學習視頻
網關限流參數
其中網關限流規則 GatewayFlowRule的字段解釋如下:
-
resource:資源名稱,可以是網關中的 route 名稱或者用戶自定義的 API 分組名稱。
-
resourceMode:規則是針對 API Gateway 的 route(RESOURCE_MODE_ROUTE_ID)還是用戶在 Sentinel 中定義的 API 分組(RESOURCE_MODE_CUSTOM_API_NAME),默認是 route。
-
grade:限流指標維度,同限流規則的 grade 字段。
-
count:限流閾值
-
intervalSec:統計時間窗口,單位是秒,默認是 1 秒。
-
controlBehavior:流量整形的控制效果,同限流規則的 controlBehavior 字段,目前支持快速失敗和勻速排隊兩種模式,默認是快速失敗。
-
burst:應對突發請求時額外允許的請求數目。
-
maxQueueingTimeoutMs:勻速排隊模式下的最長排隊時間,單位是毫秒,僅在勻速排隊模式下生效。
-
paramItem
參數限流配置。若不提供,則代表不針對參數進行限流,該網關規則將會被轉換成普通流控規則;否則會轉換成熱點規則。其中的字段:
- parseStrategy:從請求中提取參數的策略,目前支持提取來源 IP(PARAM_PARSE_STRATEGY_CLIENT_IP)、Host(PARAM_PARSE_STRATEGY_HOST)、任意 Header(PARAM_PARSE_STRATEGY_HEADER)和任意 URL 參數(PARAM_PARSE_STRATEGY_URL_PARAM)四種模式。
- fieldName:若提取策略選擇 Header 模式或 URL 參數模式,則需要指定對應的 header 名稱或 URL 參數名稱。
- pattern:參數值的匹配模式,只有匹配該模式的請求屬性值會納入統計和流控;若為空則統計該請求屬性的所有值。(1.6.2 版本開始支持)
- matchStrategy:參數值的匹配策略,目前支持精確匹配(PARAM_MATCH_STRATEGY_EXACT)、子串匹配(PARAM_MATCH_STRATEGY_CONTAINS)和正則匹配(PARAM_MATCH_STRATEGY_REGEX)。(1.6.2 版本開始支持)
用戶可以通過 GatewayRuleManager.loadRules(rules) 手動加載網關規則,或通過 GatewayRuleManager.register2Property(property) 注冊動態規則源動態推送(推薦方式)。
