Spring Cloud Gateway修改請求和響應body的內(nèi)容

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

內(nèi)容:所有原創(chuàng)文章分類匯總及配套源碼靠粪,涉及Java、Docker、Kubernetes刁笙、DevOPS等董栽;

本篇概覽

  • 作為《Spring Cloud Gateway實戰(zhàn)》系列的第九篇声搁,咱們聊聊如何用Spring Cloud Gateway修改原始請求和響應內(nèi)容,以及修改過程中遇到的問題

  • 首先是修改請求body演训,如下圖蜒秤,瀏覽器是請求發(fā)起方汁咏,真實參數(shù)只有<font color="blue">user-id</font>,經(jīng)過網(wǎng)關時被塞入字段<font color="blue">user-name</font>作媚,于是攘滩,后臺服務收到的請求就帶有<font color="blue">user-name</font>字段了

在這里插入圖片描述
  • 其次是修改響應,如下圖纸泡,服務提供方<font color="blue">provider-hello</font>的原始響應只有<font color="red">response-tag</font>字段轰驳,經(jīng)過網(wǎng)關時被塞入了<font color="blue">gateway-response-tag</font>字段,最終瀏覽器收到的響應就是<font color="red">response-tag</font>和<font color="blue">gateway-response-tag</font>兩個字段:
在這里插入圖片描述
  • 總的來說弟灼,今天要做具體事情如下:
  1. 準備工作:在服務提供者的代碼中新增一個web接口级解,用于驗證Gateway的操作是否有效
  2. 介紹修改請求body和響應body的套路
  3. 按套路開發(fā)一個過濾器(filter),用于修改請求的body
  4. 按套路開發(fā)一個過濾器(filter)田绑,用于修改響應的body
  5. 思考和嘗試:如何從Gateway返回錯誤勤哗?
  • 在實戰(zhàn)過程中,咱們順便搞清楚兩個問題:
  1. 代碼配置路由時掩驱,如何給一個路由添加多個filter芒划?
  2. 代碼配置路由和yml配置是否可以混搭,兩者有沖突嗎欧穴?

源碼下載

名稱 鏈接 備注
項目主頁 https://github.com/zq2599/blog_demos 該項目在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該項目源碼的倉庫地址,https協(xié)議
git倉庫地址(ssh) git@github.com:zq2599/blog_demos.git 該項目源碼的倉庫地址涮帘,ssh協(xié)議
  • 這個git項目中有多個文件夾拼苍,本篇的源碼在<font color="blue">spring-cloud-tutorials</font>文件夾下,如下圖紅框所示:
在這里插入圖片描述
  • <font color="blue">spring-cloud-tutorials</font>文件夾下有多個子工程调缨,本篇的代碼是<font color="red">gateway-change-body</font>疮鲫,如下圖紅框所示:
在這里插入圖片描述

準備工作

  • 為了觀察Gateway能否按預期去修改請求和響應的body吆你,咱們給服務提供者<font color="blue">provider-hello</font>增加一個接口,代碼在Hello.java中俊犯,如下:
    @PostMapping("/change")
    public Map<String, Object> change(@RequestBody Map<String, Object> map) {
        map.put("response-tag", dateStr());
        return map;
    }
  • 可見新增的web接口很簡單:將收到的請求數(shù)據(jù)作為返回值妇多,在里面添加了一個鍵值對,然后返回給請求方燕侠,有了這個接口者祖,咱們就能通過觀察返回值來判斷Gateway對請求和響應的操作是否生效

  • 來試一下,先啟動nacos(provider-hello需要的)

  • 再運行<font color="blue">provider-hello</font>應用绢彤,用Postman向其發(fā)請求試試咸包,如下圖,符合預期:

在這里插入圖片描述
  • 準備工作已完成杖虾,開始開發(fā)吧

修改請求body的套路

  • 如何用Spring Cloud Gateway修改請求的body?來看看其中的套路:
  1. 修改請求body是通過自定義filter實現(xiàn)的
  2. 配置路由及其filter的時候媒熊,有yml配置文件和代碼配置兩種方式可以配置路由奇适,官方文檔給出的demo是代碼配置的,因此今天咱們也參考官方做法芦鳍,通過代碼來配置路由和過濾器
  3. 在代碼配置路由的時候嚷往,調(diào)用<font color="blue">filters</font>方法,該方法的入?yún)⑹莻€lambda表達式
  4. 此lambda表達式固定調(diào)用modifyRequestBody方法柠衅,咱們只要定義好modifyRequestBody方法的三個入?yún)⒓纯?/li>
  5. modifyRequestBody方法的第一個入?yún)⑹禽斎腩愋?/li>
  6. 第二個入?yún)⑹欠祷仡愋?/li>
  7. 第三個是RewriteFunction接口的實現(xiàn)皮仁,這個代碼需要您自己寫,內(nèi)容是將輸入數(shù)據(jù)轉(zhuǎn)換為返回類型數(shù)據(jù)具體邏輯菲宴,咱們來看官方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();
}

