diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index 1817fb9dfb5..ecc27d16a14 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -219,6 +219,9 @@ import java.util.concurrent.Callable; *
  • An {@link org.springframework.web.servlet.mvc.method.annotation.SseEmitter} * can be used to write Server-Sent Events to the response asynchronously; * also supported as the body within {@code ResponseEntity}.
  • + *
  • A {@link org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody} + * can be used to write to the response asynchronously; + * also supported as the body within {@code ResponseEntity}.
  • *
  • {@code void} if the method handles the response itself (by * writing the response content directly, declaring an argument of type * {@link javax.servlet.ServletResponse} / {@link javax.servlet.http.HttpServletResponse} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index b0ab9190833..57687e8f5da 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -632,6 +632,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter handlers.add(new ModelMethodProcessor()); handlers.add(new ViewMethodReturnValueHandler()); handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters())); + handlers.add(new StreamingResponseBodyReturnValueHandler()); handlers.add(new HttpEntityMethodProcessor( getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice)); handlers.add(new HttpHeadersReturnValueHandler()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java index e0555786a76..4dbbd91ccad 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -265,7 +265,8 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { return this.returnValue.getClass(); } Class parameterType = super.getParameterType(); - if (ResponseBodyEmitter.class.isAssignableFrom(parameterType)) { + if (ResponseBodyEmitter.class.isAssignableFrom(parameterType) || + StreamingResponseBody.class.isAssignableFrom(parameterType)) { return parameterType; } Assert.isTrue(!ResolvableType.NONE.equals(this.returnType), "Expected one of" + diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java new file mode 100644 index 00000000000..a3a4240c205 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.mvc.method.annotation; + + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A controller method return value type for asynchronous request processing + * where the application can write directly to the response {@code OutputStream} + * without holding up the Servlet container thread. + * + *

    Note: when using this option it is highly recommended to + * configure explicitly the TaskExecutor used in Spring MVC for executing + * asynchronous requests. Both the MVC Java config and the MVC namespaces provide + * options to configure asynchronous handling. If not using those, an application + * can set the {@code taskExecutor} property of + * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter + * RequestMappingHandlerAdapter}. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +public interface StreamingResponseBody { + + /** + * A callback for writing to the response body. + * @param outputStream the stream for the response body + * @throws IOException an exception while writing + */ + void writeTo(OutputStream outputStream) throws IOException; + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java new file mode 100644 index 00000000000..3e1a09148f3 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.mvc.method.annotation; + +import java.io.OutputStream; +import java.util.concurrent.Callable; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.util.Assert; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.ModelAndViewContainer; + + +/** + * Supports return values of type + * {@link org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody} + * and also {@code ResponseEntity}. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +public class StreamingResponseBodyReturnValueHandler implements HandlerMethodReturnValueHandler { + + private static final Log logger = LogFactory.getLog(StreamingResponseBodyReturnValueHandler.class); + + + @Override + public boolean supportsReturnType(MethodParameter returnType) { + if (StreamingResponseBody.class.isAssignableFrom(returnType.getParameterType())) { + return true; + } + else if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) { + Class bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve(); + return (bodyType != null && StreamingResponseBody.class.isAssignableFrom(bodyType)); + } + return false; + } + + @Override + public void handleReturnValue(Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + + if (returnValue == null) { + mavContainer.setRequestHandled(true); + return; + } + + HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); + ServerHttpResponse outputMessage = new ServletServerHttpResponse(response); + + if (ResponseEntity.class.isAssignableFrom(returnValue.getClass())) { + ResponseEntity responseEntity = (ResponseEntity) returnValue; + outputMessage.setStatusCode(responseEntity.getStatusCode()); + outputMessage.getHeaders().putAll(responseEntity.getHeaders()); + + returnValue = responseEntity.getBody(); + if (returnValue == null) { + mavContainer.setRequestHandled(true); + return; + } + } + + Assert.isInstanceOf(StreamingResponseBody.class, returnValue); + StreamingResponseBody streamingBody = (StreamingResponseBody) returnValue; + + Callable callable = new StreamingResponseBodyTask(outputMessage.getBody(), streamingBody); + WebAsyncUtils.getAsyncManager(webRequest).startCallableProcessing(callable, mavContainer); + } + + + private static class StreamingResponseBodyTask implements Callable { + + private final OutputStream outputStream; + + private final StreamingResponseBody streamingBody; + + + public StreamingResponseBodyTask(OutputStream outputStream, StreamingResponseBody streamingBody) { + this.outputStream = outputStream; + this.streamingBody = streamingBody; + } + + @Override + public Void call() throws Exception { + this.streamingBody.writeTo(this.outputStream); + return null; + } + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java index 3079f8d228f..0c9f54d7ec7 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethodTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,14 @@ package org.springframework.web.servlet.mvc.method.annotation; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; + import javax.servlet.http.HttpServletResponse; import org.junit.Before; @@ -46,8 +50,6 @@ import org.springframework.web.method.support.HandlerMethodReturnValueHandlerCom import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.view.RedirectView; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; /** * Test fixture with {@link ServletInvocableHandlerMethod}. @@ -220,6 +222,30 @@ public class ServletInvocableHandlerMethodTests { assertEquals("", this.response.getContentAsString()); } + @Test + public void wrapConcurrentResult_ResponseBodyEmitter() throws Exception { + List> converters = new ArrayList>(); + converters.add(new StringHttpMessageConverter()); + this.returnValueHandlers.addHandler(new ResponseBodyEmitterReturnValueHandler(converters)); + ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new AsyncHandler(), "handleWithEmitter"); + handlerMethod = handlerMethod.wrapConcurrentResult(null); + handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer); + + assertEquals(200, this.response.getStatus()); + assertEquals("", this.response.getContentAsString()); + } + + @Test + public void wrapConcurrentResult_StreamingResponseBody() throws Exception { + this.returnValueHandlers.addHandler(new StreamingResponseBodyReturnValueHandler()); + ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new AsyncHandler(), "handleWithStreaming"); + handlerMethod = handlerMethod.wrapConcurrentResult(null); + handlerMethod.invokeAndHandle(this.webRequest, this.mavContainer); + + assertEquals(200, this.response.getStatus()); + assertEquals("", this.response.getContentAsString()); + } + // SPR-12287 (16/Oct/14 comments) @Test @@ -273,6 +299,7 @@ public class ServletInvocableHandlerMethodTests { } } + @SuppressWarnings("unused") private static class MethodLevelResponseBodyHandler { @ResponseBody @@ -281,28 +308,28 @@ public class ServletInvocableHandlerMethodTests { } } + @SuppressWarnings("unused") @ResponseBody private static class TypeLevelResponseBodyHandler { - @SuppressWarnings("unused") public DeferredResult handle() { return new DeferredResult(); } } + @SuppressWarnings("unused") private static class ResponseEntityHandler { - @SuppressWarnings("unused") public DeferredResult> handleDeferred() { return new DeferredResult<>(); } - @SuppressWarnings("unused") public ResponseEntity handleRawType() { return ResponseEntity.ok().build(); } } + @SuppressWarnings("unused") private static class ExceptionRaisingReturnValueHandler implements HandlerMethodReturnValueHandler { @Override @@ -317,4 +344,16 @@ public class ServletInvocableHandlerMethodTests { } } + @SuppressWarnings("unused") + private static class AsyncHandler { + + public ResponseBodyEmitter handleWithEmitter() { + return null; + } + + public StreamingResponseBody handleWithStreaming() { + return null; + } + } + } \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandlerTests.java new file mode 100644 index 00000000000..7be780de4fb --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandlerTests.java @@ -0,0 +1,170 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.mvc.method.annotation; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.mock.web.test.MockHttpServletResponse; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.async.AsyncWebRequest; +import org.springframework.web.context.request.async.StandardServletAsyncWebRequest; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.method.support.ModelAndViewContainer; + + +/** + * Unit tests for + * {@link org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBodyReturnValueHandler}. + * + * @author Rossen Stoyanchev + */ +public class StreamingResponseBodyReturnValueHandlerTests { + + private StreamingResponseBodyReturnValueHandler handler; + + private ModelAndViewContainer mavContainer; + + private NativeWebRequest webRequest; + + private MockHttpServletRequest request; + + private MockHttpServletResponse response; + + + @Before + public void setUp() throws Exception { + + this.handler = new StreamingResponseBodyReturnValueHandler(); + this.mavContainer = new ModelAndViewContainer(); + + this.request = new MockHttpServletRequest("GET", "/path"); + this.response = new MockHttpServletResponse(); + this.webRequest = new ServletWebRequest(this.request, this.response); + + AsyncWebRequest asyncWebRequest = new StandardServletAsyncWebRequest(this.request, this.response); + WebAsyncUtils.getAsyncManager(this.webRequest).setAsyncWebRequest(asyncWebRequest); + this.request.setAsyncSupported(true); + } + + @Test + public void supportsReturnType() throws Exception { + assertTrue(this.handler.supportsReturnType(returnType(TestController.class, "handle"))); + assertTrue(this.handler.supportsReturnType(returnType(TestController.class, "handleResponseEntity"))); + assertFalse(this.handler.supportsReturnType(returnType(TestController.class, "handleResponseEntityString"))); + assertFalse(this.handler.supportsReturnType(returnType(TestController.class, "handleResponseEntityParameterized"))); + } + + @Test + public void streamingResponseBody() throws Exception { + + CountDownLatch latch = new CountDownLatch(1); + + MethodParameter returnType = returnType(TestController.class, "handle"); + StreamingResponseBody streamingBody = new StreamingResponseBody() { + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + outputStream.write("foo".getBytes(Charset.forName("UTF-8"))); + latch.countDown(); + } + }; + this.handler.handleReturnValue(streamingBody, returnType, this.mavContainer, this.webRequest); + + assertTrue(this.request.isAsyncStarted()); + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertEquals("foo", this.response.getContentAsString()); + } + + + @Test + public void responseEntity() throws Exception { + + CountDownLatch latch = new CountDownLatch(1); + + MethodParameter returnType = returnType(TestController.class, "handleResponseEntity"); + ResponseEntity emitter = ResponseEntity.ok().header("foo", "bar") + .body(new StreamingResponseBody() { + + @Override + public void writeTo(OutputStream outputStream) throws IOException { + outputStream.write("foo".getBytes(Charset.forName("UTF-8"))); + latch.countDown(); + } + }); + this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest); + + assertTrue(this.request.isAsyncStarted()); + assertEquals(200, this.response.getStatus()); + assertEquals("bar", this.response.getHeader("foo")); + + assertTrue(latch.await(5, TimeUnit.SECONDS)); + assertEquals("foo", this.response.getContentAsString()); + + } + + @Test + public void responseEntityNoContent() throws Exception { + MethodParameter returnType = returnType(TestController.class, "handleResponseEntity"); + ResponseEntity emitter = ResponseEntity.noContent().build(); + this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest); + + assertFalse(this.request.isAsyncStarted()); + assertEquals(204, this.response.getStatus()); + } + + + private MethodParameter returnType(Class clazz, String methodName) throws NoSuchMethodException { + Method method = clazz.getDeclaredMethod(methodName); + return new MethodParameter(method, -1); + } + + + @SuppressWarnings("unused") + private static class TestController { + + private StreamingResponseBody handle() { + return null; + } + + private ResponseEntity handleResponseEntity() { + return null; + } + + private ResponseEntity handleResponseEntityString() { + return null; + } + + private ResponseEntity> handleResponseEntityParameterized() { + return null; + } + } + +} diff --git a/src/asciidoc/web-mvc.adoc b/src/asciidoc/web-mvc.adoc index 92fbf2944bf..6a57ec71acd 100644 --- a/src/asciidoc/web-mvc.adoc +++ b/src/asciidoc/web-mvc.adoc @@ -1261,6 +1261,8 @@ The following are the supported return types: asynchronously; also supported as the body within a `ResponseEntity`. * An `SseEmitter` can be returned to write Server-Sent Events to the response asynchronously; also supported as the body within a `ResponseEntity`. +* A `StreamingResponseBody` can be returned to write to the response OutputStream + asynchronously; also supported as the body within a `ResponseEntity`. * Any other return type is considered to be a single model attribute to be exposed to the view, using the attribute name specified through `@ModelAttribute` at the method level (or the default attribute name based on the return type class name). The model