Gateway學習筆記一 responseBody和resquestBody的獲取


1 引言

筆者在實現開發者服務網關模塊的任務過程中,遇到下列需求(有關requestBody和responseBody部分):

  • 對所有的請求,取出requestBody作為參數,調用鑒權接口
  • 不影響requsetBody前提下,路由轉發
  • 從路由轉發的回復中取出responseBody,作為參數調用統計接口

gateway的工作流程如圖,filter的傳遞中,我們通常用ServerWebExchange來獲取請求、回復、相關參數等。

我最初的思路,想當然地希望從ServerWebExchange中直接使用exchange.getXXX()方法獲取body,

 

然而實際上從exchange中無法讀取具體的requestBody和responseBody。

 

 

 

2 原因分析


2.1 ServerWebExchange

ServerWebExchange命名為服務網絡交換器,存放着重要的請求-響應屬性、請求實例和響應實例等等,有點像Context的角色

 

注意到ServerWebExchange.mutate()方法,通過使用decorator將exchange重新包裝起來。

ServerWebExchange實例可以理解為不可變實例,如果我們想要修改它,需要通過mutate()方法生成一個新的實例

 

 

2.2 ServerHttpRequest

ServerHttpRequest實例是用於承載請求相關的屬性和請求體。
Spring Cloud Gateway中底層使用Netty處理網絡請求,通過追溯源碼,可以從ReactorHttpHandlerAdapter中得知ServerWebExchange實例中持有的ServerHttpRequest實例的具體實現是ReactorServerHttpRequest

ReactorServerHttpRequest的父類AbstractServerHttpRequest中初始化內部屬性headers的時候把請求的HTTP頭部封裝為只讀的實例,所以不能直接從ServerHttpRequest實例中直接獲取請求實例並且進行修改。

 

如果要修改,需要使用2.1中提到的mutate方法重新包裝生成一個新實例,具體的實現在下面介紹。

 

3 解決方案


3.1 基於ReadBodyPredicateFactory的實現

ReadBodyPredicateFactory源碼指出,body只允許從request中讀取一次,再次讀取時會拋異常。因此對於已經讀取過的requestBody,為了不影響后期,需要對請求體內容進行二次包裝,即第一次讀取內容進行緩存,后面對同個請求體的讀取則直接返回緩存內容。

 

下面是router提供的body讀取方法,其中bodyToMono方法我們可以拿到完整的body內容,並返回指定類型inClass,body即為讀取到的請求體內容對應的數組。

下面是仿照ReadBodyPredicateFactory的方式獲取body的解決方案。
 1 public final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
 2 
 3 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
 4     log.info("仿照ReadBodyPredicateFactory的方式獲取body---------成功");
 5     return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange,
 6             (serverHttpRequest) -> ServerRequest
 7                     //用mutate()方法重新封裝
 8                     .create(exchange.mutate().request(serverHttpRequest)    
 9                             .build(), messageReaders)
10                     //獲取完整的body內容,轉成string
11                     .bodyToMono(String.class)    
12                     .doOnNext(bodyString -> {
13                             //以下是業務邏輯,把bodyString去除空格換行,再放入attributes中
14                             bodyString = bodyString.replaceAll("\r\n", "");
15                             bodyString = bodyString.replaceAll(" ", "");
16                         exchange.getAttributes().put(
17                                 Constants.AUTH_SIGN_VO_REQUEST_BODY, bodyString);
18                         log.info("放入參數的body為:{}", bodyString);
19                     })
20                     .then(chain.filter(exchange)));
21 }

用cacheRequestBodyAndRequest方法緩存了requestBody,本質上還是用ServerHttpRequestDecorator重新封裝了requestBody,覆蓋對應獲取請求體數據緩沖區,以達到多次讀取的目的。
缺陷就是,這里的bodyToMono的class寫死為string,不夠靈活。

3.2 基於ModifyRequestBodyGatewayFilterFactory的實現


官網提供了ModifyRequestBodyFilter和ModifyResponseBodyFilter來獲取修改body,但僅支持以Java DSL的方式來配置。

@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();
}

@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();
}

而在開發中,我們更傾向於使用yml配置的方式來配置filter,為了靈活開發,我在這里仿照ModifyRequestBodyFilter來實現了自己的全局過濾器

獲取reqeustBody最終方案
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
    Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)    //bodyToMono獲取body的內容
            .flatMap(
                    data -> {
                        try {
                            byte[] body = data.getBytes(StandardCharsets.UTF_8);
                            //以下是具體業務邏輯
                            String bodyString = new String(body);
                            //刪除所有換行和空格
                            bodyString = bodyString.replaceAll("\r\n", "");
                            bodyString = bodyString.replaceAll(" ", "");
                            log.debug("request body string is :{}", bodyString);
                            //把bodyString放入attribute供后續使用
                            exchange.getAttributes().put(
                                    Constants.AUTH_SIGN_VO_REQUEST_BODY, bodyString);
                        } catch (ExceptionWithErrorCode e) {
                            return Mono.error(e);
                        }
                        return Mono.just(data);
                    });
    //重新封裝修改后的body,本業務中實際無修改,但也必須重新封裝body
    BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
    HttpHeaders headers = new HttpHeaders();
    headers.putAll(exchange.getRequest().getHeaders());
    headers.remove(HttpHeaders.CONTENT_LENGTH);
    CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
    return bodyInserter.insert(outputMessage, new BodyInserterContext())
            // .log("modify_request", Level.INFO)
            .then(Mono.defer(() -> {
                ServerHttpRequestDecorator decorator = CommonUtils.decorate(exchange, headers, outputMessage);
                //decorate重新封裝
                return chain.filter(exchange.mutate().request(decorator).build());
            }));
}
  • 失敗嘗試

用正確實現取body,但不使用bodyInserter重新封裝request

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
    return serverRequest.bodyToMono(String.class)
            .flatMap(
                    //同上實現,省略
                    ……
                    }).then(chain.filter(exchange));
}

結果,每兩次調用,第二次都會報錯

 

前面也提到,requestBody只允許訪問一次,若不重新設置的話會導致后面訪問拋異常。

 

3.3 思路三


筆者在查詢資料過程中,看到網上博文提供了第三種解決思路:
由於從exchange.getRequest中獲取的是FluxMap類型,因此可以通過重寫Flux<DataBuffer> getBody()方法,包裝后的請求放到過濾器鏈中傳遞下去。這樣后面的過濾器中再使用exchange.getRequest().getBody()來獲取body時,實際上就是調用的重載后的getBody方法,獲取的最先已經緩存了的body數據。這樣就能夠實現body的多次讀取了。
不過這種思路同樣繞不過要使用ServerHttpRequestDecorator這個請求裝飾器對request進行重新包裝。

3.4 獲取responseBody


responseBody的獲取方法也是同樣的思路,可以參找ModifyResponseBodyGatewayFilterFactory的實現方式

下面是獲取resopnseBody的解決方案
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerHttpResponse originalResponse = exchange.getResponse();
    originalResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON);

    DataBufferFactory bufferFactory = originalResponse.bufferFactory();
    ServerHttpResponseDecorator response = new ServerHttpResponseDecorator(originalResponse) {
        @Override
        public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
            if (getStatusCode().equals(HttpStatus.OK) && body instanceof Flux) {
                Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
                    DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                    //dataBuffer合並成一個,解決獲取結果不全問題
                    DataBuffer join = dataBufferFactory.join(dataBuffers);
                    byte[] content = new byte[join.readableByteCount()];
                    join.read(content);
                    DataBufferUtils.release(join);
                    // 轉為字符串
                    String responseData = new String(content, Charsets.UTF_8);
                    /** 業務邏輯 */
                    //去除所有換行和空格
                    responseData = responseData.replaceAll("\r\n", "");
                    responseData = responseData.replaceAll(" ", "");
                    log.debug("responseBody string is:{}", responseData);
                    exchange.getAttributes().put(Constants.AUTH_SIGN_VO_RESPONSE_BODY, responseData);
                    byte[] uppedContent = responseData.getBytes(Charsets.UTF_8);
                    originalResponse.getHeaders().setContentLength(uppedContent.length);
                    return bufferFactory.wrap(uppedContent);
                }));
            }
            return super.writeWith(body);
        }

        @Override
        public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
            return writeWith(Flux.from(body).flatMapSequential(p -> p));
        }
    };
    return chain.filter(exchange.mutate().response(response).build());
}

 

4 總結

  • ServerWebExchange中存放着重要的請求-響應屬性、請求實例和響應實例,類似於上下文
  • ReadBodyPredicateFactory源碼指出,body只允許從request中讀取一次,再次讀取時會拋異常。因此需要二次封裝
  • bodyToMono()方法用於獲取body內容,ServerHttpResponseDecorator用於重新封裝請求,ServerWebExchange.mutate()方法用於重新生成實例
  • 可以參照ModifyRequestBodyGatewayFilterFactory 和ReadBodyPredicateFactory實現上述需求,本質都是獲取並緩存body后二次封裝。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM