為什么要有服務(wù)網(wǎng)關(guān)?
我們都知道在微服務(wù)架構(gòu)中,系統(tǒng)會(huì)被拆分為很多個(gè)微服務(wù)。那么作為客戶端要如何去調(diào)用這么多的微服務(wù)呢璃弄?難道要一個(gè)個(gè)的去調(diào)用嗎哥捕?很顯然這是不太實(shí)際的牧抽,我們需要有一個(gè)統(tǒng)一的接口與這些微服務(wù)打交道,這就是我們需要服務(wù)網(wǎng)關(guān)的原因遥赚。
我們已經(jīng)知道扬舒,在微服務(wù)架構(gòu)中,不同的微服務(wù)可以有不同的網(wǎng)絡(luò)地址凫佛,各個(gè)微服務(wù)之間通過(guò)互相調(diào)用完成用戶請(qǐng)求讲坎,客戶端可能通過(guò)調(diào)用N個(gè)微服務(wù)的接口完成一個(gè)用戶請(qǐng)求。比如:用戶查看一個(gè)商品的信息愧薛,它可能包含商品基本信息晨炕、價(jià)格信息、評(píng)論信息毫炉、折扣信息瓮栗、庫(kù)存信息等等,而這些信息獲取則來(lái)源于不同的微服務(wù)瞄勾,諸如產(chǎn)品系統(tǒng)遵馆、價(jià)格系統(tǒng)、評(píng)論系統(tǒng)丰榴、促銷系統(tǒng)货邓、庫(kù)存系統(tǒng)等等,那么要完成用戶信息查看則需要調(diào)用多個(gè)微服務(wù)四濒,這樣會(huì)帶來(lái)幾個(gè)問(wèn)題:
- 客戶端多次請(qǐng)求不同的微服務(wù)换况,增加客戶端代碼或配置編寫的復(fù)雜性
- 認(rèn)證繁雜职辨,訪問(wèn)每個(gè)服務(wù)都要進(jìn)行一次認(rèn)證
- 每個(gè)服務(wù)都通過(guò)http訪問(wèn),導(dǎo)致http請(qǐng)求增加戈二,效率不高拖慢系統(tǒng)性能
- 多個(gè)服務(wù)存在跨域請(qǐng)求問(wèn)題舒裤,處理起來(lái)比較復(fù)雜
我們?cè)撊绾谓鉀Q這些問(wèn)題呢?我們可以嘗試想一下觉吭,不要讓前端直接知道后臺(tái)諸多微服務(wù)的存在腾供,我們的系統(tǒng)本身就是從業(yè)務(wù)領(lǐng)域的層次上進(jìn)行劃分,形成多個(gè)微服務(wù)鲜滩,這是后臺(tái)的處理方式伴鳖。對(duì)于前臺(tái)而言,后臺(tái)應(yīng)該仍然類似于單體應(yīng)用一樣徙硅,一次請(qǐng)求即可榜聂,于是我們可以在客戶端和服務(wù)端之間增加一個(gè)API網(wǎng)關(guān),所有的外部請(qǐng)求先通過(guò)這個(gè)微服務(wù)網(wǎng)關(guān)嗓蘑。它只需跟網(wǎng)關(guān)進(jìn)行交互须肆,而由網(wǎng)關(guān)進(jìn)行各個(gè)微服務(wù)的調(diào)用。
這樣的話桩皿,我們就可以解決上面提到的問(wèn)題豌汇,同時(shí)開(kāi)發(fā)就可以得到相應(yīng)的簡(jiǎn)化,還可以有如下優(yōu)點(diǎn):
- 減少客戶端與微服務(wù)之間的調(diào)用次數(shù)泄隔,提高效率
- 便于監(jiān)控瘤礁,可在網(wǎng)關(guān)中監(jiān)控?cái)?shù)據(jù),可以做統(tǒng)一切面任務(wù)處理
- 便于認(rèn)證梅尤,只需要在網(wǎng)關(guān)進(jìn)行認(rèn)證即可,無(wú)需每個(gè)微服務(wù)都進(jìn)行認(rèn)證
- 降低客戶端調(diào)用服務(wù)端的復(fù)雜度
這里可以聯(lián)想到一個(gè)概念岩调,面向?qū)ο笤O(shè)計(jì)中的門面模式巷燥,即對(duì)客戶端隱藏細(xì)節(jié),API網(wǎng)關(guān)也是類似的東西号枕,只不過(guò)叫法不同而已缰揪。它是系統(tǒng)的入口,封裝了應(yīng)用程序的內(nèi)部結(jié)構(gòu)葱淳,為客戶端提供統(tǒng)一服務(wù)钝腺,一些與業(yè)務(wù)本身功能無(wú)關(guān)的公共邏輯可以在這里實(shí)現(xiàn),諸如認(rèn)證赞厕、鑒權(quán)艳狐、監(jiān)控、緩存皿桑、負(fù)載均衡毫目、流量管控蔬啡、路由轉(zhuǎn)發(fā)等等。示意圖:
總結(jié)一下镀虐,服務(wù)網(wǎng)關(guān)大概就是四個(gè)功能:統(tǒng)一接入箱蟆、流量管控、協(xié)議適配刮便、安全維護(hù)空猜。而在目前的網(wǎng)關(guān)解決方案里,有Nginx+ Lua恨旱、Spring Cloud Zuul以及Spring Cloud Gateway等等辈毯。這里以Spring Cloud Gateway為例進(jìn)行說(shuō)明。
Spring Cloud Gateway簡(jiǎn)介
Spring Cloud Gateway是Spring Cloud體系的第二代網(wǎng)關(guān)組件窖杀,基于Spring 5.0的新特性WebFlux進(jìn)行開(kāi)發(fā)漓摩,底層網(wǎng)絡(luò)通信框架使用的是Netty,所以其吞吐量高入客、性能強(qiáng)勁管毙,未來(lái)將會(huì)取代第一代的網(wǎng)關(guān)組件Zuul。Spring Cloud Gateway可以通過(guò)服務(wù)發(fā)現(xiàn)組件自動(dòng)轉(zhuǎn)發(fā)請(qǐng)求桌硫,默認(rèn)集成了Ribbon做負(fù)載均衡夭咬,以及默認(rèn)使用Hystrix對(duì)網(wǎng)關(guān)進(jìn)行保護(hù),當(dāng)然也可以選擇其他的容錯(cuò)組件铆隘,例如Sentinel
優(yōu)點(diǎn):
- 性能強(qiáng)勁:是第一代網(wǎng)關(guān)Zuul的1.6倍
- 功能強(qiáng)大:內(nèi)置了很多實(shí)用的功能卓舵,例如轉(zhuǎn)發(fā)、監(jiān)控膀钠、限流等
- 設(shè)計(jì)優(yōu)雅掏湾,容易擴(kuò)展
缺點(diǎn):
- 其實(shí)現(xiàn)依賴Netty與WebFlux,不是傳統(tǒng)的Servlet編程模型肿嘲,有一定的學(xué)習(xí)成本
- 不能在Servlet容器下工作融击,也不能構(gòu)建成WAR包,即不能將其部署在Tomcat雳窟、Jetty等Servlet容器里尊浪,只能打成jar包執(zhí)行
- 不支持Spring Boot 1.x,需2.0及更高的版本
核心概念
-
Route(路由)
Spring Cloud Gateway的基礎(chǔ)元素封救,可簡(jiǎn)單理解成一條轉(zhuǎn)發(fā)規(guī)則拇涤。包含:ID、目標(biāo)URL誉结、Predicate集合以及Filter集合
這是一段比較典型的Gateway路由配置:
spring:
cloud:
gateway:
routes:
- id: user-center # 唯一標(biāo)識(shí)鹅士,通常使用服務(wù)id
uri: lb://user-center # 目標(biāo)URL,lb代表從注冊(cè)中心獲取服務(wù)惩坑,lb是Load Balance的縮寫
predicates:
# Predicate集合
- Path=/zj/cloud/v1/user-center/** # 匹配轉(zhuǎn)發(fā)路徑
filters:
# Filter集合
- StripPrefix=4 # 從第幾級(jí)開(kāi)始轉(zhuǎn)發(fā)
2. Predicate(謂詞)
即java.util.function.Predicate
這個(gè)接口如绸,Gateway使用Predicate實(shí)現(xiàn)路由的匹配條件
3. Filter(過(guò)濾器)
與我們平時(shí)使用的Servlet編程模型里的過(guò)濾器概念類似嘱朽,同樣可以用于修改請(qǐng)求以及響應(yīng)數(shù)據(jù),可以利用Filter實(shí)現(xiàn)鑒權(quán)怔接、訪問(wèn)日志記錄搪泳,接口耗時(shí)記錄等功能
Spring Cloud Gateway架構(gòu)圖
流程介紹:
Gateway Client
發(fā)送請(qǐng)求給Spring Cloud Gateway
,Gateway Handler Mapping
會(huì)判斷請(qǐng)求的路徑是否匹配路由的配置扼脐,如果匹配則會(huì)進(jìn)入Gateway Web Handler
岸军,Web Handler
會(huì)讀取路由上所配置的過(guò)濾器,然后將該請(qǐng)求交給過(guò)濾器去處理瓦侮,最后轉(zhuǎn)發(fā)到路由配置的微服務(wù)上
- Gateway Client:泛指外部請(qǐng)求艰赞,例如瀏覽器、app肚吏、小程序等
- Proxied Service:指的是被網(wǎng)關(guān)代理的微服務(wù)
相關(guān)源碼:
- Gateway Handler Mapping:
org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping
- Gateway Web Handler:
org.springframework.cloud.gateway.handler.FilteringWebHandler
創(chuàng)建Spring Cloud Gateway項(xiàng)目
這里使用IDEA的Spring Initializr進(jìn)行項(xiàng)目的創(chuàng)建方妖,到選擇依賴這一步勾選gateway依賴,如下圖:
網(wǎng)關(guān)組件一般都配合服務(wù)發(fā)現(xiàn)組件使用罚攀,我這里使用Nacos作為服務(wù)發(fā)現(xiàn)組件党觅,具體的依賴如下:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Nacos Client -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!--整合Spring Cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--整合Spring Cloud Alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
然后編寫配置文件內(nèi)容如下:
server:
port: 8040
spring:
application:
name: gateway
cloud:
nacos:
discovery:
# 指定nacos server的地址
server-addr: 127.0.0.1:8848
gateway:
discovery:
locator:
# 讓gateway通過(guò)服務(wù)發(fā)現(xiàn)組件找到其他的微服務(wù),從而自動(dòng)轉(zhuǎn)發(fā)請(qǐng)求
enabled: true
# actuator相關(guān)配置
management:
endpoints:
web:
exposure:
# 暴露所有監(jiān)控端點(diǎn)
include: '*'
endpoint:
health:
# 總是顯示健康檢測(cè)詳情
show-details: always
完成以上步驟后斋泄,我們來(lái)啟動(dòng)這個(gè)網(wǎng)關(guān)服務(wù)杯瞻,進(jìn)行一個(gè)簡(jiǎn)單的測(cè)試,看看是否能將請(qǐng)求正常地轉(zhuǎn)發(fā)到指定的微服務(wù)上炫掐。此時(shí)有一個(gè)名為user-center的微服務(wù)魁莉,該微服務(wù)有一個(gè)按id獲取用戶信息的接口,接口路徑為/users/{id}募胃。若通過(guò)網(wǎng)關(guān)服務(wù)來(lái)訪問(wèn)這個(gè)接口旗唁,要如何做呢?很簡(jiǎn)單痹束,gateway配合服務(wù)發(fā)現(xiàn)組件使用時(shí)检疫,會(huì)有一個(gè)默認(rèn)的轉(zhuǎn)發(fā)規(guī)則,如下:
-
${GATEWAY_URL}/{微服務(wù)名稱}/{接口路徑}
所以按該規(guī)則得出來(lái)的具體url為:localhost:8040/user-center/users/{id}
参袱,訪問(wèn)結(jié)果如下:
從測(cè)試結(jié)果可以看到,gateway可以根據(jù)url上的微服務(wù)名稱將訪問(wèn)請(qǐng)求轉(zhuǎn)發(fā)到該微服務(wù)上秽梅。
以上這種是Gateway最簡(jiǎn)單的使用方式抹蚀,但通常在實(shí)際開(kāi)發(fā)中,可能不希望使用默認(rèn)的轉(zhuǎn)發(fā)規(guī)則企垦,因?yàn)檫@種方式不太靈活环壤,例如一些服務(wù)接口是存在版本劃分的,需要根據(jù)不同版本的訪問(wèn)路徑轉(zhuǎn)發(fā)到不同版本的微服務(wù)上钞诡。此時(shí)就需要自定義轉(zhuǎn)發(fā)路由郑现,實(shí)際上在第一小節(jié)的時(shí)候就已經(jīng)給出過(guò)配置示例了湃崩。修改配置如下:
spring:
cloud:
gateway:
routes:
- id: user-center # 唯一標(biāo)識(shí),通常使用服務(wù)id
uri: lb://user-center # 目標(biāo)URL接箫,lb代表從注冊(cè)中心獲取服務(wù)
predicates:
# Predicate集合
- Path=/zj/cloud/v1/user-center/** # 匹配轉(zhuǎn)發(fā)路徑
filters:
# Filter集合
- StripPrefix=4 # 從第幾級(jí)開(kāi)始轉(zhuǎn)發(fā)攒读,數(shù)字從0開(kāi)始
自定義路由的注意事項(xiàng):
- predicates配置項(xiàng)必須有,且必須配置一個(gè)及以上的Predicate辛友,但不一定非要配置Path薄扁,可以配置其他的Predicate,例如After废累、Before等邓梅,此時(shí)Path的默認(rèn)值為/**
重啟項(xiàng)目,此時(shí)訪問(wèn)的url為:localhost:8040/zj/cloud/v1/user-center/users/{id}邑滨,訪問(wèn)結(jié)果如下:
路由配置的兩種形式
Spring Cloud Gateway的路由配置有兩種形式日缨,分別是路由到指定的URL
以及路由到指定的微服務(wù)
。在這兩種形式中掖看,均支持訪問(wèn)路徑的通配及精確匹配匣距,在之前的示例中我們只使用了通配。這里將給出具體的配置示例乙各,以此直觀的了解這兩種形式及不同匹配方式在配置上的區(qū)別墨礁。
-
路由到指定的URL
通配,使用通配符/**進(jìn)行匹配耳峦,示例:spring: cloud: gateway: routes: - id: test_route # 路由的唯一標(biāo)識(shí) uri: http://www.xxx.com predicates: # 使用通配符匹配 - Path=/**
該配置使訪問(wèn)
GATEWAY_URL/**
時(shí)會(huì)轉(zhuǎn)發(fā)到http://www.xxx.com/
精確匹配恩静,配置具體的接口路徑即可,示例:spring: cloud: gateway: routes: - id: test_route # 路由的唯一標(biāo)識(shí) uri: http://www.xxx.com/user/order/detail predicates: # 指定具體的路徑進(jìn)行匹配 - Path=/user/order/detail
該配置使訪問(wèn)
GATEWAY_URL/user/order/detail
時(shí)會(huì)轉(zhuǎn)發(fā)到ttp://www.xxx.com/user/order/detail
-
路由到指定的微服務(wù)
通配蹲坷,示例:spring: cloud: gateway: routes: - id: user-center # 路由的唯一標(biāo)識(shí)驶乾,這種形式下通常是微服務(wù)名稱 uri: lb://user-center # lb代表從注冊(cè)中心獲取服務(wù) predicates: # 使用通配符匹配 - Path=/**
該配置使訪問(wèn)
GATEWAY_URL/**
時(shí)會(huì)轉(zhuǎn)發(fā)到 user-center微服務(wù)的/**
精確匹配,示例:spring: cloud: gateway: routes: - id: user-center # 路由的唯一標(biāo)識(shí)循签,這種形式下通常是微服務(wù)名稱 uri: lb://user-center/users/info # lb代表從注冊(cè)中心獲取服務(wù) predicates: # 指定具體的路徑進(jìn)行匹配 - Path=/users/info
該配置使訪問(wèn) GATEWAY_URL/users/info 時(shí)會(huì)轉(zhuǎn)發(fā)到 user-center微服務(wù)的/users/info
路由謂詞工廠
前面提到過(guò)謂詞是路由的判斷條件级乐,而路由謂詞工廠就是作用到指定路由上的一堆謂詞判斷條件。在之前的示例里县匠,我們就已經(jīng)使用過(guò)路由謂詞工廠了风科,就是自定義轉(zhuǎn)發(fā)路徑時(shí)所配置的Path。
內(nèi)置的路由謂詞工廠
Spring Cloud Gateway內(nèi)置了眾多路由謂詞工廠乞旦,這些路由謂詞工廠為路由匹配的判斷提供了有力的支持贼穆,而我們之前所使用的Path就是內(nèi)置的路由謂詞工廠之一,用于判斷當(dāng)前訪問(wèn)的接口路徑是否與該路由所配置的路徑相匹配兰粉,若匹配則進(jìn)行轉(zhuǎn)發(fā)故痊。由于Gateway內(nèi)置的路由謂詞工廠比較多,篇幅有限就不在本文中介紹了玖姑,可以參考另一篇文章:
自定義路由謂詞工廠
現(xiàn)在我們已經(jīng)知道Spring Cloud Gateway內(nèi)置了一系列的路由謂詞工廠愕秫,但如果這些內(nèi)置的路由謂詞工廠不能滿足業(yè)務(wù)需求的話慨菱,我們可以自定義路由謂詞工廠來(lái)實(shí)現(xiàn)特定的需求。例如有某個(gè)服務(wù)限制用戶只允許在09:00 - 17:00這個(gè)時(shí)間段內(nèi)才可以訪問(wèn)戴甩,內(nèi)置的路由謂詞工廠是無(wú)法滿足這個(gè)需求的符喝,所以此時(shí)我們就需要自定義能夠?qū)崿F(xiàn)該需求的路由謂詞工廠。
首先定義一個(gè)配置類等恐,用于承載時(shí)間段的配置參數(shù):
@Data
public class TimeBetweenConfig {
/**
* 開(kāi)始時(shí)間
*/
private LocalTime start;
/**
* 結(jié)束時(shí)間
*/
private LocalTime end;
}
然后定義一個(gè)路由謂詞工廠洲劣,具體代碼如下:
import com.zj.node.gateway.config.TimeBetweenConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import java.time.LocalTime;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/**
* 路由謂詞工廠必須以RoutePredicateFactory結(jié)尾,
* 這是Spring Cloud Gateway的約定
**/
@Slf4j
@Component
public class TimeBetweenRoutePredicateFactory extends AbstractRoutePredicateFactory<TimeBetweenConfig> {
public TimeBetweenRoutePredicateFactory() {
super(TimeBetweenConfig.class);
}
/**
* 實(shí)現(xiàn)謂詞判斷的方法
*/
@Override
public Predicate<ServerWebExchange> apply(TimeBetweenConfig config) {
return exchange -> {
LocalTime start = config.getStart();
LocalTime end = config.getEnd();
// 判斷當(dāng)前時(shí)間是否為允許訪問(wèn)的時(shí)間段內(nèi)
LocalTime now = LocalTime.now();
return now.isAfter(start) && now.isBefore(end);
};
}
/**
* 控制配置類(TimeBetweenConfig)屬性和配置文件中配置項(xiàng)(TimeBetween)的映射關(guān)系
*/
@Override
public List<String> shortcutFieldOrder() {
/*
* 例如我們的配置項(xiàng)是:TimeBetween=上午9:00, 下午5:00
* 那么按照順序课蔬,start對(duì)應(yīng)的是上午9:00囱稽;end對(duì)應(yīng)的是下午5:00
**/
return Arrays.asList("start", "end");
}
}
最后需要在配置文件中啟用該路由謂詞工廠,并且需要禁止gateway通過(guò)服務(wù)發(fā)現(xiàn)組件轉(zhuǎn)發(fā)請(qǐng)求到其他的微服務(wù)二跋,修改Gateway相關(guān)配置如下:
spring:
cloud:
gateway:
discovery:
locator:
# 禁止gateway通過(guò)服務(wù)發(fā)現(xiàn)組件轉(zhuǎn)發(fā)請(qǐng)求到其他的微服務(wù)
enabled: false
routes:
- id: user-center
# 目標(biāo)URL战惊,lb代表從注冊(cè)中心獲取服務(wù)
uri: lb://user-center
predicates:
# 注意名稱必須為路由謂詞工廠類名的前綴,參數(shù)為允許訪問(wèn)的時(shí)間段
- TimeBetween=上午9:00,下午5:00
可以看到這里主要是配置了我們自定義的路由謂詞工廠類名的前綴以及允許訪問(wèn)的時(shí)間段扎即,這個(gè)時(shí)間格式不是隨便配置的吞获,而是Spring Cloud Gateway的默認(rèn)時(shí)間格式,相關(guān)源碼如下:
-
org.springframework.format.support.DefaultFormattingConversionService#addDefaultFormatters
時(shí)間格式是可以注冊(cè)的谚鄙,關(guān)于時(shí)間格式注冊(cè)的相關(guān)源碼如下: org.springframework.format.datetime.standard.DateTimeFormatterRegistrar#registerFormatters
另外各拷,這里之所以要禁止gateway通過(guò)服務(wù)發(fā)現(xiàn)組件轉(zhuǎn)發(fā)請(qǐng)求到其他的微服務(wù),是因?yàn)殚_(kāi)啟該配置項(xiàng)的話會(huì)導(dǎo)致我們自定義的路由謂詞工廠不生效闷营。不生效也是有原因的烤黍,開(kāi)啟該配置項(xiàng)會(huì)令Gateway優(yōu)先將請(qǐng)求按照該配置項(xiàng)進(jìn)行轉(zhuǎn)發(fā),那么我們自定義的路由就不會(huì)生效傻盟。
到此為止我們就實(shí)現(xiàn)了一個(gè)自定義路由謂詞工廠速蕊,若此時(shí)不在允許的訪問(wèn)時(shí)間段內(nèi),訪問(wèn)就會(huì)報(bào)404娘赴,如下:
過(guò)濾器工廠
前面提到了過(guò)濾器可以為請(qǐng)求和響應(yīng)添加一些業(yè)務(wù)邏輯或者修改請(qǐng)求和響應(yīng)對(duì)象等规哲,適當(dāng)?shù)厥褂眠^(guò)濾器可以讓我們的工作事半功倍,而本小節(jié)將要介紹的過(guò)濾器工廠就是用來(lái)創(chuàng)建過(guò)濾器的诽表。在此之前我們已經(jīng)學(xué)習(xí)過(guò)路由謂詞工廠了唉锌,而過(guò)濾器工廠與路由謂詞工廠在使用上是類似的,只不過(guò)實(shí)現(xiàn)的功能不一樣竿奏。
內(nèi)置的過(guò)濾器工廠
同樣的Spring Cloud Gateway內(nèi)置了非常多的過(guò)濾器工廠袄简,有二十多個(gè)。通過(guò)這些內(nèi)置的過(guò)濾器工廠就已經(jīng)可以靈活且方便地處理請(qǐng)求和響應(yīng)數(shù)據(jù)议双,由于Gateway內(nèi)置的過(guò)濾器工廠實(shí)在太多痘番,而篇幅有限就不在本文中介紹了捉片,可以參考另一篇文章:
自定義過(guò)濾器工廠
若Spring Cloud Gateway內(nèi)置的過(guò)濾器工廠無(wú)法滿足我們的業(yè)務(wù)需求平痰,那么此時(shí)就需要自定義自己的過(guò)濾器工廠以實(shí)現(xiàn)特定功能汞舱。所謂過(guò)濾器工廠實(shí)際上就是用于創(chuàng)建過(guò)濾器實(shí)例的,而創(chuàng)建的過(guò)濾器實(shí)例都實(shí)現(xiàn)于GatewayFilter接口宗雇。
過(guò)濾器的生命周期:
- Gateway以轉(zhuǎn)發(fā)請(qǐng)求為邊界昂芜,所以其生命周期只包含pre和post:
- pre:Gateway轉(zhuǎn)發(fā)請(qǐng)求之前
- post:Gateway轉(zhuǎn)發(fā)請(qǐng)求之后
自定義過(guò)濾器工廠的方式:
- 繼承
AbstractGatewayFilterFactory
,參考源碼:org.springframework.cloud.gateway.filter.factory.RequestSizeGatewayFilterFactory
赔蒲。使用該方式實(shí)現(xiàn)的過(guò)濾器工廠的配置形式如下:
spring:
cloud:
gateway:
routes:
filters:
# 過(guò)濾器工廠的名稱
- name: RequestSize
# 該過(guò)濾器工廠的參數(shù)
args:
maxSize: 500000
- 繼承
AbstractNameValueGatewayFilterFactory
泌神,參考源碼:org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory
。使用該方式實(shí)現(xiàn)的過(guò)濾器工廠的配置形式如下:
spring:
cloud:
gateway:
routes:
filters:
# 過(guò)濾器工廠的名稱及參數(shù)以name-value的形式配置
- AddRequestHeader=S-Header, Bar
注:AbstractNameValueGatewayFilterFactory
繼承了AbstractGatewayFilterFactory
舞虱,所以實(shí)際上第二種方式是第一種方式的簡(jiǎn)化
核心API:
- exchange.getRequest().mutate().xxx:修改request
- exchange.mutate().xxx:修改exchange
- chain.filter(exchange):傳遞給下一個(gè)過(guò)濾器處理
- exchange.getResponse():獲取響應(yīng)對(duì)象
注:這里的exchange實(shí)際類型為ServerWebExchange
欢际,chain實(shí)際類型為atewayFilter
最后我們來(lái)實(shí)際動(dòng)手編寫一個(gè)自定義過(guò)濾器工廠,需求是記錄訪問(wèn)日志矾兜,這里為了簡(jiǎn)單起見(jiàn)采用第二種方式實(shí)現(xiàn)损趋,具體代碼如下:
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
/**
* 過(guò)濾器工廠必須以GatewayFilterFactory結(jié)尾,
* 這是Spring Cloud Gateway的約定
*
**/
@Slf4j
@Component
public class PreLogGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
// 使用lambda表達(dá)式來(lái)創(chuàng)建GatewayFilter的實(shí)例椅寺,實(shí)際就是匿名內(nèi)部類的簡(jiǎn)寫
return (exchange, chain) -> {
// 通過(guò)config獲取配置的參數(shù)
log.info("配置參數(shù):{}, {}", config.getName(), config.getValue());
// 修改request浑槽,可以添加一些header什么的
ServerHttpRequest modifiedRequest = exchange.getRequest()
.mutate()
.header("X-GatewayHeader","A","B")
.build();
// 打印訪問(wèn)的接口地址
String path = modifiedRequest.getURI().getPath();
log.info("訪問(wèn)的接口為:{}", path);
// 修改exchange
ServerWebExchange modifiedExchange = exchange.mutate()
.request(modifiedRequest).build();
// 傳遞給下一個(gè)過(guò)濾器處理
return chain.filter(modifiedExchange);
};
}
}
最后需要添加相關(guān)配置以啟用這個(gè)過(guò)濾器工廠,如下:
spring:
cloud:
gateway:
routes:
- id: user-center
uri: lb://user-center
predicates:
- TimeBetween=上午9:00,下午5:00
filters:
# 名稱必須為過(guò)濾器工廠類名的前綴返帕,并且參數(shù)只能有兩個(gè)桐玻,因?yàn)镹ameValueConfig里只定義了兩個(gè)屬性
- PreLog=testName,testValue
啟動(dòng)項(xiàng)目,訪問(wèn)user-center的接口
全局過(guò)濾器
現(xiàn)在我們已經(jīng)知道前面所介紹的過(guò)濾器工廠實(shí)際用于創(chuàng)建GatewayFilter
實(shí)例荆萤,并且這些GatewayFilter
實(shí)例僅作用于指定的路由上镊靴,那么有沒(méi)有可以作用于全部路由上的過(guò)濾器呢?答案是有的观腊,這就是本小節(jié)將要介紹的全局過(guò)濾器邑闲。Spring Cloud Gateway默認(rèn)就內(nèi)置了許多全局過(guò)濾器,本文僅介紹如何自定義全局過(guò)濾器梧油,關(guān)于Gateway內(nèi)置的過(guò)濾器可以參考另一篇文章:
自定義全局過(guò)濾需要實(shí)現(xiàn)GlobalFilter
接口苫耸,該接口和 GatewayFilter
有一樣的方法定義,只不過(guò) GlobalFilter
的實(shí)例會(huì)作用于所有的路由儡陨。
Tips:
官方聲明:GlobalFilter的接口定義以及用法在未來(lái)的版本可能會(huì)發(fā)生變化褪子。
個(gè)人判斷:GlobalFilter可用于生產(chǎn);如果有自定義GlobalFilter的需求骗村,理論上也可放心使用嫌褪。因?yàn)槲磥?lái)即使接口定義以及使用方式發(fā)生變化,理應(yīng)也是平滑過(guò)渡的(比如Zuul的Fallback胚股,原先叫ZuulFallbackProvider笼痛,后來(lái)改叫FallbackProvider,中間就有段時(shí)間新舊使用方式都支持,后面才逐步廢棄老的使用方式)缨伊。
接下來(lái)我們自定義一個(gè)全局過(guò)濾器摘刑,需求是打印訪問(wèn)的接口路徑以及打印該接口的訪問(wèn)耗時(shí)。具體代碼如下:
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 自定義全局過(guò)濾器
*
**/
@Slf4j
public class MyGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
log.info("[MyGlobalFilter] 訪問(wèn)的接口:{}", path);
long start = System.currentTimeMillis();
return chain.filter(exchange)
// then的內(nèi)容會(huì)在過(guò)濾器返回的時(shí)候執(zhí)行刻坊,即最后執(zhí)行
.then(Mono.fromRunnable(() ->
log.info("[ {} ] 接口的訪問(wèn)耗時(shí):{} /ms",
path, System.currentTimeMillis() - start))
);
}
}
最后需要使該全局過(guò)濾器生效枷恕,方法有很多種,可以直接在該類上加@Component注解谭胚,也可以通過(guò)代碼配置(@Bean)徐块,還有其他的一些方式。這里個(gè)人比較傾向于使用一個(gè)專門的配置類去實(shí)例化這些全局過(guò)濾器并交給Spring容器管理灾而。代碼如下:
import com.zj.node.gateway.filter.MyGlobalFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
@Slf4j
@Configuration
public class FilterConfig {
@Bean
// 該注解用于指定過(guò)濾器的執(zhí)行順序胡控,數(shù)字越小越優(yōu)先執(zhí)行
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter myGlobalFilter(){
log.info("create myGlobalFilter...");
return new MyGlobalFilter();
}
}
啟動(dòng)項(xiàng)目,看看我們自定義的全局過(guò)濾器是否已生效旁趟。