歡迎訪問我的GitHub
https://github.com/zq2599/blog_demos
內容:所有原創文章分類匯總及配套源碼,涉及Java、Docker、Kubernetes、DevOPS等;
本篇概覽
-
作為《Spring Cloud Gateway實戰》系列的第九篇,咱們聊聊如何用Spring Cloud Gateway修改原始請求和響應內容,以及修改過程中遇到的問題
-
首先是修改請求body,如下圖,瀏覽器是請求發起方,真實參數只有user-id,經過網關時被塞入字段user-name,於是,后台服務收到的請求就帶有user-name字段了
- 其次是修改響應,如下圖,服務提供方provider-hello的原始響應只有response-tag字段,經過網關時被塞入了gateway-response-tag字段,最終瀏覽器收到的響應就是response-tag和gateway-response-tag兩個字段:
- 總的來說,今天要做具體事情如下:
- 准備工作:在服務提供者的代碼中新增一個web接口,用於驗證Gateway的操作是否有效
- 介紹修改請求body和響應body的套路
- 按套路開發一個過濾器(filter),用於修改請求的body
- 按套路開發一個過濾器(filter),用於修改響應的body
- 思考和嘗試:如何從Gateway返回錯誤?
- 在實戰過程中,咱們順便搞清楚兩個問題:
- 代碼配置路由時,如何給一個路由添加多個filter?
- 代碼配置路由和yml配置是否可以混搭,兩者有沖突嗎?
源碼下載
- 本篇實戰中的完整源碼可在GitHub下載到,地址和鏈接信息如下表所示(https://github.com/zq2599/blog_demos):
名稱 | 鏈接 | 備注 |
---|---|---|
項目主頁 | https://github.com/zq2599/blog_demos | 該項目在GitHub上的主頁 |
git倉庫地址(https) | https://github.com/zq2599/blog_demos.git | 該項目源碼的倉庫地址,https協議 |
git倉庫地址(ssh) | git@github.com:zq2599/blog_demos.git | 該項目源碼的倉庫地址,ssh協議 |
- 這個git項目中有多個文件夾,本篇的源碼在spring-cloud-tutorials文件夾下,如下圖紅框所示:
- spring-cloud-tutorials文件夾下有多個子工程,本篇的代碼是gateway-change-body,如下圖紅框所示:
准備工作
- 為了觀察Gateway能否按預期去修改請求和響應的body,咱們給服務提供者provider-hello增加一個接口,代碼在Hello.java中,如下:
@PostMapping("/change")
public Map<String, Object> change(@RequestBody Map<String, Object> map) {
map.put("response-tag", dateStr());
return map;
}
-
可見新增的web接口很簡單:將收到的請求數據作為返回值,在里面添加了一個鍵值對,然后返回給請求方,有了這個接口,咱們就能通過觀察返回值來判斷Gateway對請求和響應的操作是否生效
-
來試一下,先啟動nacos(provider-hello需要的)
-
再運行provider-hello應用,用Postman向其發請求試試,如下圖,符合預期:
- 准備工作已完成,開始開發吧
修改請求body的套路
- 如何用Spring Cloud Gateway修改請求的body?來看看其中的套路:
- 修改請求body是通過自定義filter實現的
- 配置路由及其filter的時候,有yml配置文件和代碼配置兩種方式可以配置路由,官方文檔給出的demo是代碼配置的,因此今天咱們也參考官方做法,通過代碼來配置路由和過濾器
- 在代碼配置路由的時候,調用filters方法,該方法的入參是個lambda表達式
- 此lambda表達式固定調用modifyRequestBody方法,咱們只要定義好modifyRequestBody方法的三個入參即可
- modifyRequestBody方法的第一個入參是輸入類型
- 第二個入參是返回類型
- 第三個是RewriteFunction接口的實現,這個代碼需要您自己寫,內容是將輸入數據轉換為返回類型數據具體邏輯,咱們來看官方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如出一轍
- 通過代碼來配置路由和過濾器
- 在代碼配置路由的時候,調用filters方法,該方法的入參是個lambda表達式
- 此lambda表達式固定調用modifyResponseBody方法,咱們只要定義好modifyResponseBody方法的三個入參即可
- modifyRequestBody方法的第一個入參是輸入類型
- 第二個入參是返回類型
- 第三個是RewriteFunction接口的實現,這個代碼要您自己寫,內容是將輸入數據轉換為返回類型數據具體邏輯,咱們來看官方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();
}
- 套路總結出來了,接下來,咱們一起擼代碼?
按套路開發一個修改請求body的過濾器(filter)
-
廢話不說,在父工程spring-cloud-tutorials下新建子工程gateway-change-body,pom.xml無任何特殊之處,注意依賴spring-cloud-starter-gateway即可
-
啟動類毫無新意:
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的實現類,代碼很簡單,將原始的請求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;
}
/**
* 根據用戶ID獲取用戶名稱的方法,可以按實際情況來內部實現,例如查庫或緩存,或者遠程調用
* @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操作出現異常時的處理
return Mono.error(new Exception("1. json process fail", ex));
}
}
}
- 然后是按部就班的基於代碼實現路由配置,重點是lambda表達式執行modifyRequestBody方法,並且將RequestBodyRewrite作為參數傳入:
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();
}
}
- 代碼寫完了,運行工程gateway-change-body,在postman發起請求,得到響應如下圖,紅框中可見Gateway添加的內容已成功:
- 現在修改請求body已經成功,接下來再來修改服務提供者響應的body
修改響應body
-
接下來開發修改響應body的代碼
-
新增RewriteFunction接口的實現類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方法內部調用modifyResponseBody,第三個入參是ResponseBodyRewrite:
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();
}
}
- 還記得咱們的第一個問題嗎?通過上面的代碼,您應該已經看到了答案:用代碼配置路由時,多個過濾器的配置方法就是在filters方法中反復調用內置的過濾器相關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
- 把gateway-change-body服務啟動起來,此時已經有了兩個路由配置,一個在代碼中,一個在yml中,先試試yml中的這個,如下圖沒問題:
- 再試試代碼配置的路由,如下圖,結論是代碼配置路由和yml配置可以混搭
如何處理異常
-
還有個問題必須要面對:修改請求或者響應body的過程中,如果發現問題需要提前返回錯誤(例如必要的字段不存在),代碼該怎么寫?
-
咱們修改請求body的代碼集中在RequestBodyRewrite.java,增加下圖紅框內容:
- 再來試試,這次請求參數中不包含user-id,收到Gateway返回的錯誤信息如下圖:
- 看看控制台,能看到代碼中拋出的異常信息:
-
此時,聰明的您應該發現問題所在了:咱們想告訴客戶端具體的錯誤,但實際上客戶端收到的是被Gateway框架處理后的內容
-
篇幅所限,上述問題從分析到解決的過程,就留給下一篇文章吧
-
本篇的最后,請容許欣宸嘮叨兩句,聊聊為何要網關來修改請求和響應body的內容,如果您沒興趣還請忽略
網關(Gateway)為什么要做這些?
-
看過開篇的兩個圖,聰明的您一定發現了問題:為什么要破壞原始數據,一旦系統出了問題如何定位是服務提供方還是網關?
-
按照欣宸之前的經驗,盡管網關會破壞原始數據,但只做一些簡單固定的處理,一般以添加數據為主,網關不了解業務,最常見的就是鑒權、添加身份或標簽等操作
-
前面的圖中確實感受不到網關的作用,但如果網關后面有多個服務提供者,如下圖,這時候諸如鑒權、獲取賬號信息等操作由網關統一完成,比每個后台分別實現一次更有效率,后台可以更加專注於自身業務:
-
經驗豐富的您可能會對我的狡辯不屑一顧:網關統一鑒權、獲取身份,一般會把身份信息放入請求的header中,也不會修改請求和響應的內容啊,欣宸前面的一堆解釋還是沒說清楚為啥要在網關位置修改請求和響應的內容!
-
好吧,面對聰明的您,我攤牌了:本篇只是從技術上演示Spring Cloud Gateway如何修改請求和響應內容,請不要將此技術與實際后台業務耦合;
你不孤單,欣宸原創一路相伴
歡迎關注公眾號:程序員欣宸
微信搜索「程序員欣宸」,我是欣宸,期待與您一同暢游Java世界...
https://github.com/zq2599/blog_demos