如何使用Spring RestTemplate压缩HTTP请求?

19

如何对由org.springframework.web.client.RestTemplate创建的HTTP请求进行gzip压缩

我使用的是Spring 4.2.6和Spring Boot 1.3.5(仅限Java SE,不是Android或Web浏览器中的Javascript)。

我正在创建一些非常大的POST请求,并且希望请求正文被压缩。


1
你可以在这里查看 https://dev59.com/2WEi5IYBdhLWcg3wUKyC 其中有关于如何在Boot和非Boot中启用压缩以及一些性能建议的说明。 - Maxvader
1
@Maxvader,链接中的答案是关于响应压缩的。我想要的是请求压缩。 - Bartosz Bilicki
压缩来自浏览器的HTTP POST数据是关于压缩由Javascript发起的请求。我的请求是由Java发起的。 - Bartosz Bilicki
你是否查看过http://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#how-to-enable-http-response-compression? - iMysak
1
iMysak,喜欢的文档是关于服务器响应压缩的。我想压缩请求正文。 - Bartosz Bilicki
这个有什么最新进展吗? - Ortwin Angermeier
3个回答

17

我提出了两种解决方案,一种更简单不需要流式传输,另一种支持流式传输。

如果您不需要流式传输,请使用自定义ClientHttpRequestInterceptor,这是一个Spring功能。

RestTemplate rt = new RestTemplate();
rt.setInterceptors(Collections.singletonList(interceptor));

其中 interceptor 可以是:

ClientHttpRequestInterceptor interceptor = new ClientHttpRequestInterceptor() {

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {
        request.getHeaders().add("Content-Encoding", "gzip");
        byte[] gzipped = getGzip(body);
        return execution.execute(request, gzipped);
    } 
 }

getGzip复制


    private byte[] getGzip(byte[] body) throws IOException {

        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        try {
            GZIPOutputStream zipStream = new GZIPOutputStream(byteStream);
            try {
                zipStream.write(body);
            } finally {
                zipStream.close();
            }
        } finally {
            byteStream.close();
        }

        byte[] compressedData = byteStream.toByteArray();
        return compressedData;

    }

配置拦截器后,所有请求都将被压缩。

这种方法的缺点是不支持流式传输,因为ClientHttpRequestInterceptor将内容作为byte[]接收。

如果您需要流式传输,请创建一个自定义的ClientHttpRequestFactory,例如GZipClientHttpRequestFactory,并像这样使用:

    SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
    requestFactory.setBufferRequestBody(false);
    ClientHttpRequestFactory gzipRequestFactory = new GZipClientHttpRequestFactory(requestFactory);
    RestTemplate rt = new RestTemplate(gzipRequestFactory);

其中GZipClientHttpRequestFactory是:

public class GZipClientHttpRequestFactory extends AbstractClientHttpRequestFactoryWrapper {

    public GZipClientHttpRequestFactory(ClientHttpRequestFactory requestFactory) {
        super(requestFactory);
    }

    @Override
    protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory)
            throws IOException {
        ClientHttpRequest delegate = requestFactory.createRequest(uri, httpMethod);
        return new ZippedClientHttpRequest(delegate);
    }

}

ZippedClientHttpRequest 是:

public class ZippedClientHttpRequest extends WrapperClientHttpRequest
{
    private GZIPOutputStream zip;

    public ZippedClientHttpRequest(ClientHttpRequest delegate) {
        super(delegate);
        delegate.getHeaders().add("Content-Encoding", "gzip");
        // here or in getBody could add content-length to avoid chunking
        // but is it available ? 
        // delegate.getHeaders().add("Content-Length", "39");

    }

    @Override
    public OutputStream getBody() throws IOException {
        final OutputStream body = super.getBody();
        zip = new GZIPOutputStream(body);
        return zip;
    }

    @Override
    public ClientHttpResponse execute() throws IOException {
        if (zip!=null) zip.close();
        return super.execute();
    }

}

最后WrapperClientHttpRequest是:

public class WrapperClientHttpRequest implements ClientHttpRequest {

    private final ClientHttpRequest delegate;

    protected WrapperClientHttpRequest(ClientHttpRequest delegate) {
        super();
        if (delegate==null)
            throw new IllegalArgumentException("null delegate");
        this.delegate = delegate;
    }

    protected final ClientHttpRequest getDelegate() {
        return delegate;
    }

    @Override
    public OutputStream getBody() throws IOException {
        return delegate.getBody();
    }

    @Override
    public HttpHeaders getHeaders() {
        return delegate.getHeaders();
    }

    @Override
    public URI getURI() {
        return delegate.getURI();
    }

    @Override
    public HttpMethod getMethod() {
        return delegate.getMethod();
    }

    @Override
    public ClientHttpResponse execute() throws IOException {
        return delegate.execute();
    }
}

这种方法使用分块传输编码创建请求,如果已知大小,则可以通过设置内容长度标头来更改。

ClientHttpRequestInterceptor 和/或自定义 ClientHttpRequestFactory 方法的优点是它适用于 RestTemplate 的任何方法。另一种方法是通过传递 RequestCallback 来执行 execute 方法,因为 RestTemplate 的其他方法在内部创建自己的 RequestCallbacks 来生成内容。

顺便说一下,似乎有很少的支持对服务器上的gzip请求进行解压缩。 还相关:在 WebRequest 中发送gzip数据?,指向Zip Bomb问题。 我认为您需要为此编写一些代码


这个拦截器会获取完整的请求体,然后在一步中压缩它。我说得对吗?这对于发送非常大的POST请求来说并不合适,正如OP所请求的那样。 - berserkk
@berserkk 是的,正确的,它不支持流式传输,我已经更新了答案。让我们看看是否足够。这样做的优点是可以与RestTemplate的任何方法一起使用。只有在execute方法中才能传递RequestCallback,对于其他方法,RestTemplate会在内部创建自己的RequestCallback。顺便说一下,我不是那个给你点踩的人;-) - Testo Testini
你的解释已经足够了。请再次更新你的答案,这样我以后可以点赞。谢谢。 - berserkk
1
我已经添加了一个答案来补充上面@TestoTestini的答案,以利用Java 7+的“try-with-resources”语法来处理getGzip方法,因为两个OutputStream都是Closeable。我无法使其在此注释块中看起来可读,因此将其作为下面的附加“答案”提交。 - roj

2

除了@TestoTestini的答案之外,如果我们利用Java 7+的“try-with-resources”语法(因为ByteArrayOutputStreamGZIPOutputStream都实现了closeable()),那么我们可以将getGzip函数缩小到以下内容:

private byte[] getGzip(byte[] body) throws IOException {

    try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
         GZIPOutputStream zipStream = new GZIPOutputStream(byteStream)) {
            zipStream.write(body);
        byte[] compressedData = byteStream.toByteArray();
        return compressedData;
    }

}

我无法找到一种在保留上面代码格式的情况下对@TestoTestini原始答案进行评论的方法,因此提供这个答案。


0

由于我无法评论@roj的帖子,所以我在这里写一个答案。

@roj的片段虽然很整洁,但实际上并没有像@Testo Testini的片段那样完成相同的工作。

Testo在以下代码之前关闭了流: byteStream.toByteArray(); 而在@rog的答案中,这发生在stream.close()之前,因为流在try/resource块中。

如果您需要使用try-with-resources,则应在byteStream.toByteArray();之前关闭zipStream

完整的片段应该是:

private byte[] getGzip(byte[] body) throws IOException {

    try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
         GZIPOutputStream zipStream = new GZIPOutputStream(byteStream)) {
            
        zipStream.write(body);
        zipStream.close();

        byte[] compressedData = byteStream.toByteArray();
        return compressedData;
    }

}

我遇到了一个错误(“压缩文件在到达流结束标记之前就结束了”),上述方法解决了我的问题,我觉得应该分享一下。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接