У меня есть требование читать и писать сжатые (GZIP) потоки без промежуточного хранения. В настоящее время я использую Spring RestTemplate
для записи и HTTP-клиента Apache для чтения (см. Мой ответ здесь для объяснения того, почему RestTemplate
нельзя использовать для чтения больших потоков). Реализация довольно проста, я ставлю GZIPInputStream
на ответ InputStream
и двигаюсь дальше.
Теперь я хочу перейти на использование Spring 5 WebClient (просто потому, что я не сторонник статус-кво). Однако WebClient
по своей природе является реактивным и имеет дело с Flux<Stuff>
; Я считаю, что можно получить Flux<DataBuffer>
, где DataBuffer - это абстракция над ByteBuffer
. Вопрос в том, как мне распаковать его на лету без необходимости сохранять полный поток в памяти (OutOfMemoryError
, я смотрю на вас) или записи на локальный диск? Стоит отметить, что WebClient
использует Netty под капотом.
- См. Также Reactor Netty issue-251.
- Также относится к интеграции Spring issue-2300.
Признаюсь, я мало что знаю о (де) сжатии, однако я провел свое исследование, но ни один из материалов, доступных в Интернете, не оказался особенно полезным.
сжатие в прямых буферах java nio
Запись файла GZIP с помощью nio
Чтение файла GZIP из FileChannel (Java NIO)
(де) сжатие файлов с помощью NIO
Итерируемое сжатие / надувание gzip в Java
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
, и вуаля!
Заметив это здесь, так как это немного смутило меня - API немного изменился с версии 5.1.
У меня настройка аналогична принятому ответу для 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
, чтобы убедиться, что он определенно выполняется после вашего обработчика, вставляющего правильные заголовки.
Связанные вопросы
Новые вопросы
java
Java - это язык программирования высокого уровня. Используйте этот тег, если у вас возникли проблемы с использованием или пониманием самого языка. Этот тег редко используется отдельно и чаще всего используется вместе с [spring], [spring-boot], [jakarta-ee], [android], [javafx], [hadoop], [gradle] и [maven].