Skip to content

Commit 95f6e4c

Browse files
committed
Support StreamingResponseBody return value type
Issue: SPR-12831
1 parent 76cf5be commit 95f6e4c

File tree

8 files changed

+386
-9
lines changed

8 files changed

+386
-9
lines changed

spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 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.
@@ -219,6 +219,9 @@
219219
* <li>An {@link org.springframework.web.servlet.mvc.method.annotation.SseEmitter}
220220
* can be used to write Server-Sent Events to the response asynchronously;
221221
* also supported as the body within {@code ResponseEntity}.</li>
222+
* <li>A {@link org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody}
223+
* can be used to write to the response asynchronously;
224+
* also supported as the body within {@code ResponseEntity}.</li>
222225
* <li>{@code void} if the method handles the response itself (by
223226
* writing the response content directly, declaring an argument of type
224227
* {@link javax.servlet.ServletResponse} / {@link javax.servlet.http.HttpServletResponse}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,7 @@ private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
632632
handlers.add(new ModelMethodProcessor());
633633
handlers.add(new ViewMethodReturnValueHandler());
634634
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters()));
635+
handlers.add(new StreamingResponseBodyReturnValueHandler());
635636
handlers.add(new HttpEntityMethodProcessor(
636637
getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice));
637638
handlers.add(new HttpHeadersReturnValueHandler());

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 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.
@@ -265,7 +265,8 @@ public Class<?> getParameterType() {
265265
return this.returnValue.getClass();
266266
}
267267
Class<?> parameterType = super.getParameterType();
268-
if (ResponseBodyEmitter.class.isAssignableFrom(parameterType)) {
268+
if (ResponseBodyEmitter.class.isAssignableFrom(parameterType) ||
269+
StreamingResponseBody.class.isAssignableFrom(parameterType)) {
269270
return parameterType;
270271
}
271272
Assert.isTrue(!ResolvableType.NONE.equals(this.returnType), "Expected one of" +
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2002-2015 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+
package org.springframework.web.servlet.mvc.method.annotation;
17+
18+
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
22+
/**
23+
* A controller method return value type for asynchronous request processing
24+
* where the application can write directly to the response {@code OutputStream}
25+
* without holding up the Servlet container thread.
26+
*
27+
* <p><strong>Note:</strong> when using this option it is highly recommended to
28+
* configure explicitly the TaskExecutor used in Spring MVC for executing
29+
* asynchronous requests. Both the MVC Java config and the MVC namespaces provide
30+
* options to configure asynchronous handling. If not using those, an application
31+
* can set the {@code taskExecutor} property of
32+
* {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
33+
* RequestMappingHandlerAdapter}.
34+
*
35+
* @author Rossen Stoyanchev
36+
* @since 4.2
37+
*/
38+
public interface StreamingResponseBody {
39+
40+
/**
41+
* A callback for writing to the response body.
42+
* @param outputStream the stream for the response body
43+
* @throws IOException an exception while writing
44+
*/
45+
void writeTo(OutputStream outputStream) throws IOException;
46+
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2002-2015 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+
package org.springframework.web.servlet.mvc.method.annotation;
17+
18+
import java.io.OutputStream;
19+
import java.util.concurrent.Callable;
20+
21+
import javax.servlet.http.HttpServletResponse;
22+
23+
import org.apache.commons.logging.Log;
24+
import org.apache.commons.logging.LogFactory;
25+
26+
import org.springframework.core.MethodParameter;
27+
import org.springframework.core.ResolvableType;
28+
import org.springframework.http.ResponseEntity;
29+
import org.springframework.http.server.ServerHttpResponse;
30+
import org.springframework.http.server.ServletServerHttpResponse;
31+
import org.springframework.util.Assert;
32+
import org.springframework.web.context.request.NativeWebRequest;
33+
import org.springframework.web.context.request.async.WebAsyncUtils;
34+
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
35+
import org.springframework.web.method.support.ModelAndViewContainer;
36+
37+
38+
/**
39+
* Supports return values of type
40+
* {@link org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody}
41+
* and also {@code ResponseEntity<StreamingResponseBody>}.
42+
*
43+
* @author Rossen Stoyanchev
44+
* @since 4.2
45+
*/
46+
public class StreamingResponseBodyReturnValueHandler implements HandlerMethodReturnValueHandler {
47+
48+
private static final Log logger = LogFactory.getLog(StreamingResponseBodyReturnValueHandler.class);
49+
50+
51+
@Override
52+
public boolean supportsReturnType(MethodParameter returnType) {
53+
if (StreamingResponseBody.class.isAssignableFrom(returnType.getParameterType())) {
54+
return true;
55+
}
56+
else if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
57+
Class<?> bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve();
58+
return (bodyType != null && StreamingResponseBody.class.isAssignableFrom(bodyType));
59+
}
60+
return false;
61+
}
62+
63+
@Override
64+
public void handleReturnValue(Object returnValue, MethodParameter returnType,
65+
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
66+
67+
if (returnValue == null) {
68+
mavContainer.setRequestHandled(true);
69+
return;
70+
}
71+
72+
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
73+
ServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
74+
75+
if (ResponseEntity.class.isAssignableFrom(returnValue.getClass())) {
76+
ResponseEntity<?> responseEntity = (ResponseEntity<?>) returnValue;
77+
outputMessage.setStatusCode(responseEntity.getStatusCode());
78+
outputMessage.getHeaders().putAll(responseEntity.getHeaders());
79+
80+
returnValue = responseEntity.getBody();
81+
if (returnValue == null) {
82+
mavContainer.setRequestHandled(true);
83+
return;
84+
}
85+
}
86+
87+
Assert.isInstanceOf(StreamingResponseBody.class, returnValue);
88+
StreamingResponseBody streamingBody = (StreamingResponseBody) returnValue;
89+
90+
Callable<Void> callable = new StreamingResponseBodyTask(outputMessage.getBody(), streamingBody);
91+
WebAsyncUtils.getAsyncManager(webRequest).startCallableProcessing(callable, mavContainer);
92+
}
93+
94+
95+
private static class StreamingResponseBodyTask implements Callable<Void> {
96+
97+
private final OutputStream outputStream;
98+
99+
private final StreamingResponseBody streamingBody;
100+
101+
102+
public StreamingResponseBodyTask(OutputStream outputStream, StreamingResponseBody streamingBody) {
103+
this.outputStream = outputStream;
104+
this.streamingBody = streamingBody;
105+
}
106+
107+
@Override
108+
public Void call() throws Exception {
109+
this.streamingBody.writeTo(this.outputStream);
110+
return null;
111+
}
112+
}
113+
114+
}

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2015 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.
@@ -16,10 +16,14 @@
1616

1717
package org.springframework.web.servlet.mvc.method.annotation;
1818

19+
import static org.junit.Assert.*;
20+
import static org.mockito.Mockito.*;
21+
1922
import java.lang.reflect.Method;
2023
import java.util.ArrayList;
2124
import java.util.Arrays;
2225
import java.util.List;
26+
2327
import javax.servlet.http.HttpServletResponse;
2428

2529
import org.junit.Before;
@@ -46,8 +50,6 @@
4650
import org.springframework.web.method.support.ModelAndViewContainer;
4751
import org.springframework.web.servlet.view.RedirectView;
4852

49-
import static org.junit.Assert.*;
50-
import static org.mockito.Mockito.*;
5153

5254
/**
5355
* Test fixture with {@link ServletInvocableHandlerMethod}.
@@ -220,6 +222,30 @@ public void wrapConcurrentResult_ResponseEntityNullReturnValue() throws Exceptio
220222
assertEquals("", this.response.getContentAsString());
221223
}
222224

225+
@Test
226+
public void wrapConcurrentResult_ResponseBodyEmitter() throws Exception {
227+
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
228+
converters.add(new StringHttpMessageConverter());
229+
this.returnValueHandlers.addHandler(new ResponseBodyEmitterReturnValueHandler(converters));
230+
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new AsyncHandler(), "handleWithEmitter");
231+
handlerMethod = handlerMethod.wrapConcurrentResult(null);
232+
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
233+
234+
assertEquals(200, this.response.getStatus());
235+
assertEquals("", this.response.getContentAsString());
236+
}
237+
238+
@Test
239+
public void wrapConcurrentResult_StreamingResponseBody() throws Exception {
240+
this.returnValueHandlers.addHandler(new StreamingResponseBodyReturnValueHandler());
241+
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new AsyncHandler(), "handleWithStreaming");
242+
handlerMethod = handlerMethod.wrapConcurrentResult(null);
243+
handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer);
244+
245+
assertEquals(200, this.response.getStatus());
246+
assertEquals("", this.response.getContentAsString());
247+
}
248+
223249
// SPR-12287 (16/Oct/14 comments)
224250

225251
@Test
@@ -273,6 +299,7 @@ public Object dynamicReturnValue(@RequestParam(required=false) String param) {
273299
}
274300
}
275301

302+
@SuppressWarnings("unused")
276303
private static class MethodLevelResponseBodyHandler {
277304

278305
@ResponseBody
@@ -281,28 +308,28 @@ public DeferredResult<String> handle() {
281308
}
282309
}
283310

311+
@SuppressWarnings("unused")
284312
@ResponseBody
285313
private static class TypeLevelResponseBodyHandler {
286314

287-
@SuppressWarnings("unused")
288315
public DeferredResult<String> handle() {
289316
return new DeferredResult<String>();
290317
}
291318
}
292319

320+
@SuppressWarnings("unused")
293321
private static class ResponseEntityHandler {
294322

295-
@SuppressWarnings("unused")
296323
public DeferredResult<ResponseEntity<String>> handleDeferred() {
297324
return new DeferredResult<>();
298325
}
299326

300-
@SuppressWarnings("unused")
301327
public ResponseEntity handleRawType() {
302328
return ResponseEntity.ok().build();
303329
}
304330
}
305331

332+
@SuppressWarnings("unused")
306333
private static class ExceptionRaisingReturnValueHandler implements HandlerMethodReturnValueHandler {
307334

308335
@Override
@@ -317,4 +344,16 @@ public void handleReturnValue(Object returnValue, MethodParameter returnType,
317344
}
318345
}
319346

347+
@SuppressWarnings("unused")
348+
private static class AsyncHandler {
349+
350+
public ResponseBodyEmitter handleWithEmitter() {
351+
return null;
352+
}
353+
354+
public StreamingResponseBody handleWithStreaming() {
355+
return null;
356+
}
357+
}
358+
320359
}

0 commit comments

Comments
 (0)