如何对由org.springframework.web.client.RestTemplate
创建的HTTP请求进行gzip压缩?
我使用的是Spring 4.2.6
和Spring Boot 1.3.5
(仅限Java SE,不是Android或Web浏览器中的Javascript)。
我正在创建一些非常大的POST
请求,并且希望请求正文被压缩。
如何对由org.springframework.web.client.RestTemplate
创建的HTTP请求进行gzip压缩?
我使用的是Spring 4.2.6
和Spring Boot 1.3.5
(仅限Java SE,不是Android或Web浏览器中的Javascript)。
我正在创建一些非常大的POST
请求,并且希望请求正文被压缩。
我提出了两种解决方案,一种更简单不需要流式传输,另一种支持流式传输。
如果您不需要流式传输,请使用自定义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问题。 我认为您需要为此编写一些代码。
execute
方法中才能传递RequestCallback,对于其他方法,RestTemplate会在内部创建自己的RequestCallback。顺便说一下,我不是那个给你点踩的人;-) - Testo TestinigetGzip
方法,因为两个OutputStream都是Closeable
。我无法使其在此注释块中看起来可读,因此将其作为下面的附加“答案”提交。 - roj除了@TestoTestini的答案之外,如果我们利用Java 7+的“try-with-resources”语法(因为ByteArrayOutputStream
和GZIPOutputStream
都实现了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原始答案进行评论的方法,因此提供这个答案。
由于我无法评论@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;
}
}
我遇到了一个错误(“压缩文件在到达流结束标记之前就结束了”),上述方法解决了我的问题,我觉得应该分享一下。