spring cloud項目06:網關(Gateway)(1)


JAVA 8

spring boot 2.5.2

spring cloud 2020.0.3

---

 

授人以漁:

1、Spring Cloud PDF版本

最新版本,下載下來,以便查閱。

更多版本的官方文檔:

https://docs.spring.io/spring-cloud/docs/

2、Spring Cloud Gateway

沒有PDF版本,把網頁保存下來。

 

本文使用的項目:

主要路徑:前端請求經過 external.gateway 轉發到 adapter.web。在此過程中,會做一些試驗。

external.gateway  網關服務 端口 25001
adapter.web web適配層應用 端口 21001
data.user user數據層應用 端口 20001
eureka.server Eureka注冊中心 端口 10001

 

目錄

0、序章

Spring Cloud Gateway簡介

1、通過網關服務訪問其它應用

更多斷言試驗

2、過濾器使用

試驗:自定義過濾器工廠

試驗:全局過濾器

參考文檔

 

0、序章

建立項目,引入 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.的官文中會有詳情(下圖來自官網)。

 

Spring Cloud Gateway簡介

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過濾器邏輯可以:對響應數據進行修改,比如更改響應頭、轉換協議等。

 

1、通過網關服務訪問其它應用

訪問 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過濾器,之后才到達  服務代理(或應用),下一節將介紹過濾器的使用。來自博客園

 

2、過濾器使用

過濾器,從處理對象分為兩種: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、

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM