spring cloud gateway獲取response body

網關發起請求後,微服務返回的response的值要通過網關才發給客戶端。本文主要講解在spring cloud gateway 的過濾器中獲取微服務的返回值,由於不少狀況咱們須要對這個返回進行處理。網上有不少例子,可是都沒有解決個人實際問題,最後研究了下源碼找到了解決方案。html

本節內容主要從以下幾個方面講解,首先須要瞭解個人博文的內容:API網關spring cloud gateway和負載均衡框架ribbon實戰spring cloud gateway自定義過濾器 本文也將根據上面兩個項目的代碼進行擴展。代碼見spring-cloudjava

  • 新增一個rest接口:咱們在三個服務提供者(provider100一、provider100二、provider1003)裏面新建一個查詢人羣信息接口。 

本次代碼spring cloud gateway獲取response bodyreact

一:新增一個rest接口


1,首先在github上面把spring-cloud 克隆到本地。啓動三個服務提供者,再啓動網關,經過網關能正常訪問服務,而後再根據下面的代碼進行本節課的學學習。git

注意:github

  • gateway配置文件的 - Auth 最好註釋起來,除非使用postman把認證信息傳進去,能夠參考本節開頭提到的兩篇博客進行操做。

2,新建一個rest接口,返回大量的數據web

注意三個provider裏面都要添加一個這樣的類,內容spring

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就能夠看到結果輸出。apache

至此咱們在原有的微服務上面增長了一個接口,而且經過網關能正常訪問。瀏覽器

二:需求


如今咱們須要新建一個局部或者全局過濾器,在過濾器中獲取微服務返回的body。在網上查詢的時候,發現不少博文都沒有講清楚,主要體如今如下幾點:app

  • 報文能夠在網關的過濾器中獲取,可是沒有返回到客戶端
  • 報文體太長,致使返回客戶端的報文不全
  • 中文亂碼

我根據不少博文中的內容進行測試,都沒法知足個人需求。因而看了官網的5.29節:說明只能夠經過 配置類 的方式來獲取返回的body內容。

5.29

第一小節咱們啓動了三個 provider和gateway進行測試,如今爲了測試配置類這中形式,咱們只啓動上面的一個provider1001,而後新建一個簡單的gateway,注意此gateway代碼我不會上傳到GitHub,只是驗證官網給的例子是正確的。

一、新建一個工程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

四、測試:在瀏覽器訪問 http://localhost:8000/gateway/persons 若是能夠獲取到結果則說明咱們經過網關調用到了微服務。

五、使用配置類的形式:咱們從官網已經知道,經過配置類的形式能夠在代碼中獲取到返回的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;
        }
    }
}
相關文章
相關標籤/搜索