JAVA 8
spring boot 2.5.2
spring cloud 2020.0.3
---
授人以漁:
最新版本,下載下來,以便查閱。
更多版本的官方文檔:
https://docs.spring.io/spring-cloud/docs/
沒有PDF版本,把網頁保存下來。
本文使用的項目:
主要路徑:前端請求經過 external.gateway 轉發到 adapter.web。在此過程中,會做一些試驗。
external.gateway | 網關服務 | 端口 25001 |
adapter.web | web適配層應用 | 端口 21001 |
data.user | user數據層應用 | 端口 20001 |
eureka.server | Eureka注冊中心 | 端口 10001 |
目錄
建立項目,引入 spring-cloud-starter-gateway 包:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
檢查包依賴結構:
其中,依賴了 spring-cloud-gateway-server、spring-boot-starter-webflux(使用Netty服務器)。
啟動項目,發現加載了很多 RoutePredicateFactory:
檢查項目啟動后Spring容器中的Bean,可以發現很多和Gateway相關的,比如:
部分Gateway相關Bean
org.springframework.cloud.gateway.discovery.GatewayDiscoveryClientAutoConfiguration
org.springframework.cloud.gateway.config.GatewayAutoConfiguration$NettyConfiguration
gatewayHttpClient
org.springframework.cloud.gateway.config.GatewayAutoConfiguration
gatewayConfigurationService
routePredicateHandlerMapping
gatewayProperties
# 多個
**RoutePredicateFactory
# 多個
**GatewayFilterFactory
org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration
spring.cloud.gateway.loadbalancer-org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties
...
啟動后訪問 網關服務——http://localhost:25001/ ,但沒有找到頁面:里面有一個requestId,和之前的Web項目不一樣
Postman訪問結果
{
"timestamp": "2021-09-11T03:06:33.044+00:00",
"path": "/",
"status": 404,
"error": "Not Found",
"message": null,
"requestId": "18974297-1"
}
那么,有哪些端口可以訪問呢?
添加actuator檢查,也沒有發現有可用的端口:
使用actuator
# pom.xml文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# application.properties文件
management.endpoints.web.exposure.include=health, info, mappings
查看Spring Cloud文檔,其Spring Cloud Gateway下有一章“15. Actuator API”,原來,還需要添加以下配置才可以看到:
# 多了一個 gateway
management.endpoints.web.exposure.include=health, info, mappings,gateway
再次啟動 網關服務,此時,多了一個 /actuator/gateway 端點,下面是訪問結果:
訪問/actuator/gateway及其子端點
# 居然訪問不到!
# http://localhost:25001/actuator/gateway
{
"timestamp": "2021-09-11T03:41:43.253+00:00",
"path": "/actuator/gateway",
"status": 404,
"error": "Not Found",
"message": null,
"requestId": "aba1dfd7-4"
}
# 子端點 routes,,返回結果為 [],因為什么路由也沒有配置
# http://localhost:25001/actuator/gateway/routes
[]
# 子端點 globalfilters
http://localhost:25001/actuator/gateway/globalfilters
{
"org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@6b00ad9": -2147482648,
"org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@3ce53f6a": -1,
"org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@59d77850": 2147483646,
"org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration$NoLoadBalancerClientFilter@60859f5a": 10150,
"org.springframework.cloud.gateway.filter.ForwardPathFilter@1a6cf771": 0,
"org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@3ee69ad8": 10000,
"org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@2d82408": -2147483648,
"org.springframework.cloud.gateway.filter.GatewayMetricsFilter@53ed09e8": 0,
"org.springframework.cloud.gateway.filter.NettyRoutingFilter@19650aa6": 2147483647,
"org.springframework.cloud.gateway.filter.ForwardRoutingFilter@f679798": 2147483647
}
/actuator/gateway 端點還有一些子端點,S.C.的官文中會有詳情(下圖來自官網)。
S.C.的 第二代網關框架(第一代為 Netflix Zuul),不僅提供1)統一的路由方式,並且基於Filter鏈的方式提供了網關的基本功能。
使用 非阻塞模式(WebFlux、Netty),支持長連接WebSocket。
可用作為 整個分布式系統的流量入口,也可以作為 系統內部若干應用的網關服務,再統一其它應用提供服務。
功能關鍵詞:
協議轉換、路由轉發、流量聚合、流量監控、限流、權限判斷、緩存
核心組件:
路由、過濾器、斷言(Predicate)
請求處理流程:
請求》Gateway Handler Mapping》路由匹配(斷言)》Gateway Web Handler》過濾器鏈》代理服務(可以是 應用)
啟動后,存在3個Handler Mapping——不一定是 Gateway H.M.:
routePredicateHandlerMapping
requestMappingHandlerMapping
resourceHandlerMapping
Web Handler則有以下Bean:至於包含 Handler 字符的Bean 則有更多
webHandler
filteringWebHandler
過濾器鏈:
pre過濾器邏輯,處理請求后,交給代理服務;
post過濾器邏輯,收到代理服務的響應后執行並返回請求方。
pre過濾器邏輯可以:鑒權、限流、更改請求頭、轉換協議等;
post過濾器邏輯可以:對響應數據進行修改,比如更改響應頭、轉換協議等。
訪問 web適配層應用 的接口:http://localhost:21001/user/get?id=1
注,由於路由的配置特性,將配置文件轉為YAML文件會更方便。
對於上面的接口,路由配置如下:
# 路由配置
#spring: # 前面有,這里不需要
cloud:
gateway:
routes:
# 訪問 adapter.web
- id: route1
uri: http://localhost:21001
predicates:
# 嚴格按照下面的格式來,小於10的話,前加0
- After=2021-09-11T13:13:13.000+08:00[Asia/Shanghai]
路由通過 spring.cloud.gateway.routes.* 來配置,routes下每一個都是一個路由規則。
每一個路由規則,都需要有 id、uri、predicates 三個屬性,其中的 predicates為斷言。
上面使用了 After路由斷言工廠(Bean名稱 afterRoutePredicateFactory),格式要正確,否則無法啟動。After的意思是:在這個時間之后的請求都可以使用這條路由——做轉發。
配置后,查看 /actuator/gateway/routes 端點:存在一條路由了。route_id為前面配置的 id。除了上面的3個參數,還可以配置 order、filters 參數。order在存在多個路由規則的時候指定順序。
[
{
"predicate": "After: 2021-09-11T13:13:13+08:00[Asia/Shanghai]",
"route_id": "route1",
"filters": [],
"uri": "http://localhost:21001",
"order": 0
}
]
測試通過網關服務訪問路由中指定的服務:訪問成功。
和直接訪問 web適配層應用 相比,這里的返回結果 少了2個Header:Keep-Alive、Conection,有什么影響呢?TODO
在上面的訪問中,網關服務 是沒有日志輸出的。開啟調試模式(debug: true),可以看到下面的日志:
當然,網關打印太多日志會影響服務器性能。
Spring容器中有哪些斷言工廠Bean呢?
使用的時候,去掉 RoutePredicateFactory,再把首字母大寫即可。每一個Bean名稱都對應一個工廠類,可以去看源碼。
name=afterRoutePredicateFactory
name=beforeRoutePredicateFactory
name=betweenRoutePredicateFactory
name=cookieRoutePredicateFactory
name=headerRoutePredicateFactory
name=hostRoutePredicateFactory
name=methodRoutePredicateFactory
name=pathRoutePredicateFactory
name=queryRoutePredicateFactory
name=readBodyPredicateFactory
name=remoteAddrRoutePredicateFactory
name=weightRoutePredicateFactory
name=cloudFoundryRouteServiceRoutePredicateFactory
配置After斷言后的時間未到,測試結果如下:相比於正常的 路由生效時的 未找到路徑,多了 messge、requestId 兩個字段。
點擊查看代碼
{
"timestamp": "2021-09-11T07:27:45.411+00:00",
"path": "/user/get",
"status": 404,
"error": "Not Found",
"message": null,
"requestId": "6e46a6fa-8, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:61499"
}
正常的路徑沒找到結果
{
"timestamp": "2021-09-11 07:30:51",
"status": 404,
"error": "Not Found",
"path": "/user2/get"
}
路由中配置的主機不存在(也可能是服務器故障、重啟中等情況,注意,去掉前面配置的路由再做測試):
錯誤信息
響應結果:
{
"timestamp": "2021-09-11T07:37:17.437+00:00",
"path": "/user/get",
"status": 500,
"error": "Internal Server Error",
"requestId": "0d01c5bb-1, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:56362"
}
異常日志:
2021-09-11 15:37:17.449 ERROR 26812 --- [ctor-http-nio-5] a.w.r.e.AbstractErrorWebExceptionHandler : [0d01c5bb-1, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:56362] 500 Server Error for HTTP GET "/user/get?id=1"
io.netty.channel.AbstractChannel$AnnotatedConnectException: Connection refused: no further information: localhost/127.0.0.1:9999
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Caused by: java.net.ConnectException: Connection refused: no further information
正常路由、錯誤路由並存(1):正常的先配置、錯誤的后配置(下面的配置中,Order注釋掉)
正常錯誤2路由
#
# 路由配置
gateway:
routes:
# 訪問 adapter.web
- id: route1
# 1)服務
uri: http://localhost:21001
# order: 2
# 2)端口后添加部分路徑:無用,和上面效果相同
# uri: http://localhost:21001/user
predicates:
# 嚴格按照下面的格式來,小於10的話,前加0
- After=2021-09-11T14:13:13.000+08:00[Asia/Shanghai]
# 不存在的主機
- id: routeErr
uri: http://localhost:9999
# order: 1
predicates:
# 嚴格按照下面的格式來,小於10的話,前加0
- After=2021-09-11T14:13:13.000+08:00[Asia/Shanghai]
/actuator/gateway/routes 結果:order都是默認值0,且,正常的在前
響應結果
[
{
"predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
"route_id": "route1",
"filters": [],
"uri": "http://localhost:21001",
"order": 0
},
{
"predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
"route_id": "routeErr",
"filters": [],
"uri": "http://localhost:9999",
"order": 0
}
]
訪問請求,成功——。
正常路由、錯誤路由並存(2)——使用Order:錯誤的Order值為1、正常的Order值為2
將上面配置中的 Order 配置 取消注釋。
/actuator/gateway/routes 結果:routeErr 變為在前了
響應結果
[
{
"predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
"route_id": "routeErr",
"filters": [],
"uri": "http://localhost:9999",
"order": 1
},
{
"predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
"route_id": "route1",
"filters": [],
"uri": "http://localhost:21001",
"order": 2
}
]
訪問請求,失敗,且發生連接錯誤。從效果來看,請求都被 錯誤路由接管了。
Before斷言試驗:
時間格式同After斷言。
在時間參數到達前,此路由有效。
- Before=2021-09-11T14:13:13.000+08:00[Asia/Shanghai]
/actuator/gateway/routes 結果:predicate變成Before了
[
{
"predicate": "Before: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
"route_id": "route1",
"filters": [],
"uri": "http://localhost:21001",
"order": 0
}
]
訪問請求,符合預期。
Between斷言試驗:
# Between斷言: 2個參數
- Between=2021-09-11T16:13:13.000+08:00[Asia/Shanghai],2021-09-11T17:13:13.000+08:00[Asia/Shanghai]
/actuator/gateway/routes 結果:
響應結果
[
{
"predicate": "Between: 2021-09-11T16:13:13+08:00[Asia/Shanghai] and 2021-09-11T17:13:13+08:00[Asia/Shanghai]",
"route_id": "route1",
"filters": [],
"uri": "http://localhost:21001",
"order": 0
}
]
訪問請求,符合預期。
配置2個正常路由:
兩個使用After斷言的路由,uri相同,但是,route1要到 2221年才生效,而route2才是正常可用的——預期會使用route2來轉發請求。
響應結果
[
{
"predicate": "After: 2221-09-11T14:13:13+08:00[Asia/Shanghai]",
"route_id": "route1",
"filters": [],
"uri": "http://localhost:21001",
"order": 0
},
{
"predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
"route_id": "route2",
"filters": [],
"uri": "http://localhost:21001",
"order": 0
}
]
訪問請求,符合預期。
一個路由配置多個斷言:
參數 predicates 是個復數形式,這就意味着,一個路由可以配置多個斷言。
gateway:
routes:
# 訪問 adapter.web
- id: route1
# 1)服務
uri: http://localhost:21001
predicates:
# After斷言
# 嚴格按照下面的格式來,小於10的話,前加0
- After=2021-09-11T14:13:13.000+08:00[Asia/Shanghai]
# Header斷言:2個參數,鍵、值
- Header=headerParam, 123
訪問 /actuator/gateway/routes:
[
{
"predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Header: headerParam regexp=123)",
"route_id": "route1",
"filters": [],
"uri": "http://localhost:21001",
"order": 0
}
]
訪問請求:http://localhost:25001/user/get?id=1
1)不帶請求頭headerParam=123
請求失敗。
響應結果-失敗
{
"timestamp": "2021-09-11T08:43:01.274+00:00",
"path": "/user/get",
"status": 404,
"error": "Not Found",
"message": null,
"requestId": "d8c568f7-3, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:59011"
}
2)帶請求頭headerParam=123
請求成功。
補充:RouteDefinition類部分源碼
@Validated
public class RouteDefinition {
// 路由ID
private String id;
// 斷言列表
@NotEmpty
@Valid
private List<PredicateDefinition> predicates = new ArrayList<>();
// 路由過濾器
@Valid
private List<FilterDefinition> filters = new ArrayList<>();
// 代理服務地址
@NotNull
private URI uri;
// 元數據?
private Map<String, Object> metadata = new HashMap<>();
// 路由順序:值越小越優先
private int order = 0;
// ...省略
}
小結,
本節初步體驗了Gateway的路由轉發功能,試驗了幾個斷言的用法,還有更多斷言等待解鎖(Cookie、Header、Host、Method、Path、Query、RemoteAddr等)。
正如前文所言,斷言(和Order)決定了使用哪個路由去處理請求,在路由處理前,要經過路由配置的過濾器處理——這里特指pre過濾器,之后才到達 服務代理(或應用),下一節將介紹過濾器的使用。來自博客園
過濾器,從處理對象分為兩種:1)pre——過濾請求,2)post——處理響應;從作用范圍分為兩種:1)針對單個路由的過濾器(GatewayFilter接口)、2)針對所有路由的全局過濾器(GlobalFilter接口)。
GatewayFilter接口 的實現對象 主要是在 各種GatewayFilterFactory類中實現:
而 GlobalFilter接口 則有很多直接實現類:來自博客園
前文配置路由時,提到一個filters參數,便是用來配置 針對單個路由的過濾器的。
在前面的RouteDefinition類中,filters參數是FilterDefinition列表,而FilterDefinition類只有name、args兩個參數:
@Validated
public class FilterDefinition {
// 過濾器名稱
@NotNull
private String name;
// 過濾器參數
private Map<String, String> args = new LinkedHashMap<>();
// ...省略...
}
而且,FilterDefinition沒有子類。那么,怎么創建FilterDefinition對象的呢?FilterDefinition真的有用到?TODO
在S.C.Gateway中,使用的是各種**GatewayFilterFactory類來創建,比如,AddRequestHeaderGatewayFilterFactory——添加請求頭GatewayFilterFactory。
配置路由時,只需要使用 AddRequestHeader即可——區分大小寫。來自博客園
在Spring容器中,還有以下GatewayFilterFactory Bean(共發現28個):
GatewayFilterFactory Beans
name=addRequestHeaderGatewayFilterFactory
name=mapRequestHeaderGatewayFilterFactory
name=addRequestParameterGatewayFilterFactory
name=addResponseHeaderGatewayFilterFactory
name=modifyRequestBodyGatewayFilterFactory
name=dedupeResponseHeaderGatewayFilterFactory
name=modifyResponseBodyGatewayFilterFactory
name=prefixPathGatewayFilterFactory
name=preserveHostHeaderGatewayFilterFactory
name=redirectToGatewayFilterFactory
name=removeRequestHeaderGatewayFilterFactory
name=removeRequestParameterGatewayFilterFactory
name=removeResponseHeaderGatewayFilterFactory
name=rewritePathGatewayFilterFactory
name=retryGatewayFilterFactory
name=setPathGatewayFilterFactory
name=secureHeadersGatewayFilterFactory
name=setRequestHeaderGatewayFilterFactory
name=setRequestHostHeaderGatewayFilterFactory
name=setResponseHeaderGatewayFilterFactory
name=rewriteResponseHeaderGatewayFilterFactory
name=rewriteLocationResponseHeaderGatewayFilterFactory
name=setStatusGatewayFilterFactory
name=saveSessionGatewayFilterFactory
name=stripPrefixGatewayFilterFactory
name=requestHeaderToRequestUriGatewayFilterFactory
name=requestSizeGatewayFilterFactory
name=requestHeaderSizeGatewayFilterFactory
試驗:使用AddRequestHeaderGatewayFilterFactory
# 過濾器配置
filters:
# 區分大小寫
- AddRequestHeader=addHead,abc
訪問/actuator/gateway/routes:filters不再為空了,出現了一個 order=1的filter
[
{
"predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
"route_id": "route1",
"filters": [
"[[AddRequestHeader addHead = 'abc'], order = 1]"
],
"uri": "http://localhost:21001",
"order": 0
}
]
測試請求通過請求到 web適配層應用 是否添加了請求頭:來自博客園
使用Postman看不到!
使用curl命令也看不到:-v 或 --verbose參數!TODO
看來要去 web適配層應用 做一些改造才是啊!
改造代碼及測試結果
// web適配層應用的/user/get接口
@GetMapping(value="/get")
public UserVO getUser(@RequestParam Long id) {
if (Objects.isNull(id) || id < 1) {
return null;
}
log.info("getUser, id={}", id);
// 測試網關的AddRequestHeader過濾器
String addHeadVal = req.getHeader("addHead");
log.info("addHeadVal={}", addHeadVal);
return userFeign.getUser(id);
}
/*
測試結果:請求頭addHead添加成功 日志如下:
o.l.a.web.controller.UserController : addHeadVal=abc
*/
測試通過——AddRequestHeader過濾器生效了。來自博客園
試驗:使用RewritePathGatewayFilterFactory
# 過濾器配置
filters:
- AddRequestHeader=addHead,abc
# RewritePath過濾器,重寫 /web開頭的請求——去掉/web
- RewritePath=/web/(?<segment>.*), /$\{segment}
訪問 /actuator/gateway/routes:過濾器order=2
[
{
"predicate": "After: 2021-09-11T14:13:13+08:00[Asia/Shanghai]",
"route_id": "route1",
"filters": [
"[[AddRequestHeader addHead = 'abc'], order = 1]",
"[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]"
],
"uri": "http://localhost:21001",
"order": 0
}
]
執行結果:請求成功
: [c3147423-1, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:65469] HTTP GET "/web/user/get?id=1"
: [c3147423-1, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:65469] Completed 200 OK
當然,沒有被改造的 /user/get?id=1 也請求成功——可以通過 Path斷言過濾掉 /user開頭的請求:來自博客園
Path斷言使用
predicates:
配置中增加 Path斷言:
# After斷言
# 嚴格按照下面的格式來,小於10的話,前加0
- After=2021-09-11T14:13:13.000+08:00[Asia/Shanghai]
# 路徑斷言:只接受 /web 開頭的請求,,配合下面的 RewritePath過濾器一起使用
- Path=/web/**
# 過濾器配置
filters:
- AddRequestHeader=addHead,abc
# RewritePath過濾器,重寫 /web開頭的請求——去掉開頭的/web
- RewritePath=/web/(?<segment>.*), /$\{segment}
添加 Path斷言后,/user/get?id=1 訪問失敗:
{
"timestamp": "2021-09-11T14:25:35.873+00:00",
"path": "/user/get",
"status": 404,
"error": "Not Found",
"message": null,
"requestId": "73eb6d32-1, L:/0:0:0:0:0:0:0:1:25001 - R:/0:0:0:0:0:0:0:1:62670"
}
關於RewritePathGatewayFilterFactory的用法,還沒搞懂,需要繼續深入。來自博客園
它可以取代強大的Nginx的rewrite嗎?
內置的過濾器工廠可以滿足很多場景的需求了。在不滿足更多需求時,可以自定義過濾器或過濾器工廠。
過濾器工廠的相關接口和抽象類:
// 接口
@FunctionalInterface
public interface GatewayFilterFactory<C> extends ShortcutConfigurable, Configurable<C> {
}
// 抽象類1 上面頂級接口的直接抽象類:接收1個參數
public abstract class AbstractGatewayFilterFactory<C> extends AbstractConfigurable<C>
implements GatewayFilterFactory<C>, ApplicationEventPublisherAware {
}
// 抽象類2 繼承 上面的 抽象類1:?
public abstract class AbstractChangeRequestUriGatewayFilterFactory<T> extends AbstractGatewayFilterFactory<T> {
}
// 抽象類3 繼承 上面的 抽象類1:接收2個參數
public abstract class AbstractNameValueGatewayFilterFactory
extends AbstractGatewayFilterFactory<AbstractNameValueGatewayFilterFactory.NameValueConfig> {
}
本試驗展示 過濾器工廠的實現。
需參考其它內置工廠的實現,實現自己的過濾器工廠。
其中還涉及到reactor的相關內容——Mono類。
自定義過濾器工廠功能:
記錄請求耗時,並根據配置(一個參數-true/false)決定是否輸出日志。來自博客園
實現簡介:
實現AbstractGatewayFilterFactory接口,注冊為Spring容器管理的Bean,然后就可以在配置文件中使用了。
RequestTimeGatewayFilterFactory.java
package org.lib.external.gateway.filters;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 請求時間日志輸出
* 一個參數:true-輸出,false-不輸出
* @author ben
* @date 2021-09-13 09:45:28 CST
*/
public class RequestTimeGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestTimeGatewayFilterFactory.Config> {
// 為什么是 GatewayFilter.class 而不是 當前類呢?
private static final Log log = LogFactory.getLog(GatewayFilter.class);
private static final String REQUEST_TIME_BEGIN = "reqTimeBegin";
private static final String KEY = "logEnabled";
// 必須,否則不會輸出日志
// 為何實現這個函數?
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(KEY);
}
// 默認構造函數
public RequestTimeGatewayFilterFactory() {
// 必須調用下面的語句,否則拋出 ClassCastException
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
// 匿名類方式(可以轉換為 lambda表達式方式——這是個 函數式接口)
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 添加屬性值
exchange.getAttributes().put(REQUEST_TIME_BEGIN, System.currentTimeMillis());
return chain
.filter(exchange)
.then(
Mono.fromRunnable(()->{
Long startTime = exchange.getAttribute(REQUEST_TIME_BEGIN);
if (config.logEnabled && Objects.nonNull(startTime)) {
// 輸出日志
StringBuilder sb = new StringBuilder();
sb.append(exchange.getRequest().getURI().getRawPath())
.append(": ")
.append(System.currentTimeMillis() - startTime)
.append("毫秒");
log.info(sb.toString());
}
})
);
}};
}
/**
* RequestTimeGatewayFilter的配置
* @author ben
* @date 2021-09-13 09:43:35 CST
*/
public static class Config {
/**
* true: 輸出日志;false:不輸出日志
*/
private boolean logEnabled;
public boolean isLogEnabled() {
return logEnabled;
}
public void setLogEnabled(boolean logEnabled) {
this.logEnabled = logEnabled;
}
}
}
APPConfig.java
@Configuration
public class APPConfig {
/**
* 注冊新增過濾器工廠
* 注冊后,即可在配置文件中使用
*/
@Bean
public RequestTimeGatewayFilterFactory RequestTimeGatewayFilterFactory() {
return new RequestTimeGatewayFilterFactory();
}
}
使用RequestTimeGatewayFilterFactory:最后一行的配置,值為true,表示輸出日志
predicates:
# After斷言
# 嚴格按照下面的格式來,小於10的話,前加0
- After=2021-09-11T14:13:13.000+08:00[Asia/Shanghai]
# 路徑斷言:只接受 /web 開頭的請求,,配合下面的 RewritePath過濾器一起使用
- Path=/web/**
# 過濾器配置
filters:
- AddRequestHeader=addHead,abc
# RewritePath過濾器,重寫 /web開頭的請求——去掉/web
- RewritePath=/web/(?<segment>.*), /$\{segment}
# 自定義過濾器工廠:請求時間日志輸出
- RequestTime=true
訪問/actuator/gateway/routes:
[
{
"predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
"route_id": "route1",
"filters": [
"[[AddRequestHeader addHead = 'abc'], order = 1]",
"[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]",
"[org.lib.external.gateway.filters.RequestTimeGatewayFilterFactory$1@19a435c6, order = 3]"
],
"uri": "http://localhost:21001",
"order": 0
}
]
自定義路由過濾器工廠 配置成功。來自博客園
但是,其展示的信息 和 內置過濾器工廠 很不一樣,和是否重寫ToString()有關系?是的,改造如下:
@Override
public GatewayFilter apply(Config config) {
// 匿名類方式(可以轉換為 lambda表達式方式——這是個 函數式接口)
return new GatewayFilter() {
// ...省略了之前的filter函數...
// 重寫
@Override
public String toString() {
return "[RequestTime logEnabled=" + config.isLogEnabled() + "]";
}
};
}
改造后訪問 /actuator/gateway/routes:改造成功
[
{
"predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
"route_id": "route1",
"filters": [
"[[AddRequestHeader addHead = 'abc'], order = 1]",
"[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]",
"[[RequestTime logEnabled=true], order = 3]"
],
"uri": "http://localhost:21001",
"order": 0
}
]
測試自定義過濾器工廠是否生效:成功。
更改工廠類中的 log 的參數:
配置文件中,值為false的時候是沒有日志輸出的。
注,功能雖然實現了,但還需要深入了解才行,靜態內部類Config、ServerWebExchange、Mono、GatewayFilterChain等。
注,實現的過濾器工廠是根據 參考文檔1 中的實現的,本來是實現一個 是否打印請求參數——query params——的工廠:只要使用就會有日志,只不過是否輸出 請求參數,而本文改為了 是否輸出日志,設置為false的時候,沒有日志、還會影響性能。來自博客園
注,過濾器是一種類型,除了使用過濾器工廠來生產之外,還可以自定義過濾器類——實現GatewayFilter、Ordered接口即可。實現過濾器后,可以通過編碼建立路由的方式(本文暫未涉及)使用,或者,建立對應的過濾器工廠使用——此時怎么使用工廠中的配置呢?TODO
前面介紹的過濾器都是 單個路由的過濾器(GatewayFilter),還有一種 全局過濾器(GlobalFilter)——作用在所有路由上。
對於GatewayFilter,除了可以配置給單個路由使用,也可以通過下面的配置讓其全局生效(spring.cloud.gateway.default-filters):
配置及結果
# application.yml文件
#
# 路由配置
gateway:
# 配置2個GatewayFilter全局生效
default-filters:
- AddResponseHeader=X-Response-Default-Red, Default-Blue
- RequestTime=true
routes:
...省略...
# 訪問/actuator/gateway/routes
# 注意 order值,,系統自動排的
[
{
"predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
"route_id": "route1",
"filters": [
"[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
"[[AddRequestHeader addHead = 'abc'], order = 1]",
"[[RequestTime logEnabled=true], order = 2]",
"[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]"
],
"uri": "http://localhost:21001",
"order": 0
}
]
測試結果:響應頭-已添加、日志-正常。
全局過濾器的接口 及其實現類 前文展示過了,但在spring容器中有哪些Bean是全局過濾器呢?
點擊查看代碼
# 測試代碼 ConfigurableApplicationContext ctx
String[] beanNames = ctx.getBeanDefinitionNames();
Arrays.stream(beanNames).forEach((name)->{
Object bean = ctx.getBean(name);
if (bean instanceof GlobalFilter) {
cs.accept("name=" + name);
}
});
# 測試結果
name=routingFilter
name=nettyWriteResponseFilter
name=adaptCachedBodyGlobalFilter
name=removeCachedBodyFilter
name=routeToRequestUrlFilter
name=forwardRoutingFilter
name=forwardPathFilter
name=websocketRoutingFilter
name=gatewayMetricFilter
name=noLoadBalancerClientFilter
還可以使用 /actuator/gateway/globalfilters 端點查看系統的所有全局過濾器:
{
"org.springframework.cloud.gateway.filter.ForwardRoutingFilter@44bd4b0a": 2147483647,
"org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@1a865273": -1,
"org.springframework.cloud.gateway.filter.NettyRoutingFilter@26844abb": 2147483647,
"org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@288ca5f0": -2147483648,
"org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@4068102e": 10000,
"org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@1aa6e3c0": -2147482648,
"org.springframework.cloud.gateway.filter.GatewayMetricsFilter@21079a12": 0,
"org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@216e0771": 2147483646,
"org.springframework.cloud.gateway.filter.ForwardPathFilter@6c008c24": 0,
"org.springframework.cloud.gateway.config.GatewayNoLoadBalancerClientAutoConfiguration$NoLoadBalancerClientFilter@fcc6023": 10150
}
GlobalFilter也可以自定義,參考其它實現類,其都實現了 GlobalFilter、Ordered接口。來自博客園
自定義后,將其注冊到Spring容器即可全局生效。
實現一個GlobalFilter,功能:檢查請求頭是否有token參數,沒有的話,禁止訪問系統
TokenGlobalFilter.java
package org.lib.external.gateway.filters;
import java.util.Objects;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* Token檢查
* 功能:請求頭 有token 放行;無token 阻止。
* 進一步:檢查token是否有效——結合S.C.Security TODO
* @author ben
* @date 2021-09-13 11:46:03 CST
*/
public class TokenGlobalFilter implements GlobalFilter, Ordered {
private static final Log log = LogFactory.getLog(TokenGlobalFilter.class);
private static final String TOKEN = "token";
@Override
public int getOrder() {
// 參考文檔1 中,這里設置為 -100,,兩個值都有效,-100的優先級更高
return 0;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst(TOKEN);
if (!StringUtils.hasText(token)) {
// 沒有token 阻止訪問
log.warn("請求沒有token或token無效,禁止訪問: url=" + exchange.getRequest().getURI());
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 還可以做安全校驗 TODO
// 放行
return chain.filter(exchange);
}
}
/**
* 全局過濾器注冊:TokenGlobalFilter
* @author ben
* @date 2021-09-13 11:54:28 CST
* @return
*/
@Bean
public TokenGlobalFilter tokenGlobalFilter() {
return new TokenGlobalFilter();
}
測試結果:請求頭 沒有token 或 token值為空字符串時,輸入日志,返回空,,通過。來自博客園
o.l.e.gateway.filters.TokenGlobalFilter : 請求沒有token或token無效,禁止訪問: url=http://localhost:25001/web/user/get?id=1
訪問 /actuator/gateway/globalfilters 端點,可以看到 自定義的全局過濾器:
{
...
"org.lib.external.gateway.filters.TokenGlobalFilter@312b34e3": 0,
...
}
本文介紹了:
1)在配置文件中添加路由;
2)在配置文件中使用斷言;
3)在配置文件中配置過濾器GatewayFilter;來自博客園
4)自定義GatewayFilter;
5)配置GatewayFilter為全局過濾器;
6)自定義全局過濾器GlobalFilter等內容;
...
基本上可以讓S.C.Gateway運行起來了。來自博客園
不過,Gateway還有更多內容需要研究的,比如,編程方式實現gaeway配置、服務化配合(結合服務注冊中心)、實現限流等……
》》》全文完《《《
還需要多看官文、源碼,這才可以get到更多、更准確的信息。
使用S.C.Gateway的最佳實踐是怎樣的呢?待探索、實踐。來自博客園
先看官文,再寫博文,效率會更高的。
1、《深入理解Spring Cloud與微服務構建》
2019年9月第2版,作者:方志朋
2、