Spring Cloud Gateway 设置全局接口访问日志
虽然网关只做转发,但是对于每个转发的请求,我们都希望能够在日志中打印出请求的信息,网上版本很多,踩了很多坑,目前没找到完美的解决方案,最后我这个应该是大成版。希望对大家有用。
先贴代码,再说遇到什么坑吧。
/**
* @author chenzhangx
* @date 2021/11/30 15:09
*/
@Component
public class AccessFilter extends AbstractFilter implements GlobalFilter, Ordered {
private static Logger logger = LoggerFactory.getLogger(AccessFilter.class.getSimpleName());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return cacheRequestBody(exchange, (serverHttpRequest) -> {
// don't mutate and build if same request object
if (serverHttpRequest == exchange.getRequest()) {
return chain.filter(exchange);
}
return chain.filter(exchange.mutate().request(serverHttpRequest).build());
});
}
@Override
public int getOrder() {
return -4;
}
//---------------------------------------------- private ---------------------------------------------------------
private void logInfo(ServerHttpRequest request, String body) {
String uri = request.getPath().value();
String params = request.getQueryParams().toString();
String method = request.getMethodValue();
String ip = request.getRemoteAddress().toString();
String headers = request.getHeaders().entrySet()
.stream()
.map(entry -> entry.getKey() + ": [" + String.join(";", entry.getValue()) + "]")
.collect(Collectors.joining("\n"));
long accessDate = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
sb.append("\n==================================[API_CALL]==================================\n");
sb.append("uri : " + uri + "\n");
sb.append("method : " + method + "\n");
sb.append("ip : " + ip + "\n");
sb.append("params : " + params + "\n");
sb.append("body : " + body + "\n");
sb.append("accessDate : " + accessDate + "\n");
sb.append("headers : { \n" + headers + " }\n");
sb.append("==============================================================================\n");
logger.info(String.valueOf(sb));
}
private Mono<Void> cacheRequestBody(ServerWebExchange exchange, Function<ServerHttpRequest, Mono<Void>> function) {
ServerHttpResponse response = exchange.getResponse();
ServerHttpRequest request = exchange.getRequest();
NettyDataBufferFactory factory = (NettyDataBufferFactory) response.bufferFactory();
// Join all the DataBuffers so we have a single DataBuffer for the body
return DataBufferUtils.join(exchange.getRequest().getBody())
.defaultIfEmpty(factory.wrap(new EmptyByteBuf(factory.getByteBufAllocator())))
.map((dataBuffer) -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
String bodyString = new String(bytes, StandardCharsets.UTF_8);
logInfo(request, bodyString);
// 这里下面的代码我原先没写,后续的转发直接失效,因为body数据被拿出来了
Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
DataBuffer buffer = exchange.getResponse().bufferFactory()
.wrap(bytes);
return Mono.just(buffer);
});
return (ServerHttpRequest) new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
}).switchIfEmpty(Mono.just(exchange.getRequest())).flatMap(function);
}
}
思路就是定义一个全局Filter,因为是接入日志的打印,所以order小点。
代码就这些,复制即用,说下遇到的坑吧
- 1、日志打印完后,body消失转发失败 这个是因为流传输中,你把buffer里面的数据拿出来了,但是不再塞进去,他就没了哦,所以需要再自己包装一个request。
- 2、如果是post请求,但是没有请求体,就会出现过滤器不通过的情况(如果不知道我说啥忽略 这个是真的坑,看网上的方法是判断是否是post或者header中的content length来判断是否有请求体,但是post可以没有请求体,contentLength也可以不传)
- 3、如果将获取body的过程提取为方法,可能是webflux异步的原因(我猜的,我太菜了,如果知道的大佬评论说一下),会先return,然后body信息就拿不到为null,所以网上的方法都是将提取body的代码写在return中的。
希望有帮到大家!