一、需求
要求对Spring Cloud Gateway(后面统一使用SCG表述)代理的后端服务进行日志记录。记录的内容包括请求信息(请求方式、请求地址、请求头、RequestBody等)、响应信息(响应头、响应码及ResponseBody等);
二、环境及版本
Spring Boot 2.3.5.RELEASE
Spring Cloud Gateway 2.2.7.RELEASE
三、问题描述
因为SCG 是基于 Spring 5.x 版本的 WebFlux 实现的,源码中很多地方也都是异步实现的;且我本人对 WebFlux 并没有太多开发经验。结果在获取请求与响应的 Body 内容时遇到了很多坑....
先说第一个坑:
java.lang.IllegalStateException: Only one connection receive subscriber allowed.
这个问题是由于在 Filter 中提前消费了 RequestBody 导致后面的 Filter 在获取 RequestBody时返回 Null 就会提示这个异常;网上对这个问题的解决文章有很多,基本都是 2018年左右的文章内容,当你尝试按照这些文章去解决问题的时候你会发现这并无卵用;再此处贴出一段代码来为大家讲解究其何因:
位置 1 创建了一个 AtomicReference 的实例对象用来缓存 Body数据(只要知道它是一个容器就可以了).
位置 2 是一个异步流订阅的骚操作,在这个骚操作中将得到的 Body 数据转换为 String 类型 然后释放内存空间;接着在将这个 String 放倒 AtomicReference 对象容器中;
位置 3 将缓存在 AtomicReference 对象容器中的 Body 取出;
执行完上面三个步骤后你会发现拿到的 Body 是空的!!!!
原因是 位置2的操作 是一个异步订阅操;在你当前线程执行的时候 位置2地方的代码可能并没有执行,所以导致你在位置3 获取数据是一个 Null 对象。这段代码应该在 2018 年发布的 SCG 版本中应该是可以获得对象的(盲目猜测当时的SCG 并非是真正的异步执行),但是现在肯定不行了。
四、解决方案
注: 你现在看到的解决方式可能会在以后的SCG 版本中失效,所以你在测试的时候如果发现没有用一定要认真看一下自己当前的SCG 版本是否和我的一致;
经过几天时间的源码阅读发现了两个可以借鉴的类:ModifyRequestBodyGatewayFilterFactory.java
ModifyResponseBodyGatewayFilterFactory.java
通过对上面这两个类的理解我把它整理成为了我需要的 FilterFactory :
打印 RequestBody 代码:
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (isTargetLogMediaType(exchange.getRequest())) {
Class inClass = String.class;
Class outClass = String.class;
String requestId = exchange.getAttribute(AppConfig.GATEWAY_REQUEST_ID_KEY);
ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
Mono<?> modifiedBody = serverRequest.bodyToMono(inClass).flatMap(originalBody -> {
MDC.put("type", "REQUEST-BODY");
log.info(String.format("[%s] %s", requestId, originalBody));
return Mono.just(originalBody);
});
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass);
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(() ->
chain.filter(exchange.mutate().request(decorate(exchange, headers, outputMessage)).build())
)).onErrorResume((Function<Throwable, Mono<Void>>) throwable -> Mono.error(throwable));
}
return chain.filter(exchange);
}
private ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(headers);
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
}
打印 ResponseBody 代码:
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange.mutate().response(decorate(exchange)).build());
}
private ServerHttpResponse decorate(ServerWebExchange exchange) {
return new ServerHttpResponseDecorator(exchange.getResponse()) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { // Mono<NettyDataBuffer>
if (isTargetLogMediaType(getDelegate())) {
Class inClass = String.class;
Class outClass = String.class;
String requestId = exchange.getAttribute(AppConfig.GATEWAY_REQUEST_ID_KEY);
String originalResponseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CONTENT_TYPE, originalResponseContentType);
ClientResponse clientResponse = ClientResponse
.create(exchange.getResponse().getStatusCode())
.headers(headers -> headers.putAll(httpHeaders))
.body(Flux.from(body)).build();
Mono<?> modifiedBody = clientResponse.bodyToMono(inClass).flatMap(originalBody -> {
MDC.put("type", "RESPONSE-BODY");
log.info(String.format("[%s] %s", requestId, originalBody));
return Mono.just(originalBody);
});
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, outClass);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, exchange.getResponse().getHeaders());
return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
Flux<DataBuffer> messageBody = outputMessage.getBody();
HttpHeaders headers = getDelegate().getHeaders();
if (!headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
messageBody = messageBody.doOnNext(data -> headers.setContentLength(data.readableByteCount()));
}
return getDelegate().writeWith(messageBody);
}));
}
return getDelegate().writeWith(body);
}
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWith(Flux.from(body).flatMapSequential(p -> p));
}
};
}