diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CompletionStageReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CompletionStageReturnValueHandler.java index 6543b76c56..7ec8e7c0f9 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CompletionStageReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CompletionStageReturnValueHandler.java @@ -34,7 +34,11 @@ import org.springframework.web.method.support.ModelAndViewContainer; * * @author Sebastien Deleuze * @since 4.2 + * + * @deprecated as of 4.3 {@link DeferredResultMethodReturnValueHandler} supports + * CompletionStage return values via an adapter mechanism. */ +@Deprecated @UsesJava8 public class CompletionStageReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultAdapter.java new file mode 100644 index 0000000000..2ed4958554 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultAdapter.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2016 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 org.springframework.web.context.request.async.DeferredResult; + +/** + * Contract to adapt a single-value async return value to {@code DeferredResult}. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public interface DeferredResultAdapter { + + /** + * Create a {@code DeferredResult} for the given return value. + * @param returnValue the return value, never {@code null} + * @return the DeferredResult + */ + DeferredResult adaptToDeferredResult(Object returnValue); + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java index 147e6afff8..6361c77ff1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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,7 +16,17 @@ package org.springframework.web.servlet.mvc.method.annotation; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; + import org.springframework.core.MethodParameter; +import org.springframework.lang.UsesJava8; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureCallback; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.WebAsyncUtils; @@ -24,21 +34,56 @@ import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandl import org.springframework.web.method.support.ModelAndViewContainer; /** - * Handles return values of type {@link DeferredResult}. + * Handler for return values of type {@link DeferredResult}, {@link ListenableFuture}, + * {@link CompletionStage} and any other async type with a {@link #getAdapterMap() + * registered adapter}. * * @author Rossen Stoyanchev * @since 3.2 */ public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { + private final Map, DeferredResultAdapter> adapterMap; + + + public DeferredResultMethodReturnValueHandler() { + this.adapterMap = new HashMap, DeferredResultAdapter>(5); + this.adapterMap.put(DeferredResult.class, new SimpleDeferredResultAdapter()); + this.adapterMap.put(ListenableFuture.class, new ListenableFutureAdapter()); + if (ClassUtils.isPresent("java.util.concurrent.CompletionStage", getClass().getClassLoader())) { + this.adapterMap.put(CompletionStage.class, new CompletionStageAdapter()); + } + } + + + /** + * Return the map with {@code DeferredResult} adapters. + *

By default the map contains adapters for {@code DeferredResult}, which + * simply downcasts, {@link ListenableFuture}, and {@link CompletionStage}. + * @return the map of adapters + */ + public Map, DeferredResultAdapter> getAdapterMap() { + return this.adapterMap; + } + + private DeferredResultAdapter getAdapterFor(Class type) { + for (Class adapteeType : getAdapterMap().keySet()) { + if (adapteeType.isAssignableFrom(type)) { + return getAdapterMap().get(adapteeType); + } + } + return null; + } + + @Override public boolean supportsReturnType(MethodParameter returnType) { - return DeferredResult.class.isAssignableFrom(returnType.getParameterType()); + return (getAdapterFor(returnType.getParameterType()) != null); } @Override public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) { - return (returnValue != null && returnValue instanceof DeferredResult); + return (returnValue != null && (getAdapterFor(returnValue.getClass()) != null)); } @Override @@ -50,8 +95,74 @@ public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMetho return; } - DeferredResult deferredResult = (DeferredResult) returnValue; - WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer); + DeferredResultAdapter adapter = getAdapterFor(returnValue.getClass()); + Assert.notNull(adapter); + DeferredResult result = adapter.adaptToDeferredResult(returnValue); + WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(result, mavContainer); + } + + + /** + * Adapter for {@code DeferredResult} return values. + */ + private static class SimpleDeferredResultAdapter implements DeferredResultAdapter { + + @Override + public DeferredResult adaptToDeferredResult(Object returnValue) { + Assert.isInstanceOf(DeferredResult.class, returnValue); + return (DeferredResult) returnValue; + } + } + + /** + * Adapter for {@code ListenableFuture} return values. + */ + private static class ListenableFutureAdapter implements DeferredResultAdapter { + + @Override + public DeferredResult adaptToDeferredResult(Object returnValue) { + Assert.isInstanceOf(ListenableFuture.class, returnValue); + final DeferredResult result = new DeferredResult(); + ((ListenableFuture) returnValue).addCallback(new ListenableFutureCallback() { + @Override + public void onSuccess(Object value) { + result.setResult(value); + } + @Override + public void onFailure(Throwable ex) { + result.setErrorResult(ex); + } + }); + return result; + } + } + + /** + * Adapter for {@code CompletionStage} return values. + */ + @UsesJava8 + private static class CompletionStageAdapter implements DeferredResultAdapter { + + @Override + public DeferredResult adaptToDeferredResult(Object returnValue) { + Assert.isInstanceOf(CompletionStage.class, returnValue); + final DeferredResult result = new DeferredResult(); + @SuppressWarnings("unchecked") + CompletionStage future = (CompletionStage) returnValue; + future.handle(new BiFunction() { + @Override + public Object apply(Object value, Throwable ex) { + if (ex != null) { + result.setErrorResult(ex); + } + else { + result.setResult(value); + } + return null; + } + }); + return result; + } } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java index 18d25a4f16..3a3706f06b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -31,7 +31,11 @@ import org.springframework.web.method.support.ModelAndViewContainer; * * @author Rossen Stoyanchev * @since 4.1 + * + * @deprecated as of 4.3 {@link DeferredResultMethodReturnValueHandler} supports + * ListenableFuture return values via an adapter mechanism. */ +@Deprecated public class ListenableFutureReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { @Override 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 298be712d3..a48efb1b9e 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 @@ -48,7 +48,6 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessage import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.ui.ModelMap; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.web.accept.ContentNegotiationManager; @@ -117,10 +116,6 @@ import org.springframework.web.util.WebUtils; public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { - private static final boolean completionStagePresent = ClassUtils.isPresent( - "java.util.concurrent.CompletionStage", RequestMappingHandlerAdapter.class.getClassLoader()); - - private List customArgumentResolvers; private HandlerMethodArgumentResolverComposite argumentResolvers; @@ -677,10 +672,6 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter handlers.add(new CallableMethodReturnValueHandler()); handlers.add(new DeferredResultMethodReturnValueHandler()); handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory)); - handlers.add(new ListenableFutureReturnValueHandler()); - if (completionStagePresent) { - handlers.add(new CompletionStageReturnValueHandler()); - } // Annotation-based return value types handlers.add(new ModelAttributeMethodProcessor(false)); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterAdapter.java new file mode 100644 index 0000000000..debd346239 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterAdapter.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2016 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 org.springframework.http.server.ServerHttpResponse; + + +/** + * Contract to adapt streaming async types to {@code ResponseBodyEmitter}. + * @author Rossen Stoyanchev + * @since 4.3 + */ +public interface ResponseBodyEmitterAdapter { + + /** + * Obtain a {@code ResponseBodyEmitter} for the given return value. If + * the return is the body {@code ResponseEntity} then the given + * {@code ServerHttpResponse} contains its status and headers. + * @param returnValue the return value, never {@code null} + * @param response the response + * @return the return value adapted to a {@code ResponseBodyEmitter} + */ + ResponseBodyEmitter adaptToEmitter(Object returnValue, ServerHttpResponse response); + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java index 9a21605860..de081800bd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -18,8 +18,9 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; import java.io.OutputStream; +import java.util.HashMap; import java.util.List; - +import java.util.Map; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletResponse; @@ -44,8 +45,9 @@ import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandl import org.springframework.web.method.support.ModelAndViewContainer; /** - * Supports return values of type {@link ResponseBodyEmitter} and also - * {@code ResponseEntity}. + * Handler for return values of type {@link ResponseBodyEmitter} (and the + * {@code ResponseEntity} sub-class) as well as any other + * async type with a {@link #getAdapterMap() registered adapter}. * * @author Rossen Stoyanchev * @since 4.2 @@ -54,36 +56,61 @@ public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethod private static final Log logger = LogFactory.getLog(ResponseBodyEmitterReturnValueHandler.class); + private final List> messageConverters; + private final Map, ResponseBodyEmitterAdapter> adapterMap; + public ResponseBodyEmitterReturnValueHandler(List> messageConverters) { Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); this.messageConverters = messageConverters; + this.adapterMap = new HashMap, ResponseBodyEmitterAdapter>(3); + this.adapterMap.put(ResponseBodyEmitter.class, new SimpleResponseBodyEmitterAdapter()); + } + + + /** + * Return the map with {@code ResponseBodyEmitter} adapters. + * By default the map contains a single adapter {@code ResponseBodyEmitter} + * that simply downcasts the return value. + * @return the map of adapters + */ + public Map, ResponseBodyEmitterAdapter> getAdapterMap() { + return this.adapterMap; + } + + private ResponseBodyEmitterAdapter getAdapterFor(Class type) { + for (Class adapteeType : getAdapterMap().keySet()) { + if (adapteeType.isAssignableFrom(type)) { + return getAdapterMap().get(adapteeType); + } + } + return null; } @Override public boolean supportsReturnType(MethodParameter returnType) { - if (ResponseBodyEmitter.class.isAssignableFrom(returnType.getParameterType())) { - return true; + Class bodyType; + if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) { + bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve(); } - else if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) { - Class bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve(); - return (bodyType != null && ResponseBodyEmitter.class.isAssignableFrom(bodyType)); + else { + bodyType = returnType.getParameterType(); } - return false; + return (getAdapterFor(bodyType) != null); } @Override public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) { if (returnValue != null) { - if (returnValue instanceof ResponseBodyEmitter) { - return true; + Object adaptFrom = returnValue; + if (returnValue instanceof ResponseEntity) { + adaptFrom = ((ResponseEntity) returnValue).getBody(); } - else if (returnValue instanceof ResponseEntity) { - Object body = ((ResponseEntity) returnValue).getBody(); - return (body != null && body instanceof ResponseBodyEmitter); + if (adaptFrom != null) { + return (getAdapterFor(adaptFrom.getClass()) != null); } } return false; @@ -115,8 +142,9 @@ public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethod ServletRequest request = webRequest.getNativeRequest(ServletRequest.class); ShallowEtagHeaderFilter.disableContentCaching(request); - Assert.isInstanceOf(ResponseBodyEmitter.class, returnValue); - ResponseBodyEmitter emitter = (ResponseBodyEmitter) returnValue; + ResponseBodyEmitterAdapter adapter = getAdapterFor(returnValue.getClass()); + Assert.notNull(adapter); + ResponseBodyEmitter emitter = adapter.adaptToEmitter(returnValue, outputMessage); emitter.extendResponse(outputMessage); // Commit the response and wrap to ignore further header changes @@ -132,6 +160,18 @@ public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethod } + /** + * Adapter for {@code ResponseBodyEmitter} return values. + */ + private static class SimpleResponseBodyEmitterAdapter implements ResponseBodyEmitterAdapter { + + @Override + public ResponseBodyEmitter adaptToEmitter(Object returnValue, ServerHttpResponse response) { + Assert.isInstanceOf(ResponseBodyEmitter.class, returnValue); + return (ResponseBodyEmitter) returnValue; + } + } + /** * ResponseBodyEmitter.Handler that writes with HttpMessageConverter's. */ diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultReturnValueHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultReturnValueHandlerTests.java new file mode 100644 index 0000000000..9e902ac621 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultReturnValueHandlerTests.java @@ -0,0 +1,197 @@ +/* + * Copyright 2002-2016 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.lang.reflect.Method; +import java.util.concurrent.CompletableFuture; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.MethodParameter; +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.mock.web.test.MockHttpServletResponse; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.SettableListenableFuture; +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.DeferredResult; +import org.springframework.web.context.request.async.StandardServletAsyncWebRequest; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.method.support.ModelAndViewContainer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for {@link DeferredResultMethodReturnValueHandler}. + * @author Rossen Stoyanchev + */ +public class DeferredResultReturnValueHandlerTests { + + private DeferredResultMethodReturnValueHandler handler; + + private MockHttpServletRequest request; + + private NativeWebRequest webRequest; + + + @Before + public void setUp() throws Exception { + this.handler = new DeferredResultMethodReturnValueHandler(); + this.request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + this.webRequest = new ServletWebRequest(this.request, response); + + AsyncWebRequest asyncWebRequest = new StandardServletAsyncWebRequest(this.request, response); + WebAsyncUtils.getAsyncManager(this.webRequest).setAsyncWebRequest(asyncWebRequest); + this.request.setAsyncSupported(true); + } + + + @Test + public void supportsReturnType() throws Exception { + assertTrue(this.handler.supportsReturnType(returnType("handleDeferredResult"))); + assertTrue(this.handler.supportsReturnType(returnType("handleListenableFuture"))); + assertTrue(this.handler.supportsReturnType(returnType("handleCompletableFuture"))); + assertFalse(this.handler.supportsReturnType(returnType("handleString"))); + } + + @Test + public void deferredResult() throws Exception { + MethodParameter returnType = returnType("handleDeferredResult"); + DeferredResult deferredResult = new DeferredResult<>(); + handleReturnValue(deferredResult, returnType); + + assertTrue(this.request.isAsyncStarted()); + assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult()); + + deferredResult.setResult("foo"); + assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult()); + assertEquals("foo", WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult()); + } + + @Test + public void deferredResultWitError() throws Exception { + MethodParameter returnType = returnType("handleDeferredResult"); + DeferredResult deferredResult = new DeferredResult<>(); + handleReturnValue(deferredResult, returnType); + + assertTrue(this.request.isAsyncStarted()); + assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult()); + + IllegalStateException ex = new IllegalStateException(); + deferredResult.setErrorResult(ex); + assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult()); + assertSame(ex, WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult()); + } + + @Test + public void listenableFuture() throws Exception { + MethodParameter returnType = returnType("handleListenableFuture"); + SettableListenableFuture future = new SettableListenableFuture<>(); + handleReturnValue(future, returnType); + + assertTrue(this.request.isAsyncStarted()); + assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult()); + + future.set("foo"); + assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult()); + assertEquals("foo", WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult()); + } + + @Test + public void listenableFutureWithError() throws Exception { + MethodParameter returnType = returnType("handleListenableFuture"); + SettableListenableFuture future = new SettableListenableFuture<>(); + handleReturnValue(future, returnType); + + assertTrue(this.request.isAsyncStarted()); + assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult()); + + IllegalStateException ex = new IllegalStateException(); + future.setException(ex); + assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult()); + assertSame(ex, WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult()); + } + + @Test + public void completableFuture() throws Exception { + MethodParameter returnType = returnType("handleCompletableFuture"); + SettableListenableFuture future = new SettableListenableFuture<>(); + handleReturnValue(future, returnType); + + assertTrue(this.request.isAsyncStarted()); + assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult()); + + future.set("foo"); + assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult()); + assertEquals("foo", WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult()); + } + + @Test + public void completableFutureWithError() throws Exception { + MethodParameter returnType = returnType("handleCompletableFuture"); + CompletableFuture future = new CompletableFuture<>(); + handleReturnValue(future, returnType); + + assertTrue(this.request.isAsyncStarted()); + assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult()); + + IllegalStateException ex = new IllegalStateException(); + future.completeExceptionally(ex); + assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult()); + assertSame(ex, WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult()); + } + + + private void handleReturnValue(Object returnValue, MethodParameter returnType) throws Exception { + ModelAndViewContainer mavContainer = new ModelAndViewContainer(); + this.handler.handleReturnValue(returnValue, returnType, mavContainer, this.webRequest); + } + + private MethodParameter returnType(String methodName) throws NoSuchMethodException { + Method method = TestController.class.getDeclaredMethod(methodName); + return new MethodParameter(method, -1); + } + + + @SuppressWarnings("unused") + private static class TestController { + + private String handleString() { + return null; + } + + private DeferredResult handleDeferredResult() { + return null; + } + + private ListenableFuture handleListenableFuture() { + return null; + } + + private CompletableFuture handleCompletableFuture() { + return null; + } + + + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java index c38dd69de0..1d287f05b4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 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. @@ -51,14 +51,12 @@ public class ResponseBodyEmitterReturnValueHandlerTests { private ResponseBodyEmitterReturnValueHandler handler; - private ModelAndViewContainer mavContainer; - - private NativeWebRequest webRequest; - private MockHttpServletRequest request; private MockHttpServletResponse response; + private NativeWebRequest webRequest; + @Before public void setUp() throws Exception { @@ -67,8 +65,6 @@ public class ResponseBodyEmitterReturnValueHandlerTests { new StringHttpMessageConverter(), new MappingJackson2HttpMessageConverter()); this.handler = new ResponseBodyEmitterReturnValueHandler(converters); - this.mavContainer = new ModelAndViewContainer(); - this.request = new MockHttpServletRequest(); this.response = new MockHttpServletResponse(); this.webRequest = new ServletWebRequest(this.request, this.response); @@ -80,18 +76,18 @@ public class ResponseBodyEmitterReturnValueHandlerTests { @Test public void supportsReturnType() throws Exception { - assertTrue(this.handler.supportsReturnType(returnType(TestController.class, "handle"))); - assertTrue(this.handler.supportsReturnType(returnType(TestController.class, "handleSse"))); - assertTrue(this.handler.supportsReturnType(returnType(TestController.class, "handleResponseEntity"))); - assertFalse(this.handler.supportsReturnType(returnType(TestController.class, "handleResponseEntityString"))); - assertFalse(this.handler.supportsReturnType(returnType(TestController.class, "handleResponseEntityParameterized"))); + assertTrue(this.handler.supportsReturnType(returnType("handle"))); + assertTrue(this.handler.supportsReturnType(returnType("handleSse"))); + assertTrue(this.handler.supportsReturnType(returnType("handleResponseEntity"))); + assertFalse(this.handler.supportsReturnType(returnType("handleResponseEntityString"))); + assertFalse(this.handler.supportsReturnType(returnType("handleResponseEntityParameterized"))); } @Test public void responseBodyEmitter() throws Exception { - MethodParameter returnType = returnType(TestController.class, "handle"); + MethodParameter returnType = returnType("handle"); ResponseBodyEmitter emitter = new ResponseBodyEmitter(); - this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest); + handleReturnValue(emitter, returnType); assertTrue(this.request.isAsyncStarted()); assertEquals("", this.response.getContentAsString()); @@ -133,8 +129,8 @@ public class ResponseBodyEmitterReturnValueHandlerTests { emitter.onTimeout(mock(Runnable.class)); emitter.onCompletion(mock(Runnable.class)); - MethodParameter returnType = returnType(TestController.class, "handle"); - this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest); + MethodParameter returnType = returnType("handle"); + handleReturnValue(emitter, returnType); verify(asyncWebRequest).setTimeout(19000L); verify(asyncWebRequest).addTimeoutHandler(any(Runnable.class)); @@ -144,9 +140,9 @@ public class ResponseBodyEmitterReturnValueHandlerTests { @Test public void sseEmitter() throws Exception { - MethodParameter returnType = returnType(TestController.class, "handleSse"); + MethodParameter returnType = returnType("handleSse"); SseEmitter emitter = new SseEmitter(); - this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest); + handleReturnValue(emitter, returnType); assertTrue(this.request.isAsyncStarted()); assertEquals(200, this.response.getStatus()); @@ -174,9 +170,9 @@ public class ResponseBodyEmitterReturnValueHandlerTests { @Test public void responseEntitySse() throws Exception { - MethodParameter returnType = returnType(TestController.class, "handleResponseEntitySse"); - ResponseEntity emitter = ResponseEntity.ok().header("foo", "bar").body(new SseEmitter()); - this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest); + MethodParameter returnType = returnType("handleResponseEntitySse"); + ResponseEntity entity = ResponseEntity.ok().header("foo", "bar").body(new SseEmitter()); + handleReturnValue(entity, returnType); assertTrue(this.request.isAsyncStarted()); assertEquals(200, this.response.getStatus()); @@ -186,17 +182,21 @@ public class ResponseBodyEmitterReturnValueHandlerTests { @Test public void responseEntitySseNoContent() throws Exception { - MethodParameter returnType = returnType(TestController.class, "handleResponseEntitySse"); - ResponseEntity emitter = ResponseEntity.noContent().build(); - this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest); + MethodParameter returnType = returnType("handleResponseEntitySse"); + ResponseEntity entity = ResponseEntity.noContent().build(); + handleReturnValue(entity, returnType); assertFalse(this.request.isAsyncStarted()); assertEquals(204, this.response.getStatus()); } + private void handleReturnValue(Object returnValue, MethodParameter returnType) throws Exception { + ModelAndViewContainer mavContainer = new ModelAndViewContainer(); + this.handler.handleReturnValue(returnValue, returnType, mavContainer, this.webRequest); + } - private MethodParameter returnType(Class clazz, String methodName) throws NoSuchMethodException { - Method method = clazz.getDeclaredMethod(methodName); + private MethodParameter returnType(String methodName) throws NoSuchMethodException { + Method method = TestController.class.getDeclaredMethod(methodName); return new MethodParameter(method, -1); }