Gateway 中是否能寫 Servlet Filter
這個(gè)問題我之前也困惑過一陣子。因?yàn)閯偨佑| Spring Cloud Gateway 的時(shí)候,腦子里還帶著以前用 Spring MVC 或者傳統(tǒng) Web 應(yīng)用的思維——動(dòng)不動(dòng)就想加個(gè) Filter 來處理請(qǐng)求日志、鑒權(quán)、跨域啥的。
但后來發(fā)現(xiàn),Gateway 和傳統(tǒng)的 Servlet 容器壓根不是一個(gè)路子。今天就聊聊我的理解,順便說說怎么在 Gateway 里“做類似 Filter 的事”。
Gateway 不是跑在 Servlet 容器上的
這點(diǎn)很重要。Spring Cloud Gateway 是基于 Reactor 模型構(gòu)建的,底層用的是 Netty,而不是 Tomcat、Jetty 這些我們熟悉的 Servlet 容器。
而 javax.servlet.Filter(或者 jakarta.servlet.Filter)這個(gè)接口,是 Servlet 規(guī)范的一部分。它只能在支持 Servlet 的容器里運(yùn)行。所以,如果你硬要在 Gateway 項(xiàng)目里寫一個(gè) @Component 注解的 Filter,它根本不會(huì)被調(diào)用——因?yàn)?Netty 根本不認(rèn)識(shí)它。
我一開始不信邪,寫了下面這段代碼:
@Component
public class MyServletFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("我在 Gateway 里打印這句話");
chain.doFilter(request, response);
}
}結(jié)果?啟動(dòng)正常,但發(fā)任何請(qǐng)求,控制臺(tái)都沒輸出。白忙活一場(chǎng)。
那 Gateway 用什么代替 Filter?
答案是:GlobalFilter 和 GatewayFilter。
這兩兄弟才是 Gateway 世界的“過濾器”。它們不是 Servlet 那一套,而是響應(yīng)式編程模型下的組件。
全局過濾器(GlobalFilter)
如果你希望對(duì)所有路由都生效,比如統(tǒng)一記錄請(qǐng)求耗時(shí)、統(tǒng)一鑒權(quán),那就寫個(gè) GlobalFilter:
@Component
public class LoggingGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
long start = System.currentTimeMillis();
ServerHttpRequest request = exchange.getRequest();
System.out.println("請(qǐng)求來了: " + request.getURI());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long end = System.currentTimeMillis();
System.out.println("請(qǐng)求結(jié)束,耗時(shí): " + (end - start) + "ms");
}));
}
@Override
public int getOrder() {
return -1; // 數(shù)字越小,優(yōu)先級(jí)越高
}
}注意這里返回的是 Mono<Void>,整個(gè)流程是異步非阻塞的。你不能像在 Servlet Filter 里那樣直接 chain.doFilter() 就完事,得用 then()、flatMap() 這些操作符來組合邏輯。
局部過濾器(GatewayFilter)
如果只想對(duì)某個(gè)特定路由加邏輯,比如給 /api/user/** 加個(gè) token 校驗(yàn),可以在配置文件里配,也可以自定義 GatewayFilterFactory。
舉個(gè)簡(jiǎn)單的例子,在 application.yml 里加個(gè)內(nèi)置的過濾器:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- AddRequestHeader=X-Source, gateway這會(huì)給匹配的請(qǐng)求加上一個(gè)請(qǐng)求頭。當(dāng)然,你也可以自己寫復(fù)雜的邏輯,比如解析 JWT、修改請(qǐng)求體等。
我們的經(jīng)驗(yàn)是……
在團(tuán)隊(duì)遷移老項(xiàng)目到 Gateway 的過程中,最常踩的坑就是“想當(dāng)然地用 Servlet 思維寫代碼”。比如:
- 試圖在 Gateway 里用
HttpServletRequest—— 不行,得用ServerHttpRequest - 想用
ThreadLocal存用戶信息 —— 響應(yīng)式環(huán)境下線程會(huì)切換,得用Context或Reactor Context - 直接調(diào)用阻塞的數(shù)據(jù)庫或 HTTP 接口 —— 會(huì)導(dǎo)致性能瓶頸,最好用
WebClient等非阻塞方式
所以我的建議是:一旦用了 Gateway,就徹底告別 Servlet 那套東西。別想著“能不能兼容”,而是“怎么用新模型解決問題”。
Gateway 中能寫 Servlet Filter 嗎?(補(bǔ)充版)
上面講了 Gateway 不能用 Servlet Filter,但我覺得有些問題還需要補(bǔ)充一下:“Spring Boot 不是自帶 Tomcat 嗎?為啥 Filter 不生效?”、“Mono/Flux 到底是啥?”、“Netty 又是個(gè)什么玩意兒?”
這些問題其實(shí)戳中了很多人遷移微服務(wù)網(wǎng)關(guān)時(shí)的認(rèn)知盲區(qū)。我就結(jié)合我們踩過的坑,再嘮一嘮。
1. Spring Boot 自帶 Tomcat,為什么 Filter 不執(zhí)行?
這是個(gè)特別容易混淆的點(diǎn)!我一開始也納悶:明明項(xiàng)目是 Spring Boot 啊,application.properties 里也沒改啥,怎么 Filter 就不跑了?
關(guān)鍵在于:你啟動(dòng)的是不是“WebFlux”應(yīng)用?
Spring Boot 從 2.x 開始,支持兩種 Web 編程模型:
- Spring MVC:基于 Servlet,跑在 Tomcat/Jetty 上,用
DispatcherServlet處理請(qǐng)求。 - Spring WebFlux:響應(yīng)式編程模型,可以跑在 Netty、Undertow,甚至也能跑在 Tomcat 上(但不用 Servlet 那套)。
而 Spring Cloud Gateway 是基于 WebFlux 構(gòu)建的。就算你沒顯式排除 Tomcat,只要引入了 spring-cloud-starter-gateway,Spring Boot 會(huì)自動(dòng)切換成 WebFlux 模式,并且默認(rèn)使用 Netty 作為內(nèi)嵌服務(wù)器。
你可以試試看:?jiǎn)?dòng)一個(gè)純 Gateway 項(xiàng)目,控制臺(tái)會(huì)打印類似這樣的日志:
Netty started on port 8080
而不是:
Tomcat started on port 8080
?? 重點(diǎn)來了:即使你強(qiáng)行保留 Tomcat(比如加了
spring-boot-starter-web),只要用了 Gateway,它內(nèi)部的路由和過濾邏輯依然是走 WebFlux + Reactor 的,不會(huì)經(jīng)過 Servlet 容器的 Filter 鏈。所以你寫的@Component Filter根本沒機(jī)會(huì)被調(diào)用。
我們的經(jīng)驗(yàn)是:Gateway 項(xiàng)目里千萬別同時(shí)引入 spring-boot-starter-web 和 spring-cloud-starter-gateway,否則會(huì)沖突,啟動(dòng)都可能失敗。
2. Mono 和 Flux 是啥?為啥 Gateway 非要用它們?
簡(jiǎn)單說,Mono 和 Flux 是 Project Reactor 提供的兩個(gè)核心類,用來做響應(yīng)式編程(Reactive Programming)。
Mono<T>:表示 0 或 1 個(gè)元素的異步流。比如一個(gè) HTTP 響應(yīng)、一個(gè)用戶對(duì)象。Flux<T>:表示 0 到 N 個(gè)元素的異步流。比如一個(gè)消息隊(duì)列、一個(gè)文件流。
它們和傳統(tǒng)的 List、User 有什么區(qū)別?最大的不同是:它們不代表“已經(jīng)拿到的數(shù)據(jù)”,而是“未來會(huì)拿到數(shù)據(jù)的承諾”。
舉個(gè)例子:
// 傳統(tǒng)方式(阻塞) User user = userService.findById(1); // 線程卡在這兒,等數(shù)據(jù)庫返回 // 響應(yīng)式方式(非阻塞) Mono<User> userMono = userService.findById(1); // 立刻返回,不等!
在 Gateway 的 GlobalFilter 里,你看到的這個(gè)方法簽名:
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
意思就是:“我處理完這個(gè)請(qǐng)求后,會(huì)給你一個(gè)信號(hào)(Void),但我不阻塞線程”。整個(gè)鏈路靠 Mono 的組合(比如 .then(), .flatMap())來串聯(lián)。
為什么 Gateway 要用這套?
因?yàn)榫W(wǎng)關(guān)要扛高并發(fā)。如果每個(gè)請(qǐng)求都占一個(gè)線程(像 Tomcat 那樣),1萬個(gè)并發(fā)就得開1萬個(gè)線程,內(nèi)存直接爆掉。而 Netty + Reactor 用少量線程就能處理幾萬甚至幾十萬連接——靠的就是“非阻塞 + 異步回調(diào)”。
所以,不是 Gateway “非要搞復(fù)雜”,而是為了性能不得不這么干。
3. Netty 是個(gè)什么東西?
你可以把 Netty 理解為一個(gè) 高性能網(wǎng)絡(luò)通信框架。它不依賴 Servlet 規(guī)范,直接跟操作系統(tǒng)底層的網(wǎng)絡(luò)接口打交道(比如 Linux 的 epoll)。
Tomcat 是“面向請(qǐng)求-響應(yīng)”的:來一個(gè) HTTP 請(qǐng)求 → 分配一個(gè)線程 → 處理 → 返回 → 線程釋放。
Netty 是“事件驅(qū)動(dòng)”的:所有連接注冊(cè)到 EventLoop(可以理解為一個(gè)線程池),當(dāng)有數(shù)據(jù)可讀/可寫時(shí),才觸發(fā)回調(diào)。一個(gè)線程能同時(shí)管成百上千個(gè)連接。
Spring Cloud Gateway 選擇 Netty,就是因?yàn)椋?/p>
- 啟動(dòng)快
- 內(nèi)存占用低
- 支持長(zhǎng)連接、WebSocket、HTTP/2 等高級(jí)特性
- 天然契合響應(yīng)式編程模型
打個(gè)比方:
- Tomcat 像是“每個(gè)顧客配一個(gè)服務(wù)員”,人多了就忙不過來。
- Netty 像是“一個(gè)服務(wù)員盯所有桌子,誰舉手就服務(wù)誰”,效率高得多。
當(dāng)然,Netty 學(xué)習(xí)曲線陡一點(diǎn),調(diào)試也麻煩些。但對(duì)網(wǎng)關(guān)這種 I/O 密集型場(chǎng)景,它是目前 Java 生態(tài)里最成熟的選擇。
總結(jié)一下
- 不能在 Spring Cloud Gateway 中使用
Servlet Filter,因?yàn)樗慌茉?Servlet 容器上。 - 要實(shí)現(xiàn)類似功能,請(qǐng)用
GlobalFilter或GatewayFilter。 - 別硬套舊習(xí)慣,響應(yīng)式編程有它自己的套路,適應(yīng)了其實(shí)也挺香。
- 如果你只是做個(gè)后臺(tái)管理,QPS 幾十,用 Spring MVC + Tomcat 完全夠用,F(xiàn)ilter 寫起來也順手。
- 但如果你要做 API 網(wǎng)關(guān),面對(duì)的是成千上萬的移動(dòng)端或第三方調(diào)用,那 Gateway + Netty + Reactor 這套組合拳,就是更合適的選擇。
我自己現(xiàn)在寫 Gateway 項(xiàng)目,已經(jīng)完全不用 Filter 這個(gè)詞了,一說“過濾器”,默認(rèn)就是指 GlobalFilter。思維轉(zhuǎn)過來之后,開發(fā)反而更清爽。
到此這篇關(guān)于Gateway 中能寫 Servlet Filter 嗎?的文章就介紹到這了,更多相關(guān)Gateway 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
IDEA運(yùn)行SpringBoot項(xiàng)目的超詳細(xì)步驟截圖
在當(dāng)前的開發(fā)中Spring Boot開發(fā)框架已經(jīng)成為主流,下面這篇文章主要給大家介紹了關(guān)于IDEA運(yùn)行SpringBoot項(xiàng)目的超詳細(xì)步驟截圖,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2022-11-11
Java如何實(shí)現(xiàn)簡(jiǎn)單后臺(tái)訪問并獲取IP
這篇文章主要介紹了Java如何實(shí)現(xiàn)簡(jiǎn)單后臺(tái)訪問并獲取IP,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10
如何基于Jenkins構(gòu)建Jmeter項(xiàng)目
這篇文章主要介紹了如何基于Jenkins構(gòu)建Jmeter項(xiàng)目,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09
IntelliJ IDEA設(shè)置Tabs實(shí)現(xiàn)同時(shí)打開多個(gè)文件且分行顯示
今天小編就為大家分享一篇關(guān)于IntelliJ IDEA設(shè)置Tabs實(shí)現(xiàn)同時(shí)打開多個(gè)文件且分行顯示,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2018-10-10
SpringBoot整合Flink CDC實(shí)現(xiàn)實(shí)時(shí)追蹤mysql數(shù)據(jù)變動(dòng)
我們將整合Spring Boot和Apache Flink CDC(Change Data Capture)來實(shí)現(xiàn)實(shí)時(shí)數(shù)據(jù)追蹤,下面是一個(gè)基本的實(shí)踐流程代碼,包括搭建Spring Boot項(xiàng)目、整合Flink CDC以及實(shí)現(xiàn)數(shù)據(jù)變動(dòng)的實(shí)時(shí)追蹤,需要的朋友可以參考下2024-07-07
關(guān)于Java限流功能的簡(jiǎn)單實(shí)現(xiàn)
這篇文章主要介紹了關(guān)于Java限流功能的簡(jiǎn)單實(shí)現(xiàn),在Java中,限流是一種常見的技術(shù)手段,用于控制系統(tǒng)的訪問速率,以保護(hù)系統(tǒng)免受過載和濫用,需要的朋友可以參考下2023-07-07
Mybatis批量插入大量數(shù)據(jù)的最優(yōu)方式總結(jié)
批量插入功能是我們?nèi)粘9ぷ髦斜容^常見的業(yè)務(wù)功能之一,下面這篇文章主要給大家總結(jié)介紹了關(guān)于Mybatis批量插入大量數(shù)據(jù)的幾種最優(yōu)方式,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03