修改響應body的套路

  • 用Spring Cloud Gateway修改響應body的套路和前面的請求body如出一轍
  1. 通過代碼來配置路由和過濾器
  2. 在代碼配置路由的時候,調(diào)用<font color="blue">filters</font>方法喝峦,該方法的入?yún)⑹莻€lambda表達式
  3. 此lambda表達式固定調(diào)用modifyResponseBody方法势誊,咱們只要定義好modifyResponseBody方法的三個入?yún)⒓纯?/li>
  4. modifyRequestBody方法的第一個入?yún)⑹禽斎腩愋?/li>
  5. 第二個入?yún)⑹欠祷仡愋?/li>
  6. 第三個是RewriteFunction接口的實現(xiàn),這個代碼要您自己寫谣蠢,內(nèi)容是將輸入數(shù)據(jù)轉(zhuǎn)換為返回類型數(shù)據(jù)具體邏輯粟耻,咱們來看官方Demo,其實就是上述套路:
@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é)出來了眉踱,接下來挤忙,咱們一起擼代碼?

按套路開發(fā)一個修改請求body的過濾器(filter)

  • 廢話不說谈喳,在父工程<font color="blue">spring-cloud-tutorials</font>下新建子工程<font color="red">gateway-change-body</font>册烈,pom.xml無任何特殊之處,注意依賴<font color="blue">spring-cloud-starter-gateway</font>即可

  • 啟動類毫無新意:

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:
  #服務端口
  port: 8081
spring:
  application:
    name: gateway-change-body
  • 然后是核心邏輯:修改請求body的代碼婿禽,既RewriteFunction的實現(xiàn)類茄厘,代碼很簡單矮冬,將原始的請求body解析成Map對象,取出user-id字段次哈,生成user-name字段放回map胎署,apply方法返回的是個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獲取用戶名稱的方法,可以按實際情況來內(nèi)部實現(xià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后寫入map
            map.put("user-name", mockUserName(userId));

            // 添加一個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)異常時的處理
            return Mono.error(new Exception("1. json process fail", ex));
        }
    }
}
  • 然后是按部就班的基于代碼實現(xiàn)路由配置,重點是lambda表達式執(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();
    }
}
  • 代碼寫完了巨坊,運行工程<font color="blue">gateway-change-body</font>,在postman發(fā)起請求此改,得到響應如下圖趾撵,紅框中可見Gateway添加的內(nèi)容已成功:
在這里插入圖片描述
  • 現(xiàn)在修改請求body已經(jīng)成功,接下來再來修改服務提供者響應的body

修改響應body

  • 接下來開發(fā)修改響應body的代碼

  • 新增RewriteFunction接口的實現(xiàn)類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");

            // 添加一個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表達式里面占调,filters方法內(nèi)部調(diào)用modifyResponseBody,第三個入?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();
    }
}
  • 還記得咱們的第一個問題嗎移剪?通過上面的代碼究珊,您應該已經(jīng)看到了答案:用代碼配置路由時,多個過濾器的配置方法就是在filters方法中反復調(diào)用內(nèi)置的過濾器相關API纵苛,下圖紅框中的都可以:
在這里插入圖片描述
  • 運行服務剿涮,用Postman驗證效果,如下圖紅框攻人,Gateway在響應body中成功添加了一個key&value:
在這里插入圖片描述

代碼配置路由和yml配置是否可以混搭取试?

  • 前面有兩個問題,接下來回答第二個怀吻,咱們在application.yml中增加一個路由配置:
server:
  #服務端口
  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
  • 把<font color="blue">gateway-change-body</font>服務啟動起來想括,此時已經(jīng)有了兩個路由配置,一個在代碼中烙博,一個在yml中瑟蜈,先試試yml中的這個,如下圖沒問題:
在這里插入圖片描述
  • 再試試代碼配置的路由渣窜,如下圖铺根,結(jié)論是<font color="red">代碼配置路由和yml配置可以混搭</font>
在這里插入圖片描述

如何處理異常

  • 還有個問題必須要面對:修改請求或者響應body的過程中,如果發(fā)現(xiàn)問題需要提前返回錯誤(例如必要的字段不存在)乔宿,代碼該怎么寫位迂?

  • 咱們修改請求body的代碼集中在RequestBodyRewrite.java,增加下圖紅框內(nèi)容:

