我需要在没有中间存储的情况下读取和写入压缩(GZIP)流。目前,我正在使用Spring scala> def aLotOfZeroes: Int => String = "0" * _
aLotOfZeroes: Int => String
scala> aLotOfZeroes(10)
res16: String = 0000000000
进行编写,使用Apache HTTP客户端进行阅读(请参阅我的回答here,了解为什么RestTemplate
不能用于读大流)。实施非常简单,我在回复RestTemplate
上拍了GZIPInputStream
并继续前进。
现在,我想切换到使用Spring 5 WebClient(因为我不是现状的粉丝)。但是,InputStream
在性质上具有反应性并处理WebClient
;我相信有可能获得Flux<Stuff>
,其中DataBuffer是Flux<DataBuffer>
的抽象。问题是,如何在不必将完整流存储在内存中(ByteBuffer
,我正在看着你)或写入本地磁盘的情况下即时解压缩它?值得一提的是OutOfMemoryError
使用了Netty。
我承认不太了解(de)压缩,但是,我做了我的研究,但网上提供的资料似乎都没什么特别有帮助。
compression on java nio direct buffers
Reading a GZIP file from a FileChannel (Java NIO)
答案 0 :(得分:2)
public class HttpResponseHeadersHandler extends ChannelInboundHandlerAdapter {
private final HttpHeaders httpHeaders;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof HttpResponse &&
!HttpStatus.resolve(((HttpResponse) msg).status().code()).is1xxInformational()) {
HttpHeaders headers = ((HttpResponse) msg).headers();
httpHeaders.forEach(e -> {
log.warn("Modifying {} from: {} to: {}.", e.getKey(), headers.get(e.getKey()), e.getValue());
headers.set(e.getKey(), e.getValue());
});
}
ctx.fireChannelRead(msg);
}
}
然后我创建一个ClientHttpConnector
以与WebClient
一起使用,并在afterNettyContextInit
中添加处理程序:
ctx.addHandlerLast(new ReadTimeoutHandler(readTimeoutMillis, TimeUnit.MILLISECONDS));
ctx.addHandlerLast(new Slf4JLoggingHandler());
if (forceDecompression) {
io.netty.handler.codec.http.HttpHeaders httpHeaders = new ReadOnlyHttpHeaders(
true,
CONTENT_ENCODING, GZIP,
CONTENT_TYPE, APPLICATION_JSON
);
HttpResponseHeadersHandler headersModifier = new HttpResponseHeadersHandler(httpHeaders);
ctx.addHandlerFirst(headersModifier);
}
ctx.addHandlerLast(new HttpContentDecompressor());
当然,对于非GZIP压缩的响应,这会失败,因此我仅将WebClient
的这个实例用于特定用例,我确定该响应已被压缩。
写作很简单:Spring有一个ResourceEncoder
,因此InputStream
可以简单地转换为InputStreamResource
,瞧!
答案 1 :(得分:0)
在这里注意到这一点,让我有些困惑-从5.1开始,API进行了一些更改。
我对ChannelInboundHandler
的接受设置与我类似:
public class GzipJsonHeadersHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof HttpResponse
&& !HttpStatus.resolve(((HttpResponse) msg).status().code()).is1xxInformational()) {
HttpHeaders headers = ((HttpResponse) msg).headers();
headers.clear();
headers.set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
headers.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON);
}
ctx.fireChannelRead(msg);
}
}
(为简单起见,我所需的标头值只是在此处进行硬编码,否则完全相同。)
要注册它却有所不同:
WebClient.builder()
.clientConnector(
new ReactorClientHttpConnector(
HttpClient.from(
TcpClient.create()
.doOnConnected(c -> {
c.addHandlerFirst(new HttpContentDecompressor());
c.addHandlerFirst(new HttpResponseHeadersHandler());
})
).compress(true)
)
)
.build();
Netty现在似乎维护了一个与系统列表分开(以及之后)的处理程序的用户列表,而addHandlerFirst()
仅将您的处理程序放在用户列表的前面。因此,它需要显式调用HttpContentDecompressor
,以确保在您的处理程序插入正确的标头之后肯定可以执行它。