优雅的实现spring-cloud-gateway修改请求和响应体

本文介绍了一种优雅地在Spring Cloud Gateway中监控请求和响应体的方法,通过自定义全局过滤器,实现对请求体和响应体的修改与日志记录,解决了请求体只能读取一次的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

# 优雅的实现spring-cloud-gateway修改请求和响应体
 
## 引言
    最近公司的微服务越来越多,管理起来比较麻烦,决定引入微服务网关。咨询了百度老师发现Spring Cloud gateway功能已经比较完善,性能也还很不错。决定就是它了。
    建工程,加依赖,写配置,一切看起来都那么的简单加顺利。
    只是某天某人突然提了一个需求,为了方便调试让我增加1个可以查看每个post+json+restApi请求的请求头,请求体,响应头,响应体的功能。
    我想这还不简单嘛,直接加个全局过滤器就搞定了。
    赶紧加班加点写了一个过滤器实现GlobalFilter和Ordered接口,在接口中接收获取请求和响应的信息通过日志打印出来就OK了吧。
    
    理想是美好的,现实是残酷的。才发现遇到了不好处理的问题。
## 遇到的问题

* 1.ServerHttpRequest请求对象的请求体只能获取一次,一旦获取了就不能继续往下传递。
    Flux<DataBuffer> getBody();

* 2.请求体方法返回的是基于webFlux响应式的函数结构,特别难处理。
     
* 3.ServerHttpResponse响应体根本就没有获取响应体的方法。

* 4.系统提供的修改请求体和响应体的类只实现了GatewayFilter过滤器,无法对全局进行监控而且还只能通过Java DSL进行配置.不可能每增加一个服务都修改代码。我需要的是全局过滤器。
    ModifyRequestBodyGatewayFilterFactory  系统提供的修改请求体的类
    ModifyResponseBodyGatewayFilterFactory 系统提供的修改响应体的类
