关于Netty HTTP Request Body 只能 subscribe 一次问题的解决方法
Reactor HTTP Request Body 被 subscribe 一次后,下次 subscribe 的结果为空。这是因为 Reactor 将 HTTP Request Body 的发布源头 为FluxRecive ,这是一个动态发布者,发布内容为 HTTP Request 的消息体。因此,当 HTTP Request 的 消息体 被 subscribe 一次后,后续所有的 subscribe 都是 空。因为 HTTP Request 的消息体只发送一次。
如果想多次获取 HTTP Request Body,就需要在第一次 subscribe 时,将 Request Body 缓存起来,这样后续 subscribe 时直接从缓存获取 body 信息即可。
1. 使用自定义的装饰类实现表体缓存
该方法会复制表体内容,这会在一定程度上影响性能。
1.1 创建 ServerHttpRequest 装饰类
该类的主要作用是重写 getBody() 方法,增加缓存功能。
public class BodyCacheServerHttpRequestDecorator extends ServerHttpRequestDecorator {
private Logger LOGGER = LoggerFactory.getLogger(BodyCacheServerHttpRequestDecorator.class);
private boolean firstSubscribe = true;
private List<byte[]> listByteArray = new ArrayList<>();
public BodyCacheServerHttpRequestDecorator(ServerHttpRequest delegate) {
super(delegate);
}
@Override
public Flux<DataBuffer> getBody() {
if (firstSubscribe) {
// return super.getBody().map(dataBuffer -> ((NettyDataBuffer) dataBuffer)
// .getNativeBuffer()).map(byteBuf -> {
// firstSubscribe = false;
//
// byte[] dst = new byte[byteBuf.readableBytes()];
// byteBuf.readBytes(dst);
// listByteArray.add(dst);
//
// byteBuf.release();
//
// return getDataBuffer(dst);
// });
return super.getBody().map(dataBuffer -> {
firstSubscribe = false;
ByteBuf copy = ((NettyDataBuffer) dataBuffer).getNativeBuffer().copy();
byte[] dst = new byte[copy.readableBytes()];
copy.readBytes(dst);
listByteArray.add(dst);
copy.release();
return dataBuffer;
});
} else {
return Flux.fromIterable(listByteArray).map(bytes -> getDataBuffer(bytes));
}
}
private DataBuffer getDataBuffer(byte[] bytes) {
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
return nettyDataBufferFactory.wrap(bytes);
}
}
1.2 创建 ServerWebExchange 装饰类
该类的主要作用是 重写 getRequest() 方法,返回我们包装过的 BodyCacheServerHttpRequestDecorator 对象。
public class BodyCacheServerWebExchange extends ServerWebExchangeDecorator {
private final ServerHttpRequest serverHttpRequest;
public BodyCacheServerWebExchange(ServerWebExchange delegate) {
super(delegate);
serverHttpRequest = new BodyCacheServerHttpRequestDecorator(delegate.getRequest());
}
@Override
public ServerHttpRequest getRequest() {
return serverHttpRequest;
}
}
1.3 添加 web-flux WebFilter
该 WebFilter 设置较高的优先级,将 FilterChain 的请求替换为 我们创建的 BodyCacheServerWebExchange 对象。
@Component
public class BodyCacheWebFilter implements WebFilter, Ordered {
private final static Logger LOGGER = LoggerFactory.getLogger(BodyCacheWebFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
BodyCacheServerWebExchange exchangeDecorator = new BodyCacheServerWebExchange(exchange);
return chain.filter(exchangeDecorator);
}
@Override
public int getOrder() {
return -1;
}
}
2. Spring Cloud Gateway 缓存表体的实现
@Component
public class BodyCacheWebFilter implements WebFilter, Ordered {
private final static Logger LOGGER = LoggerFactory.getLogger(BodyCacheWebFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
LOGGER.info("Try to cache request body if present!");
return ServerWebExchangeUtils
.cacheRequestBody(exchange,
(serverHttpRequest) -> chain.filter(
exchange.mutate().request(serverHttpRequest).build()))
.switchIfEmpty(chain.filter(exchange));
}
@Override
public int getOrder() {
return -1;
}
}
org.springframework.cloud.gateway.support.ServerWebExchangeUtils#cacheRequestBody()
private static <T> Mono<T> cacheRequestBody(ServerWebExchange exchange,
boolean cacheDecoratedRequest,
Function<ServerHttpRequest, Mono<T>> function) {
// Join all the DataBuffers so we have a single DataBuffer for the body
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
if (dataBuffer.readableByteCount() > 0) {
if (log.isTraceEnabled()) {
log.trace("retaining body in exchange attribute");
}
exchange.getAttributes().put(CACHED_REQUEST_BODY_ATTR,
dataBuffer);
}
ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return Mono.<DataBuffer>fromSupplier(() -> {
if (exchange.getAttributeOrDefault(
CACHED_REQUEST_BODY_ATTR, null) == null) {
// probably == downstream closed
return null;
}
// TODO: deal with Netty
NettyDataBuffer pdb = (NettyDataBuffer) dataBuffer;
return pdb.factory()
.wrap(pdb.getNativeBuffer().retainedSlice());
}).flux();
}
};
if (cacheDecoratedRequest) {
exchange.getAttributes().put(
CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR, decorator);
}
return function.apply(decorator);
});
}
3. 应用
我们的应用中有两处消费了表体内容,经测试都能正常访问并返回表体内容。
3.1 日志输出表体内容
exchange.getRequest().getBody().collectList().doOnDiscard(NettyDataBuffer.class, NettyDataBuffer::release).
subscribe(
list -> {
if (CollectionUtils.isEmpty(list)) {
statitic.put("LOGGING_REQUEST_BODY", "");
return;
}
StringBuilder sb = new StringBuilder();
list.forEach(dataBuffer -> {
try {
sb.append(IOUtils.toString(dataBuffer.asInputStream()));
} catch (IOException e) {
LOGGER.error("Write input stream to String exception.", e);
} finally {
((NettyDataBuffer) dataBuffer).release();
}
});
statitic.put("LOGGING_REQUEST_BODY", sb.toString());
}
);
改进版,使用工具类org.springframework.core.io.buffer.DataBufferUtils#join
DataBufferUtils.join(exchange.getRequest().getBody()).
subscribe(
dataBuffer -> {
try {
String body = IOUtils.toString(dataBuffer.asInputStream());
statitic.put("LOGGING_REQUEST_BODY", body);
} catch (IOException e) {
LOGGER.error("Write input stream to String exception.", e);
} finally {
((NettyDataBuffer) dataBuffer).release();
}
}
);
3.2 SpringCloudGateway转发表体内容
org.springframework.cloud.gateway.filter.NettyRoutingFilter#filter
return nettyOutbound.options(NettyPipeline.SendOptions::flushOnEach)
.send(request.getBody()
.map(dataBuffer -> ((NettyDataBuffer) dataBuffer)
.getNativeBuffer()));
扩展
Request Body的动态发布者 FluxReceive
reactor.netty.channel.FluxReceive#request
@Override
public void request(long n) {
if (Operators.validate(n)) {
if (eventLoop.inEventLoop()) {
this.receiverDemand = Operators.addCap(receiverDemand, n);
drainReceiver();
}
else {
eventLoop.execute(() -> {
this.receiverDemand = Operators.addCap(receiverDemand, n);
drainReceiver();
});
}
}
}