在這里插入圖片描述
  • 再來試試,這次請求參數(shù)中不包含<font color="blue">user-id</font>掂林,收到Gateway返回的錯誤信息如下圖:
在這里插入圖片描述
  • 看看控制臺臣缀,能看到代碼中拋出的異常信息:
在這里插入圖片描述
  • 此時,聰明的您應該發(fā)現(xiàn)問題所在了:咱們想告訴客戶端具體的錯誤泻帮,但實際上客戶端收到的是被Gateway框架處理后的內(nèi)容

  • 篇幅所限精置,上述問題從分析到解決的過程,就留給下一篇文章吧

  • 本篇的最后锣杂,請容許欣宸嘮叨兩句脂倦,聊聊為何要網(wǎng)關來修改請求和響應body的內(nèi)容,如果您沒興趣還請忽略

網(wǎng)關(Gateway)為什么要做這些元莫?

  • 看過開篇的兩個圖赖阻,聰明的您一定發(fā)現(xiàn)了問題:為什么要破壞原始數(shù)據(jù),一旦系統(tǒng)出了問題如何定位是服務提供方還是網(wǎng)關踱蠢?

  • 按照欣宸之前的經(jīng)驗火欧,盡管網(wǎng)關會破壞原始數(shù)據(jù),但只做一些簡單固定的處理茎截,一般以添加數(shù)據(jù)為主苇侵,網(wǎng)關不了解業(yè)務,最常見的就是鑒權(quán)稼虎、添加身份或標簽等操作

  • 前面的圖中確實感受不到網(wǎng)關的作用,但如果網(wǎng)關后面有多個服務提供者招刨,如下圖霎俩,這時候諸如鑒權(quán)、獲取賬號信息等操作由網(wǎng)關統(tǒng)一完成沉眶,比每個后臺分別實現(xiàn)一次更有效率打却,后臺可以更加專注于自身業(yè)務:

在這里插入圖片描述
  • 經(jīng)驗豐富的您可能會對我的狡辯不屑一顧:網(wǎng)關統(tǒng)一鑒權(quán)、獲取身份谎倔,一般會把身份信息放入請求的header中柳击,也不會修改請求和響應的內(nèi)容啊,欣宸前面的一堆解釋還是沒說清楚為啥要在網(wǎng)關位置修改請求和響應的內(nèi)容片习!

  • 好吧捌肴,面對聰明的您,我攤牌了:本篇只是從技術(shù)上演示Spring Cloud Gateway如何修改請求和響應內(nèi)容藕咏,請不要將此技術(shù)與實際后臺業(yè)務耦合状知;

你不孤單,欣宸原創(chuàng)一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 數(shù)據(jù)庫+中間件系列
  6. DevOps系列

歡迎關注公眾號:程序員欣宸

微信搜索「程序員欣宸」孽查,我是欣宸饥悴,期待與您一同暢游Java世界...
https://github.com/zq2599/blog_demos

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子西设,更是在濱河造成了極大的恐慌瓣铣,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贷揽,死亡現(xiàn)場離奇詭異棠笑,居然都是意外死亡,警方通過查閱死者的電腦和手機擒滑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門腐晾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人丐一,你說我怎么就攤上這事藻糖。” “怎么了库车?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵巨柒,是天一觀的道長。 經(jīng)常有香客問我柠衍,道長洋满,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任珍坊,我火速辦了婚禮牺勾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘阵漏。我一直安慰自己驻民,他們只是感情好,可當我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布履怯。 她就那樣靜靜地躺著回还,像睡著了一般。 火紅的嫁衣襯著肌膚如雪叹洲。 梳的紋絲不亂的頭發(fā)上柠硕,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天,我揣著相機與錄音运提,去河邊找鬼蝗柔。 笑死,一個胖子當著我的面吹牛民泵,可吹牛的內(nèi)容都是我干的诫咱。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼洪灯,長吁一口氣:“原來是場噩夢啊……” “哼坎缭!你這毒婦竟也來了竟痰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后惑畴,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铜秆,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡祥得,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蒋得,到底是詐尸還是另有隱情级及,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布额衙,位于F島的核電站饮焦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏窍侧。R本人自食惡果不足惜县踢,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望伟件。 院中可真熱鬧硼啤,春花似錦、人聲如沸斧账。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽其骄。三九已至亏镰,卻和暖如春扯旷,著一層夾襖步出監(jiān)牢的瞬間拯爽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工钧忽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留毯炮,地道東北人。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓耸黑,卻偏偏與公主長得像桃煎,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子大刊,可洞房花燭夜當晚...
    茶點故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容