code学习

Spring Cloud Gateway中获取Request Body

作者:mtung

. 1. 简介

本文环境:springboot 2.6.4 springcloud 2021.0.1.0

关于Springcloud gateway的报文获取,网上的写法较多都是参考ModifyRequestBodyGatewayFilterFactory,确实可以解决问题,但相对来说对filter的顺序也做了比较死的约束,对于那些每种路由需要不同的过滤顺序的需求不能做到很好的支持,且据说性能下降剧烈(没有具体实验);

本文同样参考springcloud gateway 源码,只不过参考的是ReadBodyRoutePredicateFactory,相对上述方案来说有性能提升(没有实验佐证);

2. 参考ModifyRequestBodyGatewayFilterFactory的写法

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.DefaultServerRequest;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;


@Slf4j
@Component
public class RequestBodyOperationFilter implements GatewayFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        if (request.getMethod() != HttpMethod.POST) {
            return chain.filter(exchange);
        }
        return operationExchange(exchange, chain);
    }
    private Mono<Void> operationExchange(ServerWebExchange exchange, GatewayFilterChain chain) {
        // mediaType
        MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
        // read & modify body
        ServerRequest serverRequest = new DefaultServerRequest(exchange);
        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
                .flatMap(body -> {
                    if (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) {
                        // 对原先的body进行修改操作
                        String newBody = "{\"testName\":\"testValue\"}";
                        return Mono.just(newBody);
                    }
                    return Mono.empty();
                });
        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())
                .then(Mono.defer(() -> {
                    ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
                            exchange.getRequest()) {
                        @Override
                        public HttpHeaders getHeaders() {
                            long contentLength = headers.getContentLength();
                            HttpHeaders httpHeaders = new HttpHeaders();
                            httpHeaders.putAll(super.getHeaders());
                            if (contentLength > 0) {
                                httpHeaders.setContentLength(contentLength);
                            } else {
                                httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                            }
                            return httpHeaders;
                        }
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return outputMessage.getBody();
                        }
                    };
                    return chain.filter(exchange.mutate().request(decorator).build());
                }));
    }
    @Override
    public int getOrder() {
        return -1;
    }
}           

3. 参考ReadBodyRoutePredicateFactory

package com.nahefa.gateway.predicate;

import org.springframework.cloud.gateway.handler.AsyncPredicate;
import org.springframework.cloud.gateway.handler.predicate.ReadBodyRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

/**
 * ReadBodyRoutePredicateFactory 仅支持RouteLocator 的方式,对YML配置不友好
 * 为解决此问题,自定义一个路由谓词工厂,方便YML的配置
 * <p>
 * 例:
 * spring.cloud.gateway.routes[0].id=abc
 * spring.cloud.gateway.routes[0].uri=lb://wjj-abc
 * spring.cloud.gateway.routes[0].predicates[0]=Path=/abc/**
 * # 使用ReadBodyPredicateFactory断言,将body读入缓存
 * spring.cloud.gateway.routes[0].predicates[1].name=GatewayReadBodyPredicateFactory
 * <p>
 * 参考: https://github.com/spring-cloud/spring-cloud-gateway/issues/1307
 *
 * @author mtung 2022/11/30 15:49
 */
@Component
public class GatewayReadBodyPredicateFactory extends ReadBodyRoutePredicateFactory {

    /**
     * 必须等于ReadBodyRoutePredicateFactory中的REQUEST_BODY_OBJECT_KEY
     */
    public static final String REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";

    @Override
    public AsyncPredicate<ServerWebExchange> applyAsync(Config config) {
        config.setPredicate(t -> true);
        // 为减少配置麻烦,此处强制为字符串,使用方自行转响应类型
        config.setInClass(String.class);
        return super.applyAsync(config);
    }

    /**
     * 配置文件中对应的名称
     * spring.cloud.gateway.routes[0].predicates[0].name=xxx
     *
     * @return
     */
    @Override
    public String name() {
        return "GatewayReadBodyPredicateFactory";
    }
}
           

注意该方式不适用与表单方式提交

4.参考地址

https://github.com/spring-cloud/spring-cloud-gateway/issues/1307

继续阅读