diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java index 03b7169279..155ef13a14 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java @@ -22,6 +22,7 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; import java.util.Collection; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; import org.junit.Before; @@ -43,6 +44,7 @@ import org.springframework.web.context.request.async.DeferredResult; * Tests with asynchronous request handling. * * @author Rossen Stoyanchev + * @author Sebastien Deleuze */ public class AsyncTests { @@ -112,9 +114,21 @@ public class AsyncTests { .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); } - // SPR-12735 + @Test // SPR-12597 + public void testCompletableFuture() throws Exception { + MvcResult mvcResult = this.mockMvc.perform(get("/1").param("completableFuture", "true")) + .andExpect(request().asyncStarted()) + .andReturn(); - @Test + this.asyncController.onMessage("Joe"); + + this.mockMvc.perform(asyncDispatch(mvcResult)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); + } + + @Test // SPR-12735 public void testPrintAsyncResult() throws Exception { MvcResult mvcResult = this.mockMvc.perform(get("/1").param("deferredResult", "true")) .andDo(print()) @@ -182,6 +196,14 @@ public class AsyncTests { return futureTask; } + @RequestMapping(value="/{id}", params="completableFuture", produces="application/json") + @ResponseBody + public CompletableFuture getCompletableFuture() { + CompletableFuture future = new CompletableFuture(); + future.complete(new Person("Joe")); + return future; + } + public void onMessage(String name) { for (DeferredResult deferredResult : this.deferredResults) { deferredResult.setResult(new Person(name)); 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 ecc27d16a1..e3b7655be0 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 @@ -213,6 +213,10 @@ import java.util.concurrent.Callable; *
  • A {@link org.springframework.util.concurrent.ListenableFuture} * which the application uses to produce a return value in a separate * thread of its own choosing, as an alternative to returning a Callable. + *
  • A {@link java.util.concurrent.CompletionStage} (implemented by + * {@link java.util.concurrent.CompletableFuture} for example) + * which the application uses to produce a return value in a separate + * thread of its own choosing, as an alternative to returning a Callable. *
  • A {@link org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter} * can be used to write multiple objects to the response asynchronously; * also supported as the body within {@code ResponseEntity}.
  • 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 new file mode 100644 index 0000000000..6e108b3697 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CompletionStageReturnValueHandler.java @@ -0,0 +1,76 @@ +/* + * 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.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.springframework.core.MethodParameter; +import org.springframework.lang.UsesJava8; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * Handles return values of type {@link CompletionStage} (implemented by + * {@link java.util.concurrent.CompletableFuture} for example). + * + * @author Sebastien Deleuze + * @since 4.2 + */ +@UsesJava8 +public class CompletionStageReturnValueHandler implements HandlerMethodReturnValueHandler { + + @Override + public boolean supportsReturnType(MethodParameter returnType) { + return CompletionStage.class.isAssignableFrom(returnType.getParameterType()); + } + + @Override + public void handleReturnValue(Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + + if (returnValue == null) { + mavContainer.setRequestHandled(true); + return; + } + + final DeferredResult deferredResult = new DeferredResult(); + WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer); + + @SuppressWarnings("unchecked") + CompletionStage future = (CompletionStage) returnValue; + future.thenAccept(new Consumer() { + @Override + public void accept(Object result) { + deferredResult.setResult(result); + } + }); + future.exceptionally(new Function() { + @Override + public Object apply(Throwable ex) { + deferredResult.setErrorResult(ex); + return null; + } + }); + + } + +} 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 e9ccd1dd48..b310405884 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 @@ -47,6 +47,7 @@ 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; @@ -115,6 +116,10 @@ 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; @@ -653,6 +658,9 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter 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));