????????Spring Cloud Gateway是 Spring Cloud 生態系統中的一個 API 網關服務,用于替換由Zuul開發的網關服務,基于Spring 5.0+Spring Boot 2.0+WebFlux等技術開發,提供了網關的基本功能,例如安全、監控、埋點和限流等,旨在為微服務架構提供一種簡單而有效的統一 API 路由管理方式。
1.網關路由
1.1.認識網關
????????什么是網關?顧明思議,網關就是網絡的關口。數據在網絡間傳輸,從一個網絡傳輸到另一網絡時就需要經過網關來做數據的路由和轉發以及數據安全的校驗。
1.2?Gateway特性
Gateway具有以下主要特性:
-
動態路由:能夠匹配任何請求屬性上的路由
-
斷言(Predicate)和過濾器(Filter):針對特定路由的靈活配置
-
集成 Hystrix 斷路器:提供熔斷功能
-
服務發現集成:與 Eureka、Consul 等服務發現組件無縫集成
-
請求限流:支持基于多種策略的限流
-
路徑重寫:支持請求路徑的重寫
1.3 Gateway相關術語
① 路由(Route):路由是網關的基本組件,Gateway包含多個路由,每個路由包含唯一的ID(路由編號)、目標URI(即請求最終被轉發到的目的地URI)、路由斷言集合和過濾器集合。
② 斷言(Predicate):實際上就是Java 8 Function Predicate的斷言功能,即匹配條件,只有滿足條件的請求才會被路由到目標URI。輸入類型是 Spring Framework ServerWebExchange。其允許開發人員自行匹配來自HTTP請求的任何內容,例如HTTP頭或參數。
③ 過濾器(Filter):作用類似于攔截加工,對于經過過濾器的請求和響應,都可以進行修改,例如Spring Framework GatewayFilter實例,可以在發送下游請求之前或之后修改請求和響應。
2.?Gateway工作流程
????????當Gateway客戶端向Gateway服務端發送請求時,請求首先被HttpWebHandlerAdapter提取組裝成網關上下文,然后網關上下文會傳遞到DispatcherHandler中。DispatcherHandler是所有請求的分發處理器,主要負責分發請求對應的處理器,比如將請求分發到對應的RoutePredicateHandlerMapping(路由斷言處理映射器)。路由斷言處理映射器主要用于路由查找,以及找到路由后返回對應的FilterWebHandler。FilterWebHandler主要負責組裝過濾器鏈并調用過濾器執行一系列過濾處理,然后把請求轉到后端對應的代理服務處理,處理完畢之后將反饋信息
Spring Cloud Gateway 的核心流程:
客戶端請求 → Gateway Handler Mapping → Gateway Web Handler → 過濾器鏈 → 代理服務
3.?Spring Cloud Gateway案例
案例說明:創建兩個簡單的微服務模擬服務提供者和網關
3.1?父工程
創建父工程統一管理Spring Boot、Spring Cloud和Spring Cloud Alibaba,pom.xml文件代碼如下所示。
<?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"><modelVersion>4.0.0</modelVersion><groupId>com.hl</groupId><artifactId>shop</artifactId><version>1.0.0</version><packaging>pom</packaging><modules><module>order-consumer</module><module>order-provider</module></modules><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.12</version><relativePath/></parent><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><spring-cloud.version>2021.0.3</spring-cloud.version><spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version><spring-boot-web.version>2.7.12</spring-boot-web.version></properties><dependencyManagement><dependencies><!--spring cloud--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><!--spring cloud alibaba--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${spring-cloud-alibaba.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.22</version></dependency><!--單元測試--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency></dependencies></project>
3.2 service-provider微服務——服務提供者
在父工程中創建service-provider微服務,整合Nacos注冊中心,并創建一個“/hello”接口來模擬服務提供者。
① 修改pom.xml文件,追加Nacos服務發現組件spring-cloud-starter-alibaba-nacos-discovery,代碼如下所示。
<?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"><modelVersion>4.0.0</modelVersion><parent><groupId>com.hl</groupId><artifactId>shop</artifactId><version>1.0.0</version></parent><artifactId>order-provider</artifactId><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--nacos 服務注冊發現--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency></dependencies></project>
② 在service-provider微服務的src/main/resources目錄下創建application.yml文件,配置服務端口號為8081、微服務名為“service-provider”、Nacos注冊中心地址為“localhost:8848”,代碼如下所示。
server:port: 8081
spring:application:name: service-provideprofiles:active: devcloud:nacos:server-addr: localhost:8848 # nacos地址
③ 按照Spring Boot規范創建項目啟動類ServiceProviderApplication,在該啟動類上追加@EnableDiscoveryClient注解(該注解表示向Nacos注冊中心注冊微服務),開啟服務注冊與發現功能,代碼如下所示。
package com.hl;
import com.hl.config.OpenFeignLoggerConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication
@EnableFeignClients
public class ServiceProviderApplication{public static void main(String[] args) {SpringApplication.run(ServiceProviderApplication.class, args);}
}
④ 創建ProviderController類,在該類上追加@RestController注解,在該類中定義一個hello()方法,返回“hello”及傳進來的實參name。
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; @RestController
public class ProviderController {@GetMapping("/hello") public String hello(@RequestParam String name) {return "hello " + name + "!"; }
}
3.3 service-gateway微服務——網關
創建微服務service-gateway,并整合到Nacos注冊中心。
① 修改pom.xml文件,追加Nacos服務發現組件spring-cloud-starter-alibaba-nacos-discovery及Gateway依賴spring-cloud-starter-gateway,修改后的pom.xml文件代碼如下所示。
<?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"><modelVersion>4.0.0</modelVersion><parent><groupId>com.hl</groupId><artifactId>shop</artifactId><version>1.0.0</version></parent><artifactId>service-gateway</artifactId> <properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!--nacos 服務注冊發現--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--負載均衡器--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency><!--添加 Gateway依賴--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency></dependencies></project>
② 在service-gateway微服務的src/main/resources目錄下創建application.yml文件,配置服務端口號為8001、微服務名為“service-gateway、Nacos注冊中心地址為“localhost:8848”,spring.cloud.gateway.discovery.locator.enabled為true開啟Gateway服務發現,即Gateway將使用服務發現來動態路由請求,application.yml代碼如下所示。
server: port: 8001
spring: application: name: service-gateway cloud: nacos: discovery: server-addr: localhost:8848 gateway: discovery: locator: enabled: true
③ 按照Spring Boot規范創建項目啟動類ServiceGatewayApplication,在該啟動類上追加@EnableDiscoveryClient注解,開啟服務注冊與發現功能,代碼如下所示。
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceGatewayApplication { public static void main(String[] args) { SpringApplication.run(ServiceGatewayApplication.class, args); }
}
3.4 測試Gateway路由轉發
????????在IDEA工具中啟動兩個微服務:service-provider和service-gateway。啟動Nacos,訪問http://localhost:8848/nacos,選擇“服務管理”的“服務列表”,可發現service-provider和service-gateway微服務實例,說明微服務已成功注冊到了Nacos注冊中心
????????通過Gateway端口號8001及服務名“service-provider”訪問服務接口,即訪問http://localhost: 8001/service-provider/hello?name=gateway,其中顯示了“hello gateway”。至此基于網關實現了路由轉發。
4.?Gateway過濾器工廠
路由規則的定義語法如下:
spring:cloud:gateway:routes:- id: itemuri: lb://item-servicepredicates:- Path=/items/**,/search/**filters: - AddRequestHeader=X-Request-red, blue
四個屬性含義如下:
-
id
:路由的唯一標示 -
predicates
:路由斷言,其實就是匹配條件 -
filters
:路由過濾條件 -
uri
:路由目標地址,lb://
代表負載均衡,從注冊中心獲取目標微服務的實例列表,并且負載均衡選擇一個訪問。
????????過濾器允許以某種方式修改傳入的HTTP請求或傳出的HTTP響應。Gateway內置豐富的過濾器,例如AddRequestHeader、AddRequestParameter、AddResponseHeader、RemoveRequestHeader、StripPrefix、RewritePath、LoadBalancerClientFilter
4.1 AddRequestHeaderGatewayFilterFacotry
????????顧明思議,就是添加請求頭的過濾器,可以給請求添加一個請求頭并傳遞到下游微服務。
使用的使用只需要在application.yaml中這樣配置:
spring:cloud:gateway:routes:- id: test_routeuri: lb://test-servicepredicates:-Path=/test/**filters:- AddRequestHeader=key, value # 逗號之前是請求頭的key,逗號之后是value
如果想要讓過濾器作用于所有的路由,則可以這樣配置:
spring:cloud:gateway:default-filters: # default-filters下的過濾器可以作用于所有路由- AddRequestHeader=key, valueroutes:- id: test_routeuri: lb://test-servicepredicates:-Path=/test/**
5.?Gateway路由斷言工廠
????????Gateway包括很多路由斷言,當HTTP請求進入Gateway之后,由于實際工作中Gateway中存在多個路由,因此路由斷言會根據配置的路由規則對請求進行斷言匹配,若匹配成功則從相應路由轉發。
名稱 | 說明 | 示例 |
---|---|---|
After | 是某個時間點后的請求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某個時間點之前的請求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某兩個時間點之前的請求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 請求必須包含某些cookie | - Cookie=chocolate, ch.p |
Header | 請求必須包含某些header | - Header=X-Request-Id, \d+ |
Host | 請求必須是訪問某個host(域名) | - Host=**.somehost.org,**.anotherhost.org |
Method | 請求方式必須是指定方式 | - Method=GET,POST |
Path | 請求路徑必須符合指定規則 | - Path=/red/{segment},/blue/** |
Query | 請求參數必須包含指定參數 | - Query=name, Jack或者- Query=name |
RemoteAddr | 請求者的ip必須是指定范圍 | - RemoteAddr=192.168.1.1/24 |
weight | 權重處理 |
5.1?Header路由斷言
????????Header路由斷言有兩個參數:Header名稱(name)和正則表達式形式的值(value)。該路由斷言用于匹配具有給定名稱且值與正則表達式匹配的HTTP頭。
spring:cloud:gateway:default-filters: # default-filters下的過濾器可以作用于所有路由- AddRequestHeader=key, valueroutes:- id: test_routeuri: lb://test-servicepredicates: - Header=X-Request-Id, \d+
6. 動態路由
????????路由規則是網關的核心內容,配置在應用的屬性配置文件中,服務啟動的時候將路由規 則加載到內存中,這屬于靜態路由方式。網關的路由配置全部是在項目啟動時由org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator
在項目啟動的時候加載,并且一經加載就會緩存到內存中的路由表內(一個Map),不會改變。也不會監聽路由變更,所以我們無法利用配置熱更新來實現路由更新。
????????可采用 Nacos 實現動態路由,把路由更新規則保存在分布式配置 中心 Nacos 中,通過 Nacos 的監聽機制,動態更新每個實例的路由規則。因此,我們必須監聽Nacos的配置變更,然后手動把最新的路由更新到路由表中。
6.1?監聽Nacos配置變更
在Nacos官網中給出了手動監聽Nacos配置變更的SDK:https://nacos.io/zh-cn/docs/sdk.html
監聽配置:
如果希望 Nacos 推送配置變更,可以使用 Nacos 動態監聽配置接口來實現。
public void addListener(String dataId, String group, Listener listener)
請求參數說明:
參數名 | 參數類型 | 描述 |
---|---|---|
dataId | string | 配置 ID,保證全局唯一性,只允許英文字符和 4 種特殊字符("."、":"、"-"、"_")。不超過 256 字節。 |
group | string | 配置分組,一般是默認的DEFAULT_GROUP。 |
listener | Listener | 監聽器,配置變更進入監聽器的回調函數。 |
示例代碼:
String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
// 1.創建ConfigService,連接Nacos
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
// 2.讀取配置
String content = configService.getConfig(dataId, group, 5000);
// 3.添加配置監聽器
configService.addListener(dataId, group, new Listener() {@Overridepublic void receiveConfigInfo(String configInfo) {// 配置變更的通知處理System.out.println("recieve1:" + configInfo);}@Overridepublic Executor getExecutor() {return null;}
});
這里核心的步驟有2步:
-
創建ConfigService,目的是連接到Nacos
-
添加配置監聽器,編寫配置變更的通知處理邏輯
第一步:
????????由于我們采用spring-cloud-starter-alibaba-nacos-config
自動裝配,因此ConfigService
已經在com.alibaba.cloud.nacos.NacosConfigAutoConfiguration
中自動創建好了:
NacosConfigManager中是負責管理Nacos的ConfigService的,具體代碼如下:
因此,只要我們拿到NacosConfigManager
就等于拿到了ConfigService
第二步:
????????編寫監聽器。雖然官方提供的SDK是ConfigService中的addListener,不過項目第一次啟動時不僅僅需要添加監聽器,也需要讀取配置,因此建議使用的API是這個:
String getConfigAndSignListener(
????????String dataId, // 配置文件id
????????String group, // 配置組,走默認
????????long timeoutMs, // 讀取配置的超時時間
????????Listener listener // 監聽器
) throws NacosException;
????????既可以配置監聽器,并且會根據dataId和group讀取配置并返回。我們就可以在項目啟動時先更新一次路由,后續隨著配置變更通知到監聽器,完成路由更新。
6.2?更新路由
????????Gateway 提供了修改路由的接口 RouteDefinitionWriter,只有通過這個接口才能修改動態路由。
package org.springframework.cloud.gateway.route;import reactor.core.publisher.Mono;/*** @author Spencer Gibb*/
public interface RouteDefinitionWriter {/*** 更新路由到路由表,如果路由id重復,則會覆蓋舊的路由*/Mono<Void> save(Mono<RouteDefinition> route);/*** 根據路由id刪除某個路由*/Mono<Void> delete(Mono<String> routeId);}
這里更新的路由,也就是RouteDefinition,包含下列常見字段:
-
id:路由id
-
predicates:路由匹配規則
-
filters:路由過濾器
-
uri:路由目的地
將來我們保存到Nacos的配置也要符合這個對象結構,將來我們以JSON來保存,格式如下:
{"id": "item","predicates": [{"name": "Path","args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}}],"filters": [],"uri": "lb://item-service"
}
以上JSON配置就等同于:
spring:cloud:gateway:routes:- id: itemuri: lb://item-servicepredicates:- Path=/items/**,/search/**
6.3?.實現動態路由
首先引入依賴:
<!--統一配置管理-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--加載bootstrap-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
然后在Nacos控制臺添加路由,路由文件名為gateway-routes.json
,類型為json
:
[{"id": "item","predicates": [{"name": "Path","args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}}],"filters": [],"uri": "lb://item-service"}
]
然后在網關gateway
的resources
目錄創建bootstrap.yaml
文件,內容如下:
server:port: 8080 # 端口
spring:application:name: gatewaycloud:nacos:server-addr: localhostconfig:file-extension: yamlshared-configs:- dataId: gateway-routes.json # 動態路由配置
然后,在gateway
中定義配置監聽器:
package com.hmall.gateway.route;import cn.hutool.json.JSONUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.hmall.common.utils.CollUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {private final RouteDefinitionWriter writer;private final NacosConfigManager nacosConfigManager;// 路由配置文件的id和分組private final String dataId = "gateway-routes.json";private final String group = "DEFAULT_GROUP";// 保存更新過的路由idprivate final Set<String> routeIds = new HashSet<>();@PostConstructpublic void initRouteConfigListener() throws NacosException {// 1.注冊監聽器并首次拉取配置String configInfo = nacosConfigManager.getConfigService().getConfigAndSignListener(dataId, group, 5000, new Listener() {@Overridepublic Executor getExecutor() {return null;}@Overridepublic void receiveConfigInfo(String configInfo) {updateConfigInfo(configInfo);}});// 2.首次啟動時,更新一次配置updateConfigInfo(configInfo);}private void updateConfigInfo(String configInfo) {log.debug("監聽到路由配置變更,{}", configInfo);// 1.反序列化List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);// 2.更新前先清空舊路由// 2.1.清除舊路由for (String routeId : routeIds) {writer.delete(Mono.just(routeId)).subscribe();}routeIds.clear();// 2.2.判斷是否有新的路由要更新if (CollUtils.isEmpty(routeDefinitions)) {// 無新路由配置,直接結束return;}// 3.更新路由routeDefinitions.forEach(routeDefinition -> {// 3.1.更新路由writer.save(Mono.just(routeDefinition)).subscribe();// 3.2.記錄路由id,方便將來刪除routeIds.add(routeDefinition.getId());});}
}