詳解Spring Cloud Gateway修改請(qǐng)求和響應(yīng)body的內(nèi)容
歡迎訪問(wèn)我的GitHub
這里分類(lèi)和匯總了欣宸的全部原創(chuàng)(含配套源碼):https://github.com/zq2599/blog_demos
本篇概覽
- 作為《Spring Cloud Gateway實(shí)戰(zhàn)》系列的第九篇,咱們聊聊如何用Spring Cloud Gateway修改原始請(qǐng)求和響應(yīng)內(nèi)容,以及修改過(guò)程中遇到的問(wèn)題
- 首先是修改請(qǐng)求body,如下圖,瀏覽器是請(qǐng)求發(fā)起方,真實(shí)參數(shù)只有user-id,經(jīng)過(guò)網(wǎng)關(guān)時(shí)被塞入字段user-name,于是,后臺(tái)服務(wù)收到的請(qǐng)求就帶有user-name字段了
其次是修改響應(yīng),如下圖,服務(wù)提供方provider-hello的原始響應(yīng)只有response-tag字段,經(jīng)過(guò)網(wǎng)關(guān)時(shí)被塞入了gateway-response-tag字段,最終瀏覽器收到的響應(yīng)就是response-tag和gateway-response-tag兩個(gè)字段:
總的來(lái)說(shuō),今天要做具體事情如下:
- 準(zhǔn)備工作:在服務(wù)提供者的代碼中新增一個(gè)web接口,用于驗(yàn)證Gateway的操作是否有效
- 介紹修改請(qǐng)求body和響應(yīng)body的套路
- 按套路開(kāi)發(fā)一個(gè)過(guò)濾器(filter),用于修改請(qǐng)求的body
- 按套路開(kāi)發(fā)一個(gè)過(guò)濾器(filter),用于修改響應(yīng)的body
- 思考和嘗試:如何從Gateway返回錯(cuò)誤?
在實(shí)戰(zhàn)過(guò)程中,咱們順便搞清楚兩個(gè)問(wèn)題:
- 代碼配置路由時(shí),如何給一個(gè)路由添加多個(gè)filter?
- 代碼配置路由和yml配置是否可以混搭,兩者有沖突嗎?
源碼下載
- 本篇實(shí)戰(zhàn)中的完整源碼可在GitHub下載到,地址和鏈接信息如下表所示(https://github.com/zq2599/blog_demos):
名稱(chēng) | 鏈接 | 備注 |
---|---|---|
項(xiàng)目主頁(yè) | https://github.com/zq2599/blog_demos | 該項(xiàng)目在GitHub上的主頁(yè) |
git倉(cāng)庫(kù)地址(https) | https://github.com/zq2599/blog_demos.git | 該項(xiàng)目源碼的倉(cāng)庫(kù)地址,https協(xié)議 |
git倉(cāng)庫(kù)地址(ssh) | git@github.com:zq2599/blog_demos.git | 該項(xiàng)目源碼的倉(cāng)庫(kù)地址,ssh協(xié)議 |
這個(gè)git項(xiàng)目中有多個(gè)文件夾,本篇的源碼在spring-cloud-tutorials文件夾下,如下圖紅框所示:
spring-cloud-tutorials文件夾下有多個(gè)子工程,本篇的代碼是gateway-change-body,如下圖紅框所示:
準(zhǔn)備工作
為了觀察Gateway能否按預(yù)期去修改請(qǐng)求和響應(yīng)的body,咱們給服務(wù)提供者provider-hello增加一個(gè)接口,代碼在Hello.java中,如下:
@PostMapping("/change") public Map<String, Object> change(@RequestBody Map<String, Object> map) { map.put("response-tag", dateStr()); return map; }
- 可見(jiàn)新增的web接口很簡(jiǎn)單:將收到的請(qǐng)求數(shù)據(jù)作為返回值,在里面添加了一個(gè)鍵值對(duì),然后返回給請(qǐng)求方,有了這個(gè)接口,咱們就能通過(guò)觀察返回值來(lái)判斷Gateway對(duì)請(qǐng)求和響應(yīng)的操作是否生效
- 來(lái)試一下,先啟動(dòng)nacos(provider-hello需要的)
- 再運(yùn)行provider-hello應(yīng)用,用Postman向其發(fā)請(qǐng)求試試,如下圖,符合預(yù)期:
- 準(zhǔn)備工作已完成,開(kāi)始開(kāi)發(fā)吧
修改請(qǐng)求body的套路
- 修改請(qǐng)求body是通過(guò)自定義filter實(shí)現(xiàn)的
- 配置路由及其filter的時(shí)候,有yml配置文件和代碼配置兩種方式可以配置路由,官方文檔給出的demo是代碼配置的,因此今天咱們也參考官方做法,通過(guò)代碼來(lái)配置路由和過(guò)濾器
- 在代碼配置路由的時(shí)候,調(diào)用filters方法,該方法的入?yún)⑹莻€(gè)lambda表達(dá)式
- 此lambda表達(dá)式固定調(diào)用modifyRequestBody方法,咱們只要定義好modifyRequestBody方法的三個(gè)入?yún)⒓纯?/li>
- modifyRequestBody方法的第一個(gè)入?yún)⑹禽斎腩?lèi)型
- 第二個(gè)入?yún)⑹欠祷仡?lèi)型
- 第三個(gè)是RewriteFunction接口的實(shí)現(xiàn),這個(gè)代碼需要您自己寫(xiě),內(nèi)容是將輸入數(shù)據(jù)轉(zhuǎn)換為返回類(lèi)型數(shù)據(jù)具體邏輯,咱們來(lái)看官方Demo,也就是上述套路了:
@Bean public RouteLocator routes(RouteLocatorBuilder builder) { return builder.routes() .route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org") .filters(f -> f.prefixPath("/httpbin") .modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE, (exchange, s) -> return Mono.just(new Hello(s.toUpperCase())))).uri(uri)) .build(); }
修改響應(yīng)body的套路
- 用Spring Cloud Gateway修改響應(yīng)body的套路和前面的請(qǐng)求body如出一轍
- 通過(guò)代碼來(lái)配置路由和過(guò)濾器
- 在代碼配置路由的時(shí)候,調(diào)用filters方法,該方法的入?yún)⑹莻€(gè)lambda表達(dá)式
- 此lambda表達(dá)式固定調(diào)用modifyResponseBody方法,咱們只要定義好modifyResponseBody方法的三個(gè)入?yún)⒓纯?/li>
- modifyRequestBody方法的第一個(gè)入?yún)⑹禽斎腩?lèi)型
- 第二個(gè)入?yún)⑹欠祷仡?lèi)型
- 第三個(gè)是RewriteFunction接口的實(shí)現(xiàn),這個(gè)代碼要您自己寫(xiě),內(nèi)容是將輸入數(shù)據(jù)轉(zhuǎn)換為返回類(lèi)型數(shù)據(jù)具體邏輯,咱們來(lái)看官方Demo,其實(shí)就是上述套路:
@Bean public RouteLocator routes(RouteLocatorBuilder builder) { return builder.routes() .route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org") .filters(f -> f.prefixPath("/httpbin") .modifyResponseBody(String.class, String.class, (exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri)) .build(); }
套路總結(jié)出來(lái)了,接下來(lái),咱們一起擼代碼?
按套路開(kāi)發(fā)一個(gè)修改請(qǐng)求body的過(guò)濾器(filter)
- 廢話不說(shuō),在父工程spring-cloud-tutorials下新建子工程gateway-change-body,pom.xml無(wú)任何特殊之處,注意依賴spring-cloud-starter-gateway即可
- 啟動(dòng)類(lèi)毫無(wú)新意:
package com.bolingcavalry.changebody; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ChangeBodyApplication { public static void main(String[] args) { SpringApplication.run(ChangeBodyApplication.class,args); } }
配置文件千篇一律:
server: #服務(wù)端口 port: 8081 spring: application: name: gateway-change-body
然后是核心邏輯:修改請(qǐng)求body的代碼,既RewriteFunction的實(shí)現(xiàn)類(lèi),代碼很簡(jiǎn)單,將原始的請(qǐng)求body解析成Map對(duì)象,取出user-id字段,生成user-name字段放回map,apply方法返回的是個(gè)Mono:
package com.bolingcavalry.changebody.function; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.Map; @Slf4j public class RequestBodyRewrite implements RewriteFunction<String, String> { private ObjectMapper objectMapper; public RequestBodyRewrite(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } /** * 根據(jù)用戶ID獲取用戶名稱(chēng)的方法,可以按實(shí)際情況來(lái)內(nèi)部實(shí)現(xiàn),例如查庫(kù)或緩存,或者遠(yuǎn)程調(diào)用 * @param userId * @return */ private String mockUserName(int userId) { return "user-" + userId; } @Override public Publisher<String> apply(ServerWebExchange exchange, String body) { try { Map<String, Object> map = objectMapper.readValue(body, Map.class); // 取得id int userId = (Integer)map.get("user-id"); // 得到nanme后寫(xiě)入map map.put("user-name", mockUserName(userId)); // 添加一個(gè)key/value map.put("gateway-request-tag", userId + "-" + System.currentTimeMillis()); return Mono.just(objectMapper.writeValueAsString(map)); } catch (Exception ex) { log.error("1. json process fail", ex); // json操作出現(xiàn)異常時(shí)的處理 return Mono.error(new Exception("1. json process fail", ex)); } } }
然后是按部就班的基于代碼實(shí)現(xiàn)路由配置,重點(diǎn)是lambda表達(dá)式執(zhí)行modifyRequestBody方法,并且將RequestBodyRewrite作為參數(shù)傳入:
package com.bolingcavalry.changebody.config; import com.bolingcavalry.changebody.function.RequestBodyRewrite; import com.bolingcavalry.changebody.function.ResponseBodyRewrite; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import reactor.core.publisher.Mono; @Configuration public class FilterConfig { @Bean public RouteLocator routes(RouteLocatorBuilder builder, ObjectMapper objectMapper) { return builder .routes() .route("path_route_change", r -> r.path("/hello/change") .filters(f -> f .modifyRequestBody(String.class,String.class,new RequestBodyRewrite(objectMapper)) ) .uri("http://127.0.0.1:8082")) .build(); } }
代碼寫(xiě)完了,運(yùn)行工程gateway-change-body,在postman發(fā)起請(qǐng)求,得到響應(yīng)如下圖,紅框中可見(jiàn)Gateway添加的內(nèi)容已成功:
現(xiàn)在修改請(qǐng)求body已經(jīng)成功,接下來(lái)再來(lái)修改服務(wù)提供者響應(yīng)的body
修改響應(yīng)body
- 接下來(lái)開(kāi)發(fā)修改響應(yīng)body的代碼
- 新增RewriteFunction接口的實(shí)現(xiàn)類(lèi)ResponseBodyRewrite.java
package com.bolingcavalry.changebody.function; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.reactivestreams.Publisher; import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.Map; @Slf4j public class ResponseBodyRewrite implements RewriteFunction<String, String> { private ObjectMapper objectMapper; public ResponseBodyRewrite(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public Publisher<String> apply(ServerWebExchange exchange, String body) { try { Map<String, Object> map = objectMapper.readValue(body, Map.class); // 取得id int userId = (Integer)map.get("user-id"); // 添加一個(gè)key/value map.put("gateway-response-tag", userId + "-" + System.currentTimeMillis()); return Mono.just(objectMapper.writeValueAsString(map)); } catch (Exception ex) { log.error("2. json process fail", ex); return Mono.error(new Exception("2. json process fail", ex)); } } }
路由配置代碼中,lambda表達(dá)式里面,filters方法內(nèi)部調(diào)用modifyResponseBody,第三個(gè)入?yún)⑹荝esponseBodyRewrite:
package com.bolingcavalry.changebody.config; import com.bolingcavalry.changebody.function.RequestBodyRewrite; import com.bolingcavalry.changebody.function.ResponseBodyRewrite; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import reactor.core.publisher.Mono; @Configuration public class FilterConfig { @Bean public RouteLocator routes(RouteLocatorBuilder builder, ObjectMapper objectMapper) { return builder .routes() .route("path_route_change", r -> r.path("/hello/change") .filters(f -> f .modifyRequestBody(String.class,String.class,new RequestBodyRewrite(objectMapper)) .modifyResponseBody(String.class, String.class, new ResponseBodyRewrite(objectMapper)) ) .uri("http://127.0.0.1:8082")) .build(); } }
還記得咱們的第一個(gè)問(wèn)題嗎?通過(guò)上面的代碼,您應(yīng)該已經(jīng)看到了答案:用代碼配置路由時(shí),多個(gè)過(guò)濾器的配置方法就是在filters方法中反復(fù)調(diào)用內(nèi)置的過(guò)濾器相關(guān)API,下圖紅框中的都可以:
運(yùn)行服務(wù),用Postman驗(yàn)證效果,如下圖紅框,Gateway在響應(yīng)body中成功添加了一個(gè)key&value:
代碼配置路由和yml配置是否可以混搭?
前面有兩個(gè)問(wèn)題,接下來(lái)回答第二個(gè),咱們?cè)赼pplication.yml中增加一個(gè)路由配置:
server: #服務(wù)端口 port: 8081 spring: application: name: gateway-change-body cloud: gateway: routes: - id: path_route_str uri: http://127.0.0.1:8082 predicates: - Path=/hello/str
把gateway-change-body服務(wù)啟動(dòng)起來(lái),此時(shí)已經(jīng)有了兩個(gè)路由配置,一個(gè)在代碼中,一個(gè)在yml中,先試試yml中的這個(gè),如下圖沒(méi)問(wèn)題:
再試試代碼配置的路由,如下圖,結(jié)論是代碼配置路由和yml配置可以混搭
如何處理異常
- 還有個(gè)問(wèn)題必須要面對(duì):修改請(qǐng)求或者響應(yīng)body的過(guò)程中,如果發(fā)現(xiàn)問(wèn)題需要提前返回錯(cuò)誤(例如必要的字段不存在),代碼該怎么寫(xiě)?
- 咱們修改請(qǐng)求body的代碼集中在RequestBodyRewrite.java,增加下圖紅框內(nèi)容:
- 再來(lái)試試,這次請(qǐng)求參數(shù)中不包含user-id,收到Gateway返回的錯(cuò)誤信息如下圖:
看看控制臺(tái),能看到代碼中拋出的異常信息:
- 此時(shí),聰明的您應(yīng)該發(fā)現(xiàn)問(wèn)題所在了:咱們想告訴客戶端具體的錯(cuò)誤,但實(shí)際上客戶端收到的是被Gateway框架處理后的內(nèi)容
- 篇幅所限,上述問(wèn)題從分析到解決的過(guò)程,就留給下一篇文章吧
- 本篇的最后,請(qǐng)容許欣宸嘮叨兩句,聊聊為何要網(wǎng)關(guān)來(lái)修改請(qǐng)求和響應(yīng)body的內(nèi)容,如果您沒(méi)興趣還請(qǐng)忽略
網(wǎng)關(guān)(Gateway)為什么要做這些?
看過(guò)開(kāi)篇的兩個(gè)圖,聰明的您一定發(fā)現(xiàn)了問(wèn)題:為什么要破壞原始數(shù)據(jù),一旦系統(tǒng)出了問(wèn)題如何定位是服務(wù)提供方還是網(wǎng)關(guān)?
- 按照欣宸之前的經(jīng)驗(yàn),盡管網(wǎng)關(guān)會(huì)破壞原始數(shù)據(jù),但只做一些簡(jiǎn)單固定的處理,一般以添加數(shù)據(jù)為主,網(wǎng)關(guān)不了解業(yè)務(wù),最常見(jiàn)的就是鑒權(quán)、添加身份或標(biāo)簽等操作
- 前面的圖中確實(shí)感受不到網(wǎng)關(guān)的作用,但如果網(wǎng)關(guān)后面有多個(gè)服務(wù)提供者,如下圖,這時(shí)候諸如鑒權(quán)、獲取賬號(hào)信息等操作由網(wǎng)關(guān)統(tǒng)一完成,比每個(gè)后臺(tái)分別實(shí)現(xiàn)一次更有效率,后臺(tái)可以更加專(zhuān)注于自身業(yè)務(wù):
- 經(jīng)驗(yàn)豐富的您可能會(huì)對(duì)我的狡辯不屑一顧:網(wǎng)關(guān)統(tǒng)一鑒權(quán)、獲取身份,一般會(huì)把身份信息放入請(qǐng)求的header中,也不會(huì)修改請(qǐng)求和響應(yīng)的內(nèi)容啊,欣宸前面的一堆解釋還是沒(méi)說(shuō)清楚為啥要在網(wǎng)關(guān)位置修改請(qǐng)求和響應(yīng)的內(nèi)容!
- 好吧,面對(duì)聰明的您,我攤牌了:本篇只是從技術(shù)上演示Spring Cloud Gateway如何修改請(qǐng)求和響應(yīng)內(nèi)容,請(qǐng)不要將此技術(shù)與實(shí)際后臺(tái)業(yè)務(wù)耦合;
你不孤單,欣宸原創(chuàng)一路相伴
到此這篇關(guān)于Spring Cloud Gateway修改請(qǐng)求和響應(yīng)body的內(nèi)容的文章就介紹到這了,更多相關(guān)Spring Cloud Gateway請(qǐng)求響應(yīng)body內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java+opencv3.2.0實(shí)現(xiàn)模板匹配
這篇文章主要為大家詳細(xì)介紹了Java+opencv3.2.0實(shí)現(xiàn)模板匹配的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02SpringMVC中的@RequestMapping注解解析
這篇文章主要介紹了SpringMVC中的@RequestMapping注解解析,SpringMVC使用@RequestMapping注解為控制器指定可以處理哪些?URL?請(qǐng)求,在控制器的類(lèi)定義及方法定義處都可標(biāo)注@RequestMapping,需要的朋友可以參考下2023-12-12SpringBoot mybatis 實(shí)現(xiàn)多級(jí)樹(shù)形菜單的示例代碼
這篇文章主要介紹了SpringBoot mybatis 實(shí)現(xiàn)多級(jí)樹(shù)形菜單的示例代碼,代碼簡(jiǎn)單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-05-05java input 調(diào)用手機(jī)相機(jī)和本地照片上傳圖片到服務(wù)器然后壓縮的方法
今天小編就為大家分享一篇java input 實(shí)現(xiàn)調(diào)用手機(jī)相機(jī)和本地照片上傳圖片到服務(wù)器然后壓縮的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-08-08深入淺析 Spring Security 緩存請(qǐng)求問(wèn)題
這篇文章主要介紹了 Spring Security 緩存請(qǐng)求問(wèn)題,本文通過(guò)實(shí)例文字相結(jié)合的形式給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2019-04-04學(xué)習(xí)在一臺(tái)新電腦上配置JAVA開(kāi)發(fā)環(huán)境
本文主要介紹了如何在一臺(tái)新電腦上配置JAVA開(kāi)發(fā)環(huán)境,每一個(gè)步驟都有對(duì)應(yīng)的截圖和文字說(shuō)明,需要的朋友可以參考下2015-07-07