第五章: 閘道器元件Gateway
官網文件: https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/
1. 概述
1.1 什麼是閘道器
微服務架構裡面還有一個非常重要的元件,就是閘道器,
在Spring Cloud 全家桶裡面也有這個角色, 在 1.x 版本中 採用的是 Zuul 閘道器,
但是因為 zuul的升級一直跳票,一直放鴿子, Spring Cloud 在2.x 中研發了一個自己的閘道器 替代了 Zuul, 那就是 Gateway
閘道器常見的功能有路由轉發、許可權校驗、限流控制等作用。
例如在微服務架構中, 如果可以使用閘道器對 請求進行轉發, 前端只需訪問一個地址,並攜帶需要呼叫的目標地址,由閘道器進行統一管理. 並且在請求過程中 對請求進行過濾,鑑權,使 微服務的 服務地址不直接暴露,保護了 微服務節點的安全
微服務架構中閘道器的位置:
1.2 Gateway 閘道器的基本特性
- Gateway 是在Spring 生態體系之上構建的 API 閘道器服務,由於底層使用 netty, 所以是基於 Spring5, Spring Boot2 和 Project Reactor等技術,Gateway旨在 提供一種簡單而有效的方式來對 API 進行路由, 以及提供一些強大的過濾器功能,例如 熔斷,限流,重試等
- Gateway作為 Spring Cloud 生態體系中的閘道器,目標是替代 Zuul, 在Spring Cloud 2.0 以上的版本中,沒有對新版本的Zuul 2.0 以上最新高效能版本進行整合, 仍然使用老的 非 Reactor 模式的 ,
- 為了提高效能, Gateway 是基於 WebFlux框架實現的, 而WebFlux 框架底層使用了高效能的 Reactor 模式的通訊框架 Netty
- Gateway 的目標提供統一的路由方式且基於 Filter 鏈的方式提供了閘道器基本的功能,例如 : 安全, 監控/指標, 和限流
原始碼架構:
什麼是WebFlux
這裡做基本介紹,詳細請自行官網學習
- 傳統的 Web框架, 比如說 spring mvc 等 都是 Servlet API 與 Servlet 容器基礎上執行的
- 但是, 在 Servlet 3.1 之後, 有了非同步非阻塞的支援, 而WebFlux 是一個典型的非阻塞非同步的框架,它的核心是基於Reactor模式的相關API 實現的,相對於傳統的web 框架來說, 它可以 執行在諸如Netty,Undertow 等支援Servlet3.1 的容器上, 非阻塞+ 函數語言程式設計(Spring5 必須使用java8)
- Spring WebFlux 是 Spring 5.0 引入的新的響應式框架, 區別於Springmvc, 它不需要依賴 Servlet API ,他是完全非同步非阻塞的,並且基於Reactor模式來實現響應式流規範
基本核心元件
Gateway 三個元件
- 路由: 閘道器的基本構建模組,它是由ID、目標URl、斷言集合和過濾器集合定義, 如果集合斷言為真,則匹配路由。
- Predicate(斷言):這是java 8的一個函式式介面predicate,可以用於lambda表 達式和方法引用,輸入型別是:Spring Framework ServerWebExchange,允許 開發人員匹配來自HTTP請求的任何內容,例如請求頭headers和引數paramers ,如果請求與斷言相匹配,則進行對應的路由
- Filter(過濾器):這些是使用特定工廠構建的Spring Framework GatewayFilter 例項,這裡可以在傳送請求之前或之後修改請求和響應
2. 基本使用
2.1 工程搭建及測試
需要搭建 Gateway 閘道器的微服務, 並註冊到註冊中心.
pom依賴:
<dependencies>
<!-- gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- <!– web Gateway不需要web依賴 –>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-web</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-actuator</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
yml配置:
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment_routh #路由的ID,沒有固定規則但要求唯一,建議配合服務名
uri: http://localhost:8001 #匹配後提供服務的路由地址,即路由跳轉地址
predicates:
- Path=/payment/get/** #路徑型別的斷言,路徑相匹配的則匹配路由
- id: payment_routh2
uri: http://localhost:8001
predicates:
- Path=/payment/lb/** #斷言,路徑相匹配的進行路由
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
主啟動類:
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
public static void main(String[] args) {
SpringApplication.run( GateWayMain9527.class,args);
}
}
上面的程式碼中, 將微服務啟動, 並註冊到7001
微服務,並在配置檔案中, 對 路徑/payment/get/**
和 /payment/lb/**
進行攔截,這就是斷言,若斷言為true,則匹配該路由,並跳轉到對應的uri
屬性下的 地址中
測試結果:
-
直接訪問
8001
微服務的 介面http://127.0.0.1:8001/payment/lb
返回結果 : "8001" 此介面返回
8001
微服務的埠 -
訪問
9527
Gateway 微服務的 地址:http://127.0.0.1:9527/payment/lb
斷言成功, 跳轉路由, 返回結果: "8001", 成功呼叫
若訪問的路徑,沒有任何路由匹配,則報錯404:
2.2 編碼方式配置路由
上面使用 yml 配置檔案的方式進行 配置路由規則, 也可以使用編碼的方式進行配置
下面我們使用編碼的方式配置路由,跳轉到百度的國內新聞
@Configuration
public class GateWayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
//路由構建器
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
//配置路由規則,對比 yml 檔案配置
// id: path_route , predicates: /guonei , uri: http://news.baidu.com/guonei
routes.route("path_route"
, r->r.path("/guonei").uri("http://news.baidu.com/guonei"))
.build();
return routes.build();
}
}
下面進行測試:
- 直接訪問百度國內新聞
http://news.baidu.com/guonei
,成功跳轉 - 通過Gateway 微服務訪問
http://127.0.0.1:9527/guonei
,也可以跳轉
2.3 使用微服務名跳轉
上面的程式碼中,我們跳轉到某個微服務,都是 直接寫對方的ip 地址,
Gateway 會自動 從註冊中心中獲取服務列表, 可以通過微服務的名稱作為路由轉發,那麼上面的程式碼就不用寫成
http://localhost:8001
而是 lb://cloud-payment-service
lb
為負載均衡,若該微服務有兩個實現,則進行負載均衡
程式碼演示:
首先必須先開啟註冊中心路由功能: spring.cloud.gateway.discovery.locator.enabled=true
pom修改:
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #開啟從註冊中心動態建立路由的功能,利用微服務名進行路由
routes:
- id: payment_routh #路由的ID,沒有固定規則但要求唯一,建議配合服務名
#uri: http://localhost:8001 #匹配後提供服務的路由地址
uri: lb://cloud-payment-service
predicates:
- Path=/payment/get/** #斷言,路徑相匹配的進行路由
- id: payment_routh2
#uri: http://localhost:8001 #匹配後提供服務的路由地址
uri: lb://cloud-payment-service
predicates:
- Path=/payment/lb/** #斷言,路徑相匹配的進行路由
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
開啟 8001
,8002
微服務,呼叫9527
地址: http://127.0.0.1:9527/payment/lb
,
輪流返回 "8001","8002" 對應微服務的地址,呼叫成功,並負載均衡
3. 斷言工廠
3.1 概述
Gateway閘道器中 另一個非常重要的元件: 斷言, 它決定一個請求是否由匹配此路由. 在前面的案例中使用的就是其中的Path 斷言工廠生成的 斷言類, 並匹配請求,跳轉到指定路徑,
GateWay給我們提供了很多不同型別的斷言工廠,都是抽象類AbstractRoutePredicateFactory
的子類
詳細使用,請檢視官網文件 : https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gateway-request-predicates-factories
分類:
時間相關:
-
AfterRoutePredicateFactory
: 匹配在指定日期時間之後發生的請求示例:
# 表示在 2017年1月20日17:42:47 之後 #此時間格式 可以使用 ZonedDateTime 類獲取 #ZonedDateTime.now(); // 預設時區 #ZonedDateTime.now(ZoneId.of("America/New_York")); // 用指定時區獲取當前時間 predicates: - After=2017-01-20T17:42:47.789-07:00[America/Denver]
-
BeforeRoutePredicateFactory
匹配在指定日期時間之前發生的請求 -
BetweenRoutePredicateFactory
匹配在datetime1
之後和在datetime2
之前的請求。該datetime2
引數必須datetime1
之後示例:
#表示在2017年1月20日17:42:47之後 並且 在2017年1月21日17:42:47之前 predicates: - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
Cookie相關:
-
CookieRoutePredicateFactory
匹配具有指定Cookie,並且值與指定的正則匹配的請求示例:
# 表示Cookie 中攜帶 鍵為chocolate,值為可以匹配正則"ch.p" 的字串 predicates: - Cookie=chocolate, ch.p
Header相關:
-
HeaderRoutePredicateFactory
, 匹配 請求頭中有指定的名,並且值匹配指定的正則表示式示例:
predicates: - Header=X-Request-Id, \d+
-
HostRoutePredicateFactory
, 匹配Host 域名列表示例:
# 匹配路徑中host 為 *.baidu.com 的 和 *.souhu.com的 predicates: - Host=**.baidu.com,**.sohu.com
-
RemoteAddrRoutePredicateFactory
,匹配請求的Remote(客戶端i來源ip)示例:
# 匹配Remote 為 192.168.1.1 至 192.168.1.254 # 斜槓後面的24 表示最後一位的最大值 即254 #16 表示最後兩位 即 255.254 #8 表示最後三位 即 255.255.254 predicates: - RemoteAddr=192.168.1.1/24
請求相關:
-
MethodRoutePredicateFactory
匹配指定的請求方式示例:
#匹配 GET,POST 請求 predicates: - Method=GET,POST
-
QueryRoutePredicateFactory
匹配請求有指定的引數key,並且值匹配指定的正則示例:
# 請求鍵為 red 值匹配正則 "gree." predicates: - Query=red, gree.
-
PathRoutePredicateFactory
匹配url 路徑,也就是我們上面案例中用到的
3.2 斷言工廠的工作原理
下面使用MethodRoutePredicateFactory
來進行演示
原始碼
public class MethodRoutePredicateFactory extends AbstractRoutePredicateFactory<MethodRoutePredicateFactory.Config> {
/** @deprecated */
@Deprecated
public static final String METHOD_KEY = "method";
public static final String METHODS_KEY = "methods";
public MethodRoutePredicateFactory() {
super(MethodRoutePredicateFactory.Config.class);
}
/**
* 封裝是 config 類中使用哪個欄位接受引數
*/
public List<String> shortcutFieldOrder() {
return Arrays.asList("methods");
}
public ShortcutType shortcutType() {
return ShortcutType.GATHER_LIST;
}
/**
* 實際生產 斷言操作類的方法
*/
public Predicate<ServerWebExchange> apply(MethodRoutePredicateFactory.Config config) {
return new GatewayPredicate() {
/**
* 斷言操作類的test方法,判斷請求是否符合條件
*/
public boolean test(ServerWebExchange exchange) {
HttpMethod requestMethod = exchange.getRequest().getMethod();
return Arrays.stream(config.getMethods()).anyMatch((httpMethod) -> {
return httpMethod == requestMethod;
});
}
public String toString() {
return String.format("Methods: %s", Arrays.toString(config.getMethods()));
}
};
}
/**
* 配置類,接受配置檔案中的配置的資訊,
*/
@Validated
public static class Config {
// 一個列舉陣列, 接受請求方式,例如 [GET,POST]
private HttpMethod[] methods;
public Config() {
}
/** @deprecated */
@Deprecated
public HttpMethod getMethod() {
return this.methods != null && this.methods.length > 0 ? this.methods[0] : null;
}
/** @deprecated */
@Deprecated
public void setMethod(HttpMethod method) {
this.methods = new HttpMethod[]{method};
}
public HttpMethod[] getMethods() {
return this.methods;
}
public void setMethods(HttpMethod... methods) {
this.methods = methods;
}
}
}
上面的程式碼中, 可以看出其實MethodRoutePredicateFactory
的實現比較簡單.生產一個GatewayPredicate
進行斷言.主要做了如下兩個操作
- 獲取配置檔案中配置的引數
- 判斷請求的方法是否匹配其中任意一個引數
3.3 自定義斷言工廠
根據上面的規則,我們可以實現自己的自定義斷言工廠
接收引數的Config 類:
public class TulingTimeBetweenConfig {
private LocalTime startTime;
private LocalTime endTime;
public LocalTime getStartTime() {
return startTime;
}
public void setStartTime(LocalTime startTime) {
this.startTime = startTime;
}
public LocalTime getEndTime() {
return endTime;
}
public void setEndTime(LocalTime endTime) {
this.endTime = endTime;
}
}
斷言工廠類,注意工廠類的類名必須以"RoutePredicateFactory"開頭, "RoutePredicateFactory" 之前的一部分則作為配置檔案中的鍵
@Component
public class TulingTimeBetweenRoutePredicateFactory extends AbstractRoutePredicateFactory<TulingTimeBetweenConfig> {
public TulingTimeBetweenRoutePredicateFactory() {
super(TulingTimeBetweenConfig.class);
}
/**
* 真正的業務判斷邏輯
*/
@Override
public Predicate<ServerWebExchange> apply(TulingTimeBetweenConfig config) {
LocalTime startTime = config.getStartTime();
LocalTime endTime = config.getEndTime();
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
LocalTime now = LocalTime.now();
//判斷當前時間是否在在配置的時間範圍類
return now.isAfter(startTime) && now.isBefore(endTime);
}
};
}
/**
* 用於接受yml中的配置 ‐ TulingTimeBetween=上午7:00,下午11:00
*/
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("startTime", "endTime");
}
}
yaml配置檔案,使用逗號分隔
predicates:
- TulingTimeBetween=上午7:00,下午11:00
測試,當請求時間為上午七點到下午十一點前的所有請求,都會被攔截
4. 過濾器工廠
上面的操作中,我們僅僅只是將 請求攔截,並跳轉到某個地址,貌似沒做什麼操作,作用很小,下面介紹過濾器的使用,將在攔截過程中做一些操作
,SpringCloudGateway 也提供了很多的過濾器工廠,我們通過一些過濾器工廠可以進行一些業務邏輯處理器,比如新增剔除響應頭,新增去除引數等.
4.1 常用過濾器簡單介紹
下面簡單介紹幾種常用的:
新增請求頭:
給攔截到請求中加入指定的請求頭和指定的值
predicates:
‐ TulingTimeBetween=上午7:00,下午11:00
filters:
‐ AddRequestHeader=X‐Request‐Company,tuling
新增請求引數
給請求加上指定的 Parameter 引數,和指定的值
predicates:
‐ TulingTimeBetween=上午7:00,下午11:00
filters:
‐ AddRequestParameter=company,tuling
為匹配的路由統一新增字首
給請求加上指定的字首,比如下面的配置中,請求http://localhost:8888/selectProductInfoById/1
會轉發到
http://localhost:8888/product‐api/selectProductInfoById/1
中
predicates:
‐ TulingTimeBetween=上午7:00,下午11:00
filters:
‐ PrefixPath=/product‐api
更多詳細使用者請參考官網,提供了豐富的過濾器工廠
4.1 自定義過濾器工廠
過濾器工廠的實現思路和斷言工廠類似,也可以參考著自定義自己的過濾器工廠,下面我們來實現一個記錄整個過濾鏈執行時間的過濾器工廠類
過濾器操作類:
在檢視原始碼過程中,發現其過濾器工廠返回過濾器操作類程式碼中,都是使用匿名內部類的方式,但是這樣過濾器的執行順序無法保證,只能按照載入順序執行,所以這裡我們將操作類單獨定義,實現Ordered
介面,保證載入順序優先
public class TimeMonitorGatewayFilter implements GatewayFilter, Ordered {
private static final String COUNT_START_TIME = "countStartTime";
private AbstractNameValueGatewayFilterFactory.NameValueConfig config;
public TimeMonitorGatewayFilter(AbstractNameValueGatewayFilterFactory.NameValueConfig config) {
this.config = config;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain
chain) {
//獲取配置檔案yml中的
// filters:
// ‐ TimeMonitor=enabled,true
String name = config.getName();
String value = config.getValue();
if (value.equals("false")) {
return null;
}
//在請求中記錄開始時間
exchange.getAttributes().put(COUNT_START_TIME,
System.currentTimeMillis());
//then方法相當於aop的後置通知一樣,當整個過濾鏈執行完畢時 ,將呼叫此方法,
return chain.filter(exchange).then(Mono.fromRunnable(new Runnable() {
@Override
public void run() {
//結束時間
Long startTime = exchange.getAttribute(COUNT_START_TIME);
//獲取開始時間 並計算差值
if (startTime != null) {
StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath())
.append(": ")
.append(System.currentTimeMillis() - startTime)
.append("ms");
sb.append(" params:").append(exchange.getRequest().getQueryParams());
//列印執行時間
System.out.println(sb.toString());
}
}
}));
}
/**
* 數字越小 Spring 載入此類越優先
* @return
*/
@Override
public int getOrder() {
return -100;
}
}
此類在執行鏈開始時執行,並記錄開始時間,並定義了結束過濾鏈結束時,計算差值
過濾器工廠類:
和斷言工廠一樣, 也是使用指定的字尾,來確定配置檔案中的配置方式,必須為"GatewayFilterFactory" 結尾
並繼承了 AbstractNameValueGatewayFilterFactory
類, 可以接受配置檔案中的 name,value 形式的引數
但是本例中只使用 value來定義
@Component
public class TimeMonitorGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
return new TimeMonitorGatewayFilter(config) ;
}
}
yaml配置檔案:
接受到的引數 : name為enabled ,value為true,但是上面的程式碼中 只用到了 true引數,含義為開啟此功能
predicates:
‐ Query=company,product
filters:
‐ TimeMonitor=enabled,true
測試: 呼叫本閘道器,[127.0.0.1:9527/payment/lb?name=10](http://127.0.0.1:9527/payment/lb?name=10)
列印日誌資訊: /payment/lb: 8ms params:{name=[10]}
4.3 自定義全域性過濾器
GateWay 框架中,還有一種特殊的過濾器, 為全域性過濾器,只要是被攔截的請求,都會被執行,上面的負載均衡功能就是
LoadBalancerClientFilter
全域性過濾器起的作用
其他全域性過濾器使用,請檢視官網: https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#global-filters
同樣,我們也可以自定義全域性過濾器:
@Component
public class MyLogGateWayFilter implements GlobalFilter,Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("進來");
//獲取 url上第一個 uname param
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if (uname==null){
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
/**
* 過濾鏈的排序
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
上面的過濾器中,進行了簡單的鑑權操作,若請求引數中沒有username,則拒絕轉發,