У меня есть требование читать и писать сжатые (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

6
Abhijit Sarkar 1 Янв 2018 в 01:19

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, и вуаля!

3
Abhijit Sarkar 5 Янв 2018 в 04:29

Заметив это здесь, так как это немного смутило меня - 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, чтобы убедиться, что он определенно выполняется после вашего обработчика, вставляющего правильные заголовки.

0
Michael Berry 2 Дек 2019 в 19:02