## 解决方案
 
 查询了网上很多的实现方法始终都不能完美的处理。不是丢包就是根本获取不到数据。
 
 基本思想:代理实现ServerHttpRequest和ServerHttpResponse类,自己先处理请求和响应体,然后重新生成1个新的数据返回出去。网上大多数都是这种方案。
 
 想起组件本身提供的有ModifyRequestBodyGatewayFilterFactory和ModifyResponseBodyGatewayFilterFactory来对请求和响应体进行修改。
 
 不过研究之后发现。这两个类生成的是GatewayFilter路由过滤器,而我们需要GlobalFilter(全局过滤器)
 
 要么把这个类拷贝出来重新自己实现1个基于全局过滤器的版本。不过这样代码改动比较大,而且重复严重,哪怕是跟组件本身的代码重复这也是不允许的。不提倡重复造轮子。

 想到 GatewayFilter和GlobalFilter过滤器除了实现的接口不同,它要实现的方法签名都是相同的。既然官方已经实现了GatewayFilter版本的,我应该可以代理直接使用。
 
 设计思路:提供一个类专门用来处理请求和响应。可以实现接口RewriteFunction,为了统一处理,输入输出参数都用byte[].代码如下:
 
 ```
  
 import java.util.stream.Collectors;
 import org.reactivestreams.Publisher;
 import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
 import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.server.reactive.ServerHttpRequest;
 import org.springframework.http.server.reactive.ServerHttpResponse;
 import org.springframework.web.server.ServerWebExchange;
 
 import lombok.extern.slf4j.Slf4j;
 import reactor.core.publisher.Mono;
 /**
  * 泛型参数1:源请求体类,源响应体
  * 泛型参数2:新请求体类,新响应体
  * @author cqyhm
  *
  */
 @Slf4j
 public class BodyRewrite implements RewriteFunction<byte[], byte[]> {
    
    /**
     * 在执行全局请求或响应的过滤器的时候会执行该方法,并把请求体或响应体传递进来。
     * @param exchange 网关处理上下文
     * @param body 源请求或响应体
     * @return 返回处理过的请求体或响应体
     */
     @Override
     public Publisher<byte[]> apply(ServerWebExchange exchange, byte[] body) {
        //如果路由没有完成应该是请求过滤器执行
         if(!ServerWebExchangeUtils.isAlreadyRouted(exchange)) {            
             exchange.getAttributes().put("request_key", new String(body));  //保存请求体到全局上下文中
             exchange.getAttributes().put("startTime", System.currentTimeMillis()); //保存启动时间到上下中
            //TODO 可以在这里对请求体进行修改
         } else { //已经路由应该是响应过滤器执行
            //TODO 可以在这里对响应体进行修改
             response(exchange, body);
         }
         return Mono.just(body);
     }
    /**
     *  打印输出响应的参数,请求体,响应体,请求头部,响应头部,请求地址,请求方法等。
     *  @param exchange 网关处理上下文
     *  @param body 源请求或响应体
     *  @return 返回处理过的请求体或响应体
     */
     public byte[] response(ServerWebExchange exchange,  byte[] responseBody) {
         try {
             ServerHttpRequest request=exchange.getRequest();
             ServerHttpResponse response=exchange.getResponse();
             String requestbody=exchange.getAttribute("request_key");
             Long startTime=exchange.getAttributeOrDefault("startTime", 0L);
             Long time=System.currentTimeMillis()-startTime;
             boolean flag=MediaType.APPLICATION_JSON.isCompatibleWith(response.getHeaders().getContentType());
             //responseBody=objectMapper.writeValueAsString(MessageBox.ok());
             log.info("\n[{}]请求地址:\n\t{} {}\n[{}]请求头部:\n{}\n[{}]路径参数:\n{}\n[{}]请求参数:\n{}"
                      + "\n[{}]响应头部:\n{}\n[{}]响应内容:\n\t{}\n[{}]执行时间[{}]毫秒", 
                     request.getId(), request.getMethod(), request.getPath(),
                     request.getId(), headers(request.getHeaders()), 
                     request.getId(), request(request),
                     request.getId(), requestbody,
                     request.getId(), headers(response.getHeaders()),
                     request.getId(), flag ? new String(responseBody) : "非JSON字符串不显示", 
                     request.getId(),    time);
             //TODO 可以对响应体进行修改
             return responseBody;        
         } catch (Exception e) {
             throw new RuntimeException("响应转换错误");
         } finally {
             exchange.getAttributes().remove("request_key");
             exchange.getAttributes().remove("startTime");
         }
     }
     
     public String headers(HttpHeaders headers) {
         return headers.entrySet().stream()
                 .map(entry -> "\t" + entry.getKey() + ": [" + String.join(";", entry.getValue()) + "]")
                 .collect(Collectors.joining("\n"));
     }
     /**
      * 处理其它get请求参数
      */
     public String request(ServerHttpRequest request) {
         String params=request.getQueryParams().entrySet().stream()
                 .map(entry -> "\t" + entry.getKey() + ": [" + String.join(";", entry.getValue()) + "]")
                 .collect(Collectors.joining("\n"));
         return params;
     }
 }
 ```
    业务处理逻辑写好之后,剩下的就是配置了。我这里只是把参数打印日志没有做处理,你甚至可以在这里修改请求体和响应体。
    代理ModifyRequestBodyGatewayFilterFactory和ModifyResponseBodyGatewayFilterFactory类中的GatewayFilter来定义GlobalFilter过滤器。

      这里是使用内部类直接实现的,但是指定GlobalFilter的顺序有问题导致无法监控,最好的办法还是单独设计两个GlobalFilter过滤器类。
    
    查看代码:
```
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.server.ServerWebExchange;

import reactor.core.publisher.Mono;

@Configuration
@ConditionalOnProperty(prefix = "logging.filter", name = "enabled", havingValue = "true", matchIfMissing = false)
public class CustomGatewayConfig {
    
    @Bean
    public BodyRewrite bodyRewrite() {
        return new BodyRewrite();
    }
    /**
     * 定义全局拦截器拦截请求体
     */
    @Bean
    @Order(NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 2)   //指定顺序必须在之前
    public GlobalFilter requestFilter(ModifyRequestBodyGatewayFilterFactory modifyRequestBody) {        
        GatewayFilter delegate=modifyRequestBody.apply(new ModifyRequestBodyGatewayFilterFactory.Config()
                                                 .setRewriteFunction(byte[].class, byte[].class, bodyRewrite()));
        return new GlobalFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                return delegate.filter(exchange, chain); 
            }
        };
    }
    /**
     * 定义全局拦截器拦截响应体
     */
    @Bean
    @Order(NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 2) //指定顺序必须在之前
    public GlobalFilter responseFilter(ModifyResponseBodyGatewayFilterFactory modifyResponseBody) {
        GatewayFilter delegate=modifyResponseBody.apply(new ModifyResponseBodyGatewayFilterFactory.Config()
                                                 .setRewriteFunction(byte[].class, byte[].class, bodyRewrite()));
        return new GlobalFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                return delegate.filter(exchange, chain);
            }
        };
    }
}
```
    关键的地方就在于官方提供的类ModifyResponseBodyGatewayFilterFactory提供的方法apply
```    
    public GatewayFilter apply(Config config);
```    
    该方法返回的是GatewayFilter我们正好可以利用它的方法filter方法
```    
    public Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain) 
```
   我们只需要构造1个它的参数config就好了。分析源代码发现它的参数有个setRewriteFunction函数
   
```
   /**
    * @param inClass   请求体的原始数据类型,我们统一使用byte[]
    * @param outClass 请求体变更过后的数据类型,我们统一使用byte[]
    * @param rewriteFunction 对请求体进行处理的类。就是我们前面定义的BodyRewrite的对象
    */
   public <T, R> Config setRewriteFunction(Class<T> inClass, Class<R> outClass, RewriteFunction<T, R> rewriteFunction)   
   
```   
  执行了ModifyResponseBodyGatewayFilterFactory的apply方法传入config参数能够得到1个唯1个GatewayFilter对象
```  
  GatewayFilter delegate=modifyResponseBody.apply(new ModifyResponseBodyGatewayFilterFactory.Config()
                                           .setRewriteFunction(byte[].class, byte[].class, bodyRewrite()));
   
```
  只需要在我们自定义的 GlobalFilter过滤器中调用delegate.filter(exchange, chain)方法
```  
  return new GlobalFilter() {
      @Override
      public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
          return delegate.filter(exchange, chain);
      }
  };
```

      以上简化的方法设置可能存在在某些版本上无法监控的问题,只需要把请求过滤器和响应过滤器完全独立出来开发就可以了。具体为什么,我还没来得及分析。以下是请求过滤器。响应过滤器类似。

```

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;

import reactor.core.publisher.Mono;

/**
 * 全局请求体过滤器
 * @author cqyhm
 *
 */
public class RequestBodyFilter implements GlobalFilter, Ordered {
    
    private GatewayFilter delegate;
    public RequestBodyFilter(ModifyRequestBodyGatewayFilterFactory modifyRequestBody, BodyRewrite bodyRewrite) {
        this.delegate=modifyRequestBody.apply(new ModifyRequestBodyGatewayFilterFactory.Config()
                                                             .setRewriteFunction(byte[].class, byte[].class, bodyRewrite));
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return delegate.filter(exchange, chain);
    }
    
    @Override
    public int getOrder() {
        return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 2;
    }
}
```

至于响应过滤器跟这个差不多,一葫芦画瓢很快就能写1个出来。

```

/**
 * 全局响应体过滤器
 */
public class ResponseBodyFilter implements GlobalFilter, Ordered  {
    
    private GatewayFilter delegate;
    public ResponseBodyFilter(ModifyResponseBodyGatewayFilterFactory modifyResponseBody, BodyRewrite bodyRewrite) {
        this.delegate=modifyResponseBody.apply(new ModifyResponseBodyGatewayFilterFactory.Config()
                                                                .setRewriteFunction(byte[].class, byte[].class, bodyRewrite));
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return delegate.filter(exchange, chain);
    }
    
    @Override
    public int getOrder() {
        return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 2;
    }
}

```

 然后注册这两个Bean基本上都可以了。
  这种方案应该是最将简便的,最优雅的,只需要实现我们的业务类BodyRewrite其它都直接使用Java config配置,对系统改动小,侵入性低。
  
  这种方案目前正在验证当中,有什么问题欢迎交流。
  好了,这就大功告成了。
 

 

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值