# 优雅的实现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配置,对系统改动小,侵入性低。
这种方案目前正在验证当中,有什么问题欢迎交流。
好了,这就大功告成了。