SpringCloud Gateway是Spring全家桶中一個比較新的項目,Spring社區是這麼介紹它的:html
該項目藉助Spring WebFlux的能力,打造了一個API網關。旨在提供一種簡單而有效的方法來做爲API服務的路由,併爲它們提供各類加強功能,例如:安全性,監控和可伸縮性。
而在真實的業務領域,咱們常常用SpringCloud Gateway來作微服務網關,若是你不理解微服務網關和傳統網關的區別,能夠閱讀此篇文章 Service Mesh和API Gateway關係深度探討 來了解二者的定位區別。java
以我粗淺的理解,傳統的API網關,每每是獨立於各個後端服務,請求先打到獨立的網關層,再打到服務集羣。而微服務網關,將流量從南北走向改成東西走向(見下圖),微服務網關和後端服務是在同一個容器中的,因此也有個別名,叫作Gateway Sidecar。react
爲啥叫Sidecar,這個詞應該怎麼理解呢,吃雞裏的三蹦子見過沒:nginx
摩托車是你的後端服務,而旁邊掛着的額外座椅就是微服務網關,他是依附於後端服務的(通常是指兩個進程在同一個容器中),是否是生動形象了一些。git
因爲本人才疏學淺,對於微服務相關概念理解上不免會有誤差。就不在此詳細講述原理性的文字了。github
本文只探討SpringCloud Gateway的入門搭建和實戰踩坑。 若是小夥伴們對原理感興趣,能夠等後續原理分析文章。web
注:本文網關項目在筆者公司已經上線運行,天天承擔百萬級別的請求,是通過實戰驗證的項目。面試
手把手造一個網關算法
踩坑實戰spring
完整項目源代碼已經收錄到個人Github:
https://github.com/qqxx6661/s...
我使用了spring-boot 2.2.5.RELEASE做爲parent依賴:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent>
在dependencyManagement中,咱們須要指定sringcloud的版本,以便保證咱們可以引入咱們想要的SpringCloud Gateway版本,因此須要用到dependencyManagement:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
最後,是在dependency中引入spring-cloud-starter-gateway:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
如此一來,咱們便引入了2.2.5.RELEASE版本的網關:
此外,請檢查一下你的依賴中是否含有spring-boot-starter-web,若是有,請幹掉它。由於咱們的SpringCloud Gateway是一個netty+webflux實現的web服務器,和Springboot Web自己就是衝突的。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
作到這裏,實際上你的項目就已經能夠啓動了,運行SpringcloudGatewayApplication,獲得結果如圖:
SpringBoot的核心概念是約定優先於配置,在之前初學Spring時,一直不理解這句話的意思,在使用SpringCloud Gateway時,更加深刻的理解了這句話。在默認狀況下,你不須要任何的配置,就可以運行起來最基本的網關。針對你以後特定的需求,再去追加配置。
而SpringCloud Gateway更強大的一點就是內置了很是多的默認功能實現,你須要的大部分功能,好比在請求中添加一個header,添加一個參數,都只須要在yml中引入相應的內置過濾器便可。
能夠說,yml是整個SpringCloud Gateway的靈魂。
一個網關最基本的功能,就是配置路由,在這方面,SpringCloud Gateway支持很是多方式。好比:
這些在官網教程中,都有詳細的介紹,就算你百度下,也會有不少民間翻譯的入門教程,我就再也不贅述了,我只用一個請求路徑作一個簡單的例子。
在公司的項目中,因爲有新老兩套後臺服務,咱們使用不一樣的uri路徑進行區分。
那麼能夠直接在yml裏面配置:
logging: level: org.springframework.cloud.gateway: DEBUG reactor.netty.http.client: DEBUG spring: cloud: gateway: default-filters: - AddRequestHeader=gateway-env, springcloud-gateway routes: - id: "server_v2" uri: "http://127.0.0.1:8002" predicates: - Path=/api/v2/** - id: "server_v1" uri: "http://127.0.0.1:8001" predicates: - Path=/api/**
上面的代碼解釋以下:
來看一下http://localhost:8080/api/xxx...
來看一下http://localhost:8080/api/v2/...
能夠看到兩個請求被正確的路由了。因爲咱們真正並無開啓後端服務,因此最後一句error請忽略。
在公司實際的項目中,我在搭建好網關後,遇到了一個接口轉義問題,相信不少讀者可能也會碰到,因此在這裏咱們最好是防患於未然,優先處理下。
問題是這樣的,不少老項目在url上並無進行轉義,致使會出現以下接口請求,http://xxxxxxxxx/api/b3d56a6f...
這樣請求過來,網關會報錯:
java.lang.IllegalArgumentException: Invalid character '=' for QUERY_PARAM in "http://pic1.ajkimg.com/display/anjuke/b3d56a6fa19975ba520189f3f55de7f6/140x140.jpg?t=1"
在不修改服務代碼邏輯的前提下,網關其實已經能夠解決這件事情,解決辦法就是升級到2.1.1.RELEASE以上的版本。
The issue was fixed in version spring-cloud-gateway 2.1.1.RELEASE.
因此咱們一開始就是用了高版本2.2.5.RELEASE,避免了這個問題,若是小夥伴發現以前使用的版本低於 2.1.1.RELEASE,請升級。
在網關的使用中,有時候會須要拿到請求body裏面的數據,好比驗證簽名,body可能須要參與簽名校驗。
可是SpringCloud Gateway因爲底層採用了webflux,其請求是流式響應的,即 Reactor 編程,要讀取 Request Body 中的請求參數就沒那麼容易了。
網上谷歌了好久,不少解決方案要麼是完全過期,要麼是版本不兼容,好在最後參考了這篇文章,終於有了思路:
https://www.jianshu.com/p/db3...
首先咱們須要將body從請求中拿出來,因爲是流式處理,Request的Body是隻能讀取一次的,若是直接經過在Filter中讀取,會致使後面的服務沒法讀取數據。
SpringCloud Gateway 內部提供了一個斷言工廠類ReadBodyPredicateFactory,這個類實現了讀取Request的Body內容並放入緩存,咱們能夠經過從緩存中獲取body內容來實現咱們的目的。
首先新建一個CustomReadBodyRoutePredicateFactory類,這裏只貼出關鍵代碼,完整代碼請看可運行的Github倉庫:
@Component public class CustomReadBodyRoutePredicateFactory extends AbstractRoutePredicateFactory<CustomReadBodyRoutePredicateFactory.Config> { protected static final Log log = LogFactory.getLog(CustomReadBodyRoutePredicateFactory.class); private List<HttpMessageReader<?>> messageReaders; @Value("${spring.codec.max-in-memory-size}") private DataSize maxInMemory; public CustomReadBodyRoutePredicateFactory() { super(Config.class); this.messageReaders = HandlerStrategies.withDefaults().messageReaders(); } public CustomReadBodyRoutePredicateFactory(List<HttpMessageReader<?>> messageReaders) { super(Config.class); this.messageReaders = messageReaders; } @PostConstruct private void overrideMsgReaders() { this.messageReaders = HandlerStrategies.builder().codecs((c) -> c.defaultCodecs().maxInMemorySize((int) maxInMemory.toBytes())).build().messageReaders(); } @Override public AsyncPredicate<ServerWebExchange> applyAsync(Config config) { return new AsyncPredicate<ServerWebExchange>() { @Override public Publisher<Boolean> apply(ServerWebExchange exchange) { Class inClass = config.getInClass(); Object cachedBody = exchange.getAttribute("cachedRequestBodyObject"); if (cachedBody != null) { try { boolean test = config.predicate.test(cachedBody); exchange.getAttributes().put("read_body_predicate_test_attribute", test); return Mono.just(test); } catch (ClassCastException var6) { if (CustomReadBodyRoutePredicateFactory.log.isDebugEnabled()) { CustomReadBodyRoutePredicateFactory.log.debug("Predicate test failed because class in predicate does not match the cached body object", var6); } return Mono.just(false); } } else { return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> { return ServerRequest.create(exchange.mutate().request(serverHttpRequest).build(), CustomReadBodyRoutePredicateFactory.this.messageReaders).bodyToMono(inClass).doOnNext((objectValue) -> { exchange.getAttributes().put("cachedRequestBodyObject", objectValue); }).map((objectValue) -> { return config.getPredicate().test(objectValue); }).thenReturn(true); }); } } @Override public String toString() { return String.format("ReadBody: %s", config.getInClass()); } }; } @Override public Predicate<ServerWebExchange> apply(Config config) { throw new UnsupportedOperationException("ReadBodyPredicateFactory is only async."); } }
代碼主要做用:在有body的請求到來時,將body讀取出來放到內存緩存中。若沒有body,則不做任何操做。
這樣咱們即可以在攔截器裏使用exchange.getAttribute("cachedRequestBodyObject")獲得body體。
對了,咱們尚未演示一個filter是如何寫的,在這裏就先寫一個完整的demofilter。
讓咱們新建類DemoGatewayFilterFactory:
@Component public class DemoGatewayFilterFactory extends AbstractGatewayFilterFactory<DemoGatewayFilterFactory.Config> { private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject"; public DemoGatewayFilterFactory() { super(Config.class); log.info("Loaded GatewayFilterFactory [DemoFilter]"); } @Override public List<String> shortcutFieldOrder() { return Collections.singletonList("enabled"); } @Override public GatewayFilter apply(DemoGatewayFilterFactory.Config config) { return (exchange, chain) -> { if (!config.isEnabled()) { return chain.filter(exchange); } log.info("-----DemoGatewayFilterFactory start-----"); ServerHttpRequest request = exchange.getRequest(); log.info("RemoteAddress: [{}]", request.getRemoteAddress()); log.info("Path: [{}]", request.getURI().getPath()); log.info("Method: [{}]", request.getMethod()); log.info("Body: [{}]", (String) exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY)); log.info("-----DemoGatewayFilterFactory end-----"); return chain.filter(exchange); }; } public static class Config { private boolean enabled; public Config() {} public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } } }
這個filter裏,咱們拿到了新鮮的請求,而且打印出了他的path,method,body等。
咱們發送一個post請求,body就寫一個「我是body」,運行網關,獲得結果:
是否是很是清晰明瞭!
你覺得這就結束了嗎?這裏有兩個很是大的坑。
上面貼出的CustomReadBodyRoutePredicateFactory類其實已是我修復過的代碼,裏面有一行.thenReturn(true)
是須要加上的。這才能保證當body爲空時,不會報出異常。至於爲啥一開始寫的有問題,顯然由於我偷懶了,直接copy網上的代碼了,哈哈哈哈哈。
這個狀況是在公司項目上線後才發現的,咱們的請求裏body有時候會比較大,可是網關會有默認大小限制。因此上線後發現了頻繁的報錯:
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144
谷歌後,找到了解決方案,須要在配置中增長了以下配置
spring: codec: max-in-memory-size: 5MB
把buffer大小改到了5M。
你覺得這就又雙叕結束了,太天真了,你會發現可能沒有生效。
問題的根源在這裏:咱們在spring配置了上面的參數,可是咱們自定義的攔截器是會初始化ServerRequest,這個DefaultServerRequest中的HttpMessageReader會使用默認的262144
因此咱們在此處須要從Spring中取出CodecConfigurer, 並將裏面的Reader傳給serverRequest。
詳細的debug過程能夠看這篇參考文獻:
http://theclouds.io/tag/sprin...
OK,找到問題後,就能夠修改咱們的代碼,在CustomReadBodyRoutePredicateFactory裏,增長:
@Value("${spring.codec.max-in-memory-size}") private DataSize maxInMemory; @PostConstruct private void overrideMsgReaders() { this.messageReaders = HandlerStrategies.builder().codecs((c) -> c.defaultCodecs().maxInMemorySize((int) maxInMemory.toBytes())).build().messageReaders(); }
這樣每次就會使用咱們的5MB來做爲最大緩存限制了。
依然提醒一下,完整的代碼能夠請看可運行的Github倉庫
講到這裏,入門實戰就差很少了,你的網關已經能夠上線使用了,你要作的就是加上你須要的業務功能,好比日誌,延籤,統計等。
不少時候,咱們的後端服務會去經過host拿到用戶的真實IP,可是經過外層反向代理nginx的轉發,極可能就須要從header裏拿X-Forward-XXX相似這樣的參數,才能拿到真實IP。
在咱們加入了微服務網關後,這個複雜的鏈路中又增長了一環。
這不,若是你不作任何設置,因爲你的網關和後端服務在同一個容器中,你的後端服務頗有可能就會拿到localhost:8080(你的網關端口)這樣的IP。
這時候,你須要在yml裏配置PreserveHostHeader,這是SpringCloud Gateway自帶的實現:
filters: - PreserveHostHeader # 防止host被修改成localhost
字面意思,就是將Host的Header保留起來,透傳給後端服務。
filter裏面的源碼貼出來給你們:
public GatewayFilter apply(Object config) { return new GatewayFilter() { public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { exchange.getAttributes().put(ServerWebExchangeUtils.PRESERVE_HOST_HEADER_ATTRIBUTE, true); return chain.filter(exchange); } public String toString() { return GatewayToStringStyler.filterToStringCreator(PreserveHostHeaderGatewayFilterFactory.this).toString(); } }; }
公司的項目中,老的後端倉庫api都以.json結尾(/api/xxxxxx.json)
,這就催生了一個需求,當咱們對老接口進行了重構後,但願其打到咱們的新服務,咱們就要將.json這個尾綴切除。能夠在filters裏設置:
filters: - RewritePath=(?<segment>/?.*).json, $\{segment} # 重構接口抹去.json尾綴
這樣就能夠實現打到後端的接口去除了.json後綴。
本文帶領讀者一步步完成了一個微服務網關的搭建,而且將許多可能隱藏的坑進行了解決。最後的成品項目在筆者公司已經上線運行,而且增長了簽名驗證,日誌記錄等業務,天天承擔百萬級別的請求,是通過實戰驗證過的項目。
最後再發一次項目源碼倉庫:
https://github.com/qqxx6661/s...
感謝你們的支持,若是文章對你起到了一丁點幫助,請點贊轉發支持一下!
大家的反饋是我持續更新的動力,謝謝~
https://cloud.tencent.com/dev...
https://juejin.cn/post/684490...
https://segmentfault.com/a/11...
https://cloud.spring.io/sprin...
https://www.cnblogs.com/savor...
https://www.servicemesher.com...
https://www.cnblogs.com/hyf-h...
https://www.codercto.com/a/52...
https://github.com/spring-clo...
https://blog.csdn.net/zhangzh...
我是一名奮鬥在一線的互聯網後端開發工程師。
平時主要關注後端開發,數據安全,邊緣計算等方向,歡迎交流。
各大平臺都能找到我
原創文章主要內容
我的公衆號:後端技術漫談
若是文章對你有幫助,請各位老闆點贊在看轉發支持一下,你的支持對我很是重要~