Spring Cloud Gateway 2.x 打印 Log
场景
在服务网关层面,需要打印出用户每次的请求body和其他的参数,gateway使用的是Reactor响应式编程,和Zuul网关获取流的写法还有些不同,
不过基本的思路是一样的,都是在filter中读取body流,然后缓存回去,因为body流,框架默认只允许读取一次。
思路
1. 添加一个filter做一次请求的拦截
GatewayConfig.java
添加一个配置类,配置一个高优先级的filter,并且注入一个PayloadServerWebExchangeDecorator
对request和response做包装的类。
package com.demo.gateway2x.config;
import com.demo.gateway2x.decorator.PayloadServerWebExchangeDecorator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.server.WebFilter;
@Configuration
public class GatewayConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE) //过滤器顺序
public WebFilter webFilter() {
return (exchange, chain) -> chain.filter(new PayloadServerWebExchangeDecorator(exchange));
}
}
PayloadServerWebExchangeDecorator.java
这个类中,我们实现了框架的ServerWebExchangeDecorator
类,同时注入了自定义的两个类,PartnerServerHttpRequestDecorator
和 PartnerServerHttpResponseDecorator
,
这两个类用于后面对请求与响应的拦截。
package com.demo.gateway2x.decorator;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebExchangeDecorator;
public class PayloadServerWebExchangeDecorator extends ServerWebExchangeDecorator {
private PartnerServerHttpRequestDecorator requestDecorator;
private PartnerServerHttpResponseDecorator responseDecorator;
public PayloadServerWebExchangeDecorator(ServerWebExchange delegate) {
super(delegate);
requestDecorator = new PartnerServerHttpRequestDecorator(delegate.getRequest());
responseDecorator = new PartnerServerHttpResponseDecorator(delegate.getResponse());
}
@Override
public ServerHttpRequest getRequest() {
return requestDecorator;
}
@Override
public ServerHttpResponse getResponse() {
return responseDecorator;
}
}
2. 在请求进入时,对request做一次拦截
PartnerServerHttpRequestDecorator.java
这个类实现了 ServerHttpRequestDecorator
, 并在构造函数中,使用响应式编程,调用了打印log的方法,注意关注 Mono<DataBuffer> mono = DataBufferUtils.join(flux);
,
这里将Flux合并成了一个Mono,因为如果不这么做,body内容过多,将会被分段打印,这里是一个恒重要的点,
在打印RequestParamsHandle.chain
打印过日志后,我们又返回了一个dataBuffer
,用作向下传递,否则dataBuffer
被读取过一次后就不能继续使用了。
package com.demo.gateway2x.decorator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static reactor.core.scheduler.Schedulers.single;
@Slf4j
public class PartnerServerHttpRequestDecorator extends ServerHttpRequestDecorator {
private Flux<DataBuffer> body;
public PartnerServerHttpRequestDecorator(ServerHttpRequest delegate) {
super(delegate);
Flux<DataBuffer> flux = super.getBody();
if (ParamsUtils.CHAIN_MEDIA_TYPE.contains(delegate.getHeaders().getContentType())) {
Mono<DataBuffer> mono = DataBufferUtils.join(flux);
body = mono.publishOn(single()).map(dataBuffer -> RequestParamsHandle.chain(delegate, log, dataBuffer)).flux();
} else {
body = flux;
}
}
@Override
public Flux<DataBuffer> getBody() {
return body;
}
}
RequestParamsHandle.java
这个类主要用来读取dataBuffer
并做了日志打印处理,也可以做一些其他的例如参数校验等使用。
package com.demo.gateway2x.decorator;
import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
public class RequestParamsHandle {
public static <T extends DataBuffer> T chain(ServerHttpRequest delegate, Logger log, T buffer) {
ParamsUtils.BodyDecorator bodyDecorator = ParamsUtils.buildBodyDecorator(buffer);
// 参数校验 和 参数打印
log.info("Payload: {}", JSON.toJSONString(validParams(getParams(delegate, bodyDecorator.getBody()))));
return (T) bodyDecorator.getDataBuffer();
}
public static Map<String,Object> getParams(ServerHttpRequest delegate, String body) {
// 整理参数
Map<String,Object> params = new HashMap<>();
if (delegate.getQueryParams() != null) {
params.putAll(delegate.getQueryParams());
}
if (!StringUtils.isEmpty(body)) {
params.putAll(JSON.parseObject(body));
}
return params;
}
public static Map<String,Object> validParams(Map<String,Object> params) {
// todo 参数校验
return params;
}
}
3. 在结果返回时,对response做一次拦截
PartnerServerHttpResponseDecorator.java
这个类和上面的request的异曲同工,拦截响应流,并做记录入处理。
package com.demo.gateway2x.decorator;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static reactor.core.scheduler.Schedulers.single;
@Slf4j
public class PartnerServerHttpResponseDecorator extends ServerHttpResponseDecorator {
PartnerServerHttpResponseDecorator(ServerHttpResponse delegate) {
super(delegate);
}
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return super.writeAndFlushWith(body);
}
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
final MediaType contentType = super.getHeaders().getContentType();
if (ParamsUtils.CHAIN_MEDIA_TYPE.contains(contentType)) {
if (body instanceof Mono) {
final Mono<DataBuffer> monoBody = (Mono<DataBuffer>) body;
return super.writeWith(monoBody.publishOn(single()).map(dataBuffer -> ResponseParamsHandle.chain(log, dataBuffer)));
} else if (body instanceof Flux) {
Mono<DataBuffer> mono = DataBufferUtils.join(body);
final Flux<DataBuffer> monoBody = mono.publishOn(single()).map(dataBuffer -> ResponseParamsHandle.chain(log, dataBuffer)).flux();
return super.writeWith(monoBody);
}
}
return super.writeWith(body);
}
}
ResponseParamsHandle.java
响应流的日志打印
package com.demo.gateway2x.decorator;
import org.slf4j.Logger;
import org.springframework.core.io.buffer.DataBuffer;
public class ResponseParamsHandle {
public static <T extends DataBuffer> T chain(Logger log, T buffer) {
ParamsUtils.BodyDecorator bodyDecorator = ParamsUtils.buildBodyDecorator(buffer);
// 参数校验 和 参数打印
log.info("Payload: {}", bodyDecorator.getBody());
return (T) bodyDecorator.getDataBuffer();
}
}
下面是实际操作,发送一次http请求:
控制台log结果:
github源码地址:https://github.com/qiaomengnan16/gateway-2x-log-demo
总结
gateway和zuul打印参数的方式思路是一致的,只是gateway采用的是reactor,写法上与zuul的直接读取流有些不同,这里需要知道的是Flux需要转换为Mono这个地方,如果不转换容易分多批打印。
参考学习了以下的博客:
自定义Spring Webflux 过滤器,解决请求body只能获取一次的问题 :https://my.oschina.net/junjunyuanyuankeke/blog/2253493
SpringCloud Gateway获取post请求体(request body):https://blog.51cto.com/thinklili/2329184
如何在Reactive Java中从Mono流获取字符串/对象?:https://www.bilibili.com/read/cv5787745
How to correctly read Flux and convert it to a single inputStream:
https://*.com/questions/46460599/how-to-correctly-read-fluxdatabuffer-and-convert-it-to-a-single-inputstream
Only one connection receive subscriber allowed解决思路: https://blog.csdn.net/weixin_40899682/article/details/82784242
推荐阅读
-
详解spring cloud构建微服务架构的网关(API GateWay)
-
Spring Cloud Gateway入门解读
-
阿里Sentinel支持Spring Cloud Gateway的实现
-
Spring Cloud GateWay 路由转发规则介绍详解
-
Spring Cloud Gateway网关XSS过滤Filter
-
详解spring cloud构建微服务架构的网关(API GateWay)
-
Spring Cloud Gateway入门解读
-
Spring Cloud Gateway 服务网关快速实现解析
-
Spring Cloud Gateway 之请求坑位[微服务IP不同请求会失败]
-
Spring Cloud Gateway网关XSS过滤Filter