網關發起請求后,微服務返回的response的值要經過網關才發給客戶端。本文主要講解在spring cloud gateway 的過濾器中獲取微服務的返回值,因為很多情況我們需要對這個返回進行處理。網上有很多例子,但是都沒有解決我的實際問題,最后研究了下源碼找到了解決方案。
本節內容主要從如下幾個方面講解,首先需要了解我的博文的內容:API網關spring cloud gateway和負載均衡框架ribbon實戰 和 spring cloud gateway自定義過濾器 本文也將根據上面兩個項目的代碼進行擴展。代碼見spring-cloud 。
- 新增一個rest接口:我們在三個服務提供者(provider1001、provider1002、provider1003)里面新建一個查詢人群信息接口。
本次代碼:spring cloud gateway獲取response body
一:新增一個rest接口
1,首先在github上面把spring-cloud 克隆到本地。啟動三個服務提供者,再啟動網關,通過網關能正常訪問服務,然后再根據下面的代碼進行本節課的學學習。
注意:
- gateway配置文件的 - Auth 最好注釋起來,除非使用postman把認證信息傳進去,可以參考本節開頭提到的兩篇博客進行操作。
2,新建一個rest接口,返回大量的數據
注意三個provider里面都要添加一個這樣的類,內容
package com.yefengyu.cloud.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.List; @RestController public class PersonController { @GetMapping("/persons") public List<Person> listPerson(){ List<Person> personList = new ArrayList<>(); for(int i = 0; i < 100; i++){ Person person = new Person(); person.setId(i); person.setName("王五" + i); person.setAddress("北京" + i); personList.add(person); } return personList; } public static class Person{ private Integer id; private String name; private String address; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } @Override public String toString() { return "Person{" + "id=" + id + ", name='" + name + '\'' + ", address='" + address + '\'' + '}'; } } }
一模一樣,無需區分是哪個provider 返回的,本節重點不是負載均衡。然后先重啟三個provider,再重啟gateway,通過gateway訪問這個接口。在瀏覽器輸入http://localhost:8080/gateway/persons就可以看到結果輸出。
至此我們在原有的微服務上面增加了一個接口,並且通過網關能正常訪問。
二:需求
現在我們需要新建一個局部或者全局過濾器,在過濾器中獲取微服務返回的body。在網上查詢的時候,發現很多博文都沒有講清楚,主要體現在以下幾點:
- 報文可以在網關的過濾器中獲取,但是沒有返回到客戶端
- 報文體太長,導致返回客戶端的報文不全
- 中文亂碼
我根據很多博文中的內容進行測試,都無法滿足我的需求。於是看了官網的5.29節:說明只可以通過 配置類 的方式來獲取返回的body內容。

第一小節我們啟動了三個 provider和gateway進行測試,現在為了測試配置類這中形式,我們只啟動上面的一個provider1001,然后新建一個簡單的gateway,注意此gateway代碼我不會上傳到GitHub,只是驗證官網給的例子是正確的。
1、新建一個工程gateway,添加依賴如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.yefengyu.cloud</groupId> <artifactId>gateway</artifactId> <version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
</dependencies>
</project>
2,新建一個啟動類GatewayApplication
package com.yefengyu.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class); } }
3,新建一個配置文件application.yml
server: port: 8000 spring: application: name: gateway_server cloud: gateway: default-filters: routes: - id: my_route uri: http://localhost:1001 predicates: - Path=/gateway/** filters: - StripPrefix=1
4、測試:在瀏覽器訪問 http://localhost:8000/gateway/persons 如果可以獲取到結果則說明我們通過網關調用到了微服務。
5、使用配置類的形式:我們從官網已經知道,通過配置類的形式可以在代碼中獲取到返回的body內容。那么我們新建一個配置類:
package com.yefengyu.cloud; 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 reactor.core.publisher.Mono; @Configuration public class MyConfig { @Bean public RouteLocator routes(RouteLocatorBuilder builder) { return builder.routes() .route("my_route", r -> r.path("/gateway/**") .filters(f -> f.stripPrefix(1) .modifyResponseBody(String.class, String.class, (exchange, s) -> { //TODO: 此處可以獲取返回體的所有信息 System.out.println(s); return Mono.just(s); })).uri("http://localhost:1001")) .build(); } }
特別注意:
- 官網的例子,拷貝過來少了一個括號,在.build的點之前加一個括號,注意對比。
- 官網代碼中的host改為了path。
- 對照代碼和上面的修改之前的yml文件,可知他們是一一對應的。
6,配置文件需要修改,不需要使用配置文件形式的,只需要配置端口即可
server:
port: 8000
7,測試:訪問 http://localhost:8000/gateway/persons ,不僅瀏覽器可以返回數據,控制台也可以打印數據,也就是說我們在代碼中可以獲取到完整的body數據了。
三:更進一步
我們一般喜歡使用配置文件的形式,而不是配置類的形式來配置網關,那么怎么實現呢?這次我們拋棄上面臨時新建的gateway工程,那只是驗證配置類形式來獲取body的。下面我們依然使用從GitHub下載的代碼,在那里面來研究。三個provider 無需變動,只要啟動就好。
我們思考下:為什么使用配置類的形式就能獲取到返回body的數據呢?這是因為spring cloud gateway內部已經實現了這個過濾器(ModifyResponseBodyGatewayFilterFactory),我們要做的是模仿他重新寫一個。
1,在我們的網關代碼中,我們新建一個局部過濾器ResponseBodyGatewayFilterFactory,並把剛才所有的代碼拷貝進去:注意不要拷貝包,並且把ModifyResponseBodyGatewayFilterFactory全部替換為ResponseBodyGatewayFilterFactory。
2,去掉代碼中的配置類
public class ModifyResponseBodyGatewayFilterFactory extends AbstractGatewayFilterFactory<ModifyResponseBodyGatewayFilterFactory.Config> {
替換為
public class ResponseBodyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
3,刪除
public ResponseBodyGatewayFilterFactory() { super(Config.class); } @Deprecated public ResponseBodyGatewayFilterFactory(ServerCodecConfigurer codecConfigurer) { this(); }
4,apply方法更改
@Override public GatewayFilter apply(Config config) { ModifyResponseGatewayFilter gatewayFilter = new ModifyResponseGatewayFilter( config); gatewayFilter.setFactory(this); return gatewayFilter; }
替換為
@Override public GatewayFilter apply(Object config) { return new ModifyResponseGatewayFilter(); }
注意有錯誤暫時不管。
5,刪除靜態內部類Config的所有內容,直到下面的ModifyResponseGatewayFilter類定義處。下面我們來操作ModifyResponseGatewayFilter類內部內容。
6,刪除
private final Config config; private GatewayFilterFactory<Config> gatewayFilterFactory; public ModifyResponseGatewayFilter(Config config) { this.config = config; }
7,刪除
@Override public String toString() { Object obj = (this.gatewayFilterFactory != null) ? this.gatewayFilterFactory : this; return filterToStringCreator(obj) .append("New content type", config.getNewContentType()) .append("In class", config.getInClass()) .append("Out class", config.getOutClass()).toString(); } public void setFactory(GatewayFilterFactory<Config> gatewayFilterFactory) { this.gatewayFilterFactory = gatewayFilterFactory; }
8,刪除無效和錯誤的依賴引入,特別是:
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
9,此時還有三處報錯,都是config對象引起的。第一二處:
Class inClass = config.getInClass();
Class outClass = config.getOutClass();
改為:
Class inClass = String.class; Class outClass = String.class;
第三處:
Mono modifiedBody = clientResponse.bodyToMono(inClass) .flatMap(originalBody -> config.rewriteFunction .apply(exchange, originalBody));
這里最為重要,是我們獲取返回報文的地方。改為:
Mono modifiedBody = clientResponse.bodyToMono(inClass) .flatMap(originalBody -> { //TODO:此次可以對返回的body進行操作 System.out.println(originalBody); return Mono.just(originalBody); });
10,配置文件增加這個局部過濾器ResponseBody即可:
filters: - StripPrefix=1 # - Auth - IPForbid=0:0:0:0:0:0:0:1 - ResponseBody
11,將ResponseBodyGatewayFilterFactory注冊到容器中,添加一個@Component注解即可。
12,啟動網關,訪問 http://localhost:8080/gateway/persons ,不僅瀏覽器可以返回數據,控制台也可以打印數據,也就是說我們在過濾器的代碼中可以獲取到完整的body數據了。
完整代碼如下:
package com.yefengyu.gateway.local; import org.reactivestreams.Publisher; import org.springframework.cloud.gateway.support.CachedBodyOutputMessage; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter; import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; import org.springframework.cloud.gateway.support.BodyInserterContext; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.server.ServerWebExchange; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR; @Component public class ResponseBodyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> { @Override public GatewayFilter apply(Object config) { return new ModifyResponseGatewayFilter(); } public class ModifyResponseGatewayFilter implements GatewayFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { return chain.filter(exchange.mutate().response(decorate(exchange)).build()); } @SuppressWarnings("unchecked") ServerHttpResponse decorate(ServerWebExchange exchange) { return new ServerHttpResponseDecorator(exchange.getResponse()) { @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { Class inClass = String.class; Class outClass = String.class; String originalResponseContentType = exchange .getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.add(HttpHeaders.CONTENT_TYPE, originalResponseContentType); ClientResponse clientResponse = ClientResponse .create(exchange.getResponse().getStatusCode()) .headers(headers -> headers.putAll(httpHeaders)) .body(Flux.from(body)).build(); Mono modifiedBody = clientResponse.bodyToMono(inClass) .flatMap(originalBody -> { //TODO:此次可以對返回的body進行操作 System.out.println(originalBody); return Mono.just(originalBody); }); BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass); CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage( exchange, exchange.getResponse().getHeaders()); return bodyInserter.insert(outputMessage, new BodyInserterContext()) .then(Mono.defer(() -> { Flux<DataBuffer> messageBody = outputMessage.getBody(); HttpHeaders headers = getDelegate().getHeaders(); if (!headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) { messageBody = messageBody.doOnNext(data -> headers .setContentLength(data.readableByteCount())); } return getDelegate().writeWith(messageBody); })); } @Override public Mono<Void> writeAndFlushWith( Publisher<? extends Publisher<? extends DataBuffer>> body) { return writeWith(Flux.from(body).flatMapSequential(p -> p)); } }; } @Override public int getOrder() { return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1; } } }
