关于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();
			});
		}
	}
}

参考

关于Spring-webflux编程中body只能获取一次的问题解决方案

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值