網關
Gateway 是 Java 微服務體系中的第二代服務網關,它是 Zuul 的替代品。
API 網關是一個服務,是系統的唯一入口。從面向對象設計的角度看,它與外觀模式類似。API 網關封裝了系統內部架構,為每個客戶端提供一個定制的 API 。
#0. 關於 Spring Cloud Netflix Zuul
Zuul 作為第一代網關,它相較於第二代網關 Gateway 而言,它最大的優勢在於:它是基於 Servlet 的,因此學習曲線幾乎為零。在並發量不高的情況下(僅在乎網關的功能,而不在乎其性能), Zuul 仍然是可選方案。
#1. 關於 Spring Cloud Gateway
Spring Cloud Gateway 基於 Spring Boot 2 ,是 Spring Cloud 的全新項目。Gateway 旨在提供一種簡單而有效的途徑來轉發請求,並為它們提供橫切關注點。
Spring Cloud Gateway 中最重要的幾個概念:
-
路由 Route:路由是網關最基礎的部分,路由信息由一個 ID 、一個目的 URL 、一組斷言工廠和一組 Filter 組成。如果路由斷言為真,則說明請求的 URL 和配置的路由匹配。
-
斷言 Predicate:Java 8 中的斷言函數。Spring Cloud Gateway 中的斷言函數輸入類型是 Spring 5.0 框架中的 ServerWebExchange 。Spring Cloud Gateway 中的斷言函數允許開發者去定義匹配來自 Http Request 中的任何信息,比如請求頭和參數等。
-
過濾器 Filter:一個標准的 Spring Web Filter 。Spring Cloud Gateway 中的 Filter 分為兩種類型:Gateway Filter 和 Global Filter 。過濾器 Filter 將會對請求和響應進行修改處理。
#2. 入門案例
作為網關來說,網關最重要的功能就是協議適配和協議轉發,協議轉發也就是最基本的路由信息轉發。
創建項目 gateway-server
,演示 Gateway 的基本路由轉發功能,也就是通過 Gateway 的 Path 路由斷言工廠實現 url 直接轉發。
-
引入 Spring Cloud Gateway:Spring Cloud Routing > Gateway :
注意
Gateway 自己使用了 netty 實現了 Web 服務,此處『不需要引入 Spring Web』,如果引入了,反而還會報沖突錯誤,無法啟動。
-
編寫主入口程序代碼,如下:
@SpringBootApplication public class GatewayServerApplication { public static void main(String[] args) { SpringApplication.run(GatewayServerApplication.class, args); } /** * 配置 */ @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(r -> r .path("/jd") .uri("http://www.jd.com:80/") .id("jd_route") ).build(); } }
Copied!上述代碼配置了一個路由規則:當用戶輸入 /jd 路徑時,Gateway 會將請求導向到 http://www.jd.com:80 (opens new window)網址。
除了這種編碼方式配置外,Gateway 還支持通過項目配置文件配置。例如:
properties 格式:spring.application.name=gateway-server spring.cloud.gateway.routes[0].id=163_route spring.cloud.gateway.routes[0].uri=http://www.163.com spring.cloud.gateway.routes[0].predicates[0]=Path=/163
yml 格式:spring:
application:
name: gateway-server
cloud:
gateway:
routes:
- id: 163_route
uri: http://www.163.com
predicates:
- Path=/163
當用戶輸入 /163 路徑時,Gateway 將會導向到 http://www.163.com (opens new window)網址。
兩種配置方式可以同時使用。
#3. Gateway 內置 Predicate
Spring Cloud Gateway 是由很多的路由斷言工廠組成。當 HTTP Request 請求進入 Spring Cloud Gateway 的時候,網關中的路由斷言工廠就會根據配置的路由規則,對 HTTP Request 請求進行斷言匹配。匹配成功則進行下一步處理,否則,斷言失敗直接返回錯誤信息。
TIP
早期的 Gateway 斷言的配置是通過代碼中的 @Bean 進行配置,后來才推出配置文件配置。
前面 163
的示例中,我們使用的就是 Path 路由斷言。
spring: cloud: gateway: routes: - id: <id> # 路由 ID,唯一 uri: <目標 URL> # 目標 URI,路由到微服務的地址 predicates: - Path=<匹配規則> # 支持通配符 - id: <id> uri: <目標 URL> predicates: - Path=<匹配規則> - id: <id> uri: <目標 URL> predicates: - Path=<匹配規則> - ...
例如:
spring: cloud: gateway: routes: - id: xxx uri: http://www.xxx.com/ predicates: - Path=/xxx/**
Path 斷言不會改變請求的 URI ,即,Gateway 收到的 URI 是什么樣的,那么它將請求轉給目標服務的時候,URI 仍然是什么。整個過程中只有 IP、端口部分會被『替換』掉 。
再重復一遍
Path 斷言不會改變請求的 URI ,整個過程中只有 IP、端口部分會被『替換』掉 。
#4. Gateway 整合 Nacos 注冊中心實現路由
Gateway 整合 Nacos Sever(注冊中心)之后,會以微服務的 name 和 URI 的對應關系為依據(利用 Path 路由斷言),改變 url 訪問路徑(使用 RewritePath 過濾器),將訪問請求 URL 的訪問請求轉給對應的微服務。
-
首先將 Gateway 視作普通的 Nacos Client 進行配置、啟動。讓其『連上』注冊中心,從注冊中心拉去各個微服務的信息(網址、端口等)。
略。
-
配置若干與 Gateway 相關的配置:
spring.cloud.gateway.discovery.locator.enabled=true spring.cloud.gateway.discovery.locator.lower-case-service-id=true # 降低日志級別,驗證配置 logging.level.org.springframework.cloud.gateway=DEBUG
Copied!-
.locator.enabled :
該配置是 Gateway 與注冊中心整合的開關項。必然要賦值為 true 。
-
.locator.lower-case-service-id :
true 表示出現在 url 中的服務名為全小寫;false 表示出現在 url 中的服務名為全大寫。
相較而言,大家看全小寫英文會更為習慣一些。另外,一旦開啟 lower-case,那么就不能用全大寫了,而且大小寫不能混用。
-
logging.level.org.springframework.cloud.gateway :
日志是非必要配置,這里配置成 DEBUG 級別是為了驗證 Gateway 自動生成了 Path 斷言規則。
-
先后啟動『注冊中心』、『服務提供者』和『Gateway』,訪問 Gateway,並在訪問路徑中加上 /服務提供者的標識
,例如:/microservice-department/hello
,你會發現這個請求會被 Gateway 轉給 microservice-department 的 /hello
。
並且,日志中會有類似如下一條信息:
Mapping [Exchange: GET http://localhost:9527/microservice-department/] to Route{id='ReactiveCompositeDiscoveryClient_MICROSERVICE-DEPARTMENT', uri=lb://MICROSERVICE-DEPARTMENT, order=0, predicate=Paths: [/microservice-department/**], match trailing slash: true, gatewayFilters=[[[RewritePath /microservice-department/(?<remaining>.*) = '/${remaining}'], order = 1]], metadata={management.port=8080}}
補充
如果你啟動了多個服務提供者實例,Gateway 會自動實現基於輪循的負載均衡路由。
#5. RewritePath 過濾器
RewritePath 過濾器可以重寫 URI,去掉 URI 中的前綴。例如,下面的自己中就是去掉所有 URI 中的 /xxx/yyy/zzz 部分,只留之后的內容,再進行轉發。
以上 Java 代碼配置等同於 .yml 配置:
spring: cloud: gateway: routes: - id: 163_route uri: http://localhost:8081 predicates: - Path=/xxx/yyy/zzz/** filters: - RewritePath=/xxx/yyy/zzz/(?<segment>.*), /$\{segment}
對於請求路徑 /xxx/yyy/zzz/hello ,當前的配置在請求到到達前會被重寫為 /hello ,
-
命名分組:
(?<name>正則表達式)
與普通分組一樣的功能,並且將匹配的子字符串捕獲到一個組名稱或編號名稱中。在獲得匹配結果時,可通過分組名進行獲取。
(?<segment>.*)
:匹配 0 個或多個任意字符,並將匹配的結果捕獲到名稱為 segment 的組中。 -
引用捕獲文本:
${name}
將名稱為 name 的命名分組所匹配到的文本內容替換到此處。
$\{segment}
:將前面捕獲到 segment 中的文本置換到此處,注意,\ 的出現是由於避免 YAML 認為這是一個變量而使用的轉義字符。
#6. 自定義全局 Filter
自定義全局過濾器要實現 GlobalFilter 接口。全局過濾器不需要指定對哪個路由生效,它對所有路由都生效。
public class XxxGlobalFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 邏輯代碼 ... if (...) { // 流程繼續向下,走到下一個過濾器,直至路由目標。 return chain.filter(exchange); } else { // 否則流程終止,拒絕路由。 exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); return exchange.getResponse().setComplete(); }; } }
本質上,全局過濾器就是 GlobalFilter 接口的實現類的實例。你配置一個(或多個)GlobalFilter 類型的 @Bean,它(們)就是全局過濾器。他們只要存在,就會被 Gateway 使用到。
@Bean @Order(-100) // 注解是為了去控制過濾器的先后順序,值越小,優先級越高。 public GlobalFilter xxxGlobalFilter() { return ...; }
上面的全局過濾器的 filter 的邏輯結構所實現的功能:當條件成立時,允許路由;否則,直接返回。
TIP
在這種行駛中路由器的所有代碼邏輯都是在『路由前』執行。
當然,這種形式的過濾器的更簡單的情況是:執行某些代碼,然后始終是放行。
這種邏輯結構的過濾器可以實現認證功能。
在過濾器中,你可以獲得與當前請求相關的一些信息:
ServerHttpRequest request = exchange.getRequest(); log.info("{}", request.getMethod()); log.info("{}", request.getURI()); log.info("{}", request.getPath()); log.info("{}", request.getQueryParams()); // Get 請求參數 request.getHeaders().keySet().forEach(key -> { log.info("{}: {}", key, request.getHeaders().get(key)) });
當然,你也可以單獨地將自定義的 GlobalFilter 定義出來,然后在 @Bean 中進行配置:
@Bean public GlobalFilter xxxGlobalFilter() { return new XxxGlobalFilter(); }
#7. JSON 形式的錯誤返回
上述的『拒絕』是以 HTTP 的錯誤形式返回,即 4xx、5xx 的錯誤。
有時,我們的返回方案是以 200 形式的『成功』返回,然后再在返回的信息中以自定義的錯誤碼和錯誤信息的形式告知請求發起者請求失敗。
此時,就需要 過濾器『成功』返回 JSON 格式的字符串:
String jsonStr = "{\"status\":\"-1\", \"msg\":\"error\"}"; byte[] bytes = jsonStr.getBytes(StandardCharsets.UTF_8); DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes); return exchange.getResponse().writeWith(Flux.just(buffer));
#8. 獲取 Body 中的請求參數(了解、自學)
由於 Gateway 是基於 Spring 5 的 WebFlux 實現的(采用的是 Reactor 編程模式),因此,從請求體中獲取參數信息是一件挺麻煩的事情。
有一些簡單的方案可以從 Request 的請求體中獲取請求參數,不過都有些隱患和缺陷。
最穩妥的方案是模仿 Gateway 中內置的 ModifyRequestBodyGatewayFilterFactory,不過,這個代碼寫起來很啰嗦。
具體內容可參考這篇文章:Spring Cloud Gateway(讀取、修改 Request Body)(opens new window)
不過考慮到 Gateway 只是做請求的『轉發』,而不會承擔業務責任,因此,是否真的需要在 Gateway 中從請求的 Body 中獲取請求數據,這個問題可以斟酌。
#9. 過濾器的另一種邏輯形式(了解、自學)
有時你對過濾器的運用並非是為了決定是否繼續路由,為了在整個流程中『嵌入』額外的代碼、邏輯:在路由之前和之后執行某些代碼。
如果僅僅是在路由至目標微服務之前執行某些代碼邏輯,那么 Filter 的形式比較簡單:
return (exchange, chain) -> { // 邏輯代碼 ... // 流程繼續向下,走到下一個過濾器,直至路由目標。 return chain.filter(exchange); }
如果,你想在路由之前和之后(即,目標微服務返回之后)都『嵌入』代碼,那么其形式就是:
@Override public GatewayFilter apply(Config config) { return ((exchange, chain) -> { log.info("目標微服務【執行前】執行"); return chain.filter(exchange) .then(Mono.fromRunnable(() -> { log.info("目標微服務【執行后】執行"); })); }); }
例如,顯示一個用於統計微服務調用時長的過濾器:
@Override public String name() { return "elapsed"; } @Override public GatewayFilter apply(Config config) { return ((exchange, chain) -> { log.info("目標微服務【執行前】執行"); return chain.filter(exchange) .then(Mono.fromRunnable(() -> { log.info("目標微服務【執行后】執行"); })); }); }
配置:
spring: cloud: gateway: routes: - id: 163_route uri: http://www.163.com predicates: - Path=/163 filters: - name: elapsed