Skip to content

Commit 14ab2c8

Browse files
Arjen Poutsmarstoyanchev
Arjen Poutsma
authored andcommitted
Request streaming for Apache HttpClient
- Added org.springframework.http.StreamingHttpOutputMessage, which allows for a settable request body (as opposed to an output stream). - Added http.client.HttpComponentsStreamingClientHttpRequest, which implements the above mentioned interface, mapping setBody() to a setEntity() call on the Apache HttpClient HttpEntityEnclosingRequest. - Added a 'bufferRequestBody' property to the HttpComponentsClientHttpRequestFactory. When this property is set to false (default is true), we return a HttpComponentsStreamingClientHttpRequest instead of a (request buffering) HttpComponentsClientHttpRequest. Issue: SPR-10728
1 parent 6407caa commit 14ab2c8

8 files changed

+381
-24
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http;
18+
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
22+
/**
23+
* Represents a HTTP output message that allows for setting a streaming body.
24+
*
25+
* @author Arjen Poutsma
26+
* @since 4.0
27+
*/
28+
public interface StreamingHttpOutputMessage extends HttpOutputMessage {
29+
30+
/**
31+
* Sets the streaming body for this message.
32+
*
33+
* @param body the streaming body
34+
*/
35+
void setBody(Body body);
36+
37+
/**
38+
* Defines the contract for bodies that can be written directly to a
39+
* {@link OuputStream}. It is useful with HTTP client libraries that provide indirect
40+
* access to an {@link OutputStream} via a callback mechanism.
41+
*/
42+
public interface Body {
43+
44+
/**
45+
* Writes this body to the given {@link OuputStream}.
46+
*
47+
* @param outputStream the output stream to write to
48+
* @throws IOException in case of errors
49+
*/
50+
void writeTo(OutputStream outputStream) throws IOException;
51+
52+
}
53+
54+
}

spring-web/src/main/java/org/springframework/http/client/AbstractClientHttpRequest.java

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2013 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -43,7 +43,7 @@ public final HttpHeaders getHeaders() {
4343

4444
@Override
4545
public final OutputStream getBody() throws IOException {
46-
checkExecuted();
46+
assertNotExecuted();
4747
return getBodyInternal(this.headers);
4848
}
4949

@@ -55,13 +55,18 @@ public Cookies getCookies() {
5555

5656
@Override
5757
public final ClientHttpResponse execute() throws IOException {
58-
checkExecuted();
58+
assertNotExecuted();
5959
ClientHttpResponse result = executeInternal(this.headers);
6060
this.executed = true;
6161
return result;
6262
}
6363

64-
private void checkExecuted() {
64+
/**
65+
* Asserts that this request has not been {@linkplain #execute() executed} yet.
66+
*
67+
* @throws IllegalStateException if this request has been executed
68+
*/
69+
protected void assertNotExecuted() {
6570
Assert.state(!this.executed, "ClientHttpRequest already executed");
6671
}
6772

spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequest.java

+22-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2013 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -73,22 +73,35 @@ public URI getURI() {
7373

7474
@Override
7575
protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
76+
addHeaders(this.httpRequest, headers);
77+
78+
if (this.httpRequest instanceof HttpEntityEnclosingRequest) {
79+
HttpEntityEnclosingRequest entityEnclosingRequest =
80+
(HttpEntityEnclosingRequest) this.httpRequest;
81+
HttpEntity requestEntity = new ByteArrayEntity(bufferedOutput);
82+
entityEnclosingRequest.setEntity(requestEntity);
83+
}
84+
HttpResponse httpResponse =
85+
this.httpClient.execute(this.httpRequest, this.httpContext);
86+
return new HttpComponentsClientHttpResponse(httpResponse);
87+
}
88+
89+
/**
90+
* Adds the given headers to the given HTTP request.
91+
*
92+
* @param httpRequest the request to add the headers to
93+
* @param headers the headers to add
94+
*/
95+
static void addHeaders(HttpUriRequest httpRequest, HttpHeaders headers) {
7696
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
7797
String headerName = entry.getKey();
7898
if (!headerName.equalsIgnoreCase(HTTP.CONTENT_LEN) &&
7999
!headerName.equalsIgnoreCase(HTTP.TRANSFER_ENCODING)) {
80100
for (String headerValue : entry.getValue()) {
81-
this.httpRequest.addHeader(headerName, headerValue);
101+
httpRequest.addHeader(headerName, headerValue);
82102
}
83103
}
84104
}
85-
if (this.httpRequest instanceof HttpEntityEnclosingRequest) {
86-
HttpEntityEnclosingRequest entityEnclosingRequest = (HttpEntityEnclosingRequest) this.httpRequest;
87-
HttpEntity requestEntity = new ByteArrayEntity(bufferedOutput);
88-
entityEnclosingRequest.setEntity(requestEntity);
89-
}
90-
HttpResponse httpResponse = this.httpClient.execute(this.httpRequest, this.httpContext);
91-
return new HttpComponentsClientHttpResponse(httpResponse);
92105
}
93106

94107
}

spring-web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2013 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -64,6 +64,7 @@ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequest
6464

6565
private HttpClient httpClient;
6666

67+
private boolean bufferRequestBody = true;
6768

6869
/**
6970
* Create a new instance of the HttpComponentsClientHttpRequestFactory with a default
@@ -128,11 +129,28 @@ public void setReadTimeout(int timeout) {
128129
getHttpClient().getParams().setIntParameter(CoreConnectionPNames.SO_TIMEOUT, timeout);
129130
}
130131

132+
/**
133+
* Indicates whether this request factory should buffer the request body internally.
134+
*
135+
* <p>Default is {@code true}. When sending large amounts of data via POST or PUT, it is
136+
* recommended to change this property to {@code false}, so as not to run out of memory.
137+
*/
138+
public void setBufferRequestBody(boolean bufferRequestBody) {
139+
this.bufferRequestBody = bufferRequestBody;
140+
}
141+
131142
@Override
132143
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
133144
HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
134145
postProcessHttpRequest(httpRequest);
135-
return new HttpComponentsClientHttpRequest(getHttpClient(), httpRequest, createHttpContext(httpMethod, uri));
146+
if (bufferRequestBody) {
147+
return new HttpComponentsClientHttpRequest(getHttpClient(), httpRequest,
148+
createHttpContext(httpMethod, uri));
149+
}
150+
else {
151+
return new HttpComponentsStreamingClientHttpRequest(getHttpClient(),
152+
httpRequest, createHttpContext(httpMethod, uri));
153+
}
136154
}
137155

138156
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.client;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.io.OutputStream;
22+
import java.net.URI;
23+
24+
import org.apache.http.Header;
25+
import org.apache.http.HttpEntity;
26+
import org.apache.http.HttpEntityEnclosingRequest;
27+
import org.apache.http.HttpResponse;
28+
import org.apache.http.client.HttpClient;
29+
import org.apache.http.client.methods.HttpUriRequest;
30+
import org.apache.http.message.BasicHeader;
31+
import org.apache.http.protocol.HttpContext;
32+
33+
import org.springframework.http.HttpHeaders;
34+
import org.springframework.http.HttpMethod;
35+
import org.springframework.http.MediaType;
36+
import org.springframework.http.StreamingHttpOutputMessage;
37+
38+
/**
39+
* {@link ClientHttpRequest} implementation that uses Apache HttpComponents HttpClient to
40+
* execute requests.
41+
*
42+
* <p>Created via the {@link org.springframework.http.client.HttpComponentsClientHttpRequestFactory}.
43+
*
44+
* @author Arjen Poutsma
45+
* @see org.springframework.http.client.HttpComponentsClientHttpRequestFactory#createRequest(java.net.URI,
46+
* org.springframework.http.HttpMethod)
47+
* @since 4.0
48+
*/
49+
final class HttpComponentsStreamingClientHttpRequest extends AbstractClientHttpRequest
50+
implements StreamingHttpOutputMessage {
51+
52+
private final HttpClient httpClient;
53+
54+
private final HttpUriRequest httpRequest;
55+
56+
private final HttpContext httpContext;
57+
58+
private Body body;
59+
60+
public HttpComponentsStreamingClientHttpRequest(HttpClient httpClient,
61+
HttpUriRequest httpRequest, HttpContext httpContext) {
62+
this.httpClient = httpClient;
63+
this.httpRequest = httpRequest;
64+
this.httpContext = httpContext;
65+
}
66+
67+
@Override
68+
public HttpMethod getMethod() {
69+
return HttpMethod.valueOf(this.httpRequest.getMethod());
70+
}
71+
72+
@Override
73+
public URI getURI() {
74+
return this.httpRequest.getURI();
75+
}
76+
77+
@Override
78+
public void setBody(Body body) {
79+
assertNotExecuted();
80+
this.body = body;
81+
}
82+
83+
@Override
84+
protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException {
85+
throw new UnsupportedOperationException(
86+
"getBody not supported when bufferRequestBody is false");
87+
}
88+
89+
@Override
90+
protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException {
91+
HttpComponentsClientHttpRequest.addHeaders(this.httpRequest, headers);
92+
93+
if (this.httpRequest instanceof HttpEntityEnclosingRequest && body != null) {
94+
HttpEntityEnclosingRequest entityEnclosingRequest =
95+
(HttpEntityEnclosingRequest) this.httpRequest;
96+
97+
HttpEntity requestEntity = new StreamingHttpEntity(getHeaders(), body);
98+
entityEnclosingRequest.setEntity(requestEntity);
99+
}
100+
HttpResponse httpResponse =
101+
this.httpClient.execute(this.httpRequest, this.httpContext);
102+
return new HttpComponentsClientHttpResponse(httpResponse);
103+
}
104+
105+
private static class StreamingHttpEntity implements HttpEntity {
106+
107+
private final HttpHeaders headers;
108+
109+
private final StreamingHttpOutputMessage.Body body;
110+
111+
private StreamingHttpEntity(HttpHeaders headers,
112+
StreamingHttpOutputMessage.Body body) {
113+
this.headers = headers;
114+
this.body = body;
115+
}
116+
117+
@Override
118+
public boolean isRepeatable() {
119+
return false;
120+
}
121+
122+
@Override
123+
public boolean isChunked() {
124+
return false;
125+
}
126+
127+
@Override
128+
public long getContentLength() {
129+
return headers.getContentLength();
130+
}
131+
132+
@Override
133+
public Header getContentType() {
134+
MediaType contentType = headers.getContentType();
135+
return contentType != null ?
136+
new BasicHeader("Content-Type", contentType.toString()) : null;
137+
}
138+
139+
@Override
140+
public Header getContentEncoding() {
141+
String contentEncoding = headers.getFirst("Content-Encoding");
142+
return contentEncoding != null ?
143+
new BasicHeader("Content-Encoding", contentEncoding) : null;
144+
145+
}
146+
147+
@Override
148+
public InputStream getContent() throws IOException, IllegalStateException {
149+
throw new IllegalStateException();
150+
}
151+
152+
@Override
153+
public void writeTo(OutputStream outputStream) throws IOException {
154+
body.writeTo(outputStream);
155+
}
156+
157+
@Override
158+
public boolean isStreaming() {
159+
return true;
160+
}
161+
162+
@Override
163+
public void consumeContent() throws IOException {
164+
throw new UnsupportedOperationException();
165+
}
166+
}
167+
168+
}

0 commit comments

Comments
 (0)