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 0de20698c6c..657b6024e48 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 @@ -34,6 +34,8 @@ import org.springframework.test.web.Person; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.ui.Model; +import org.springframework.util.concurrent.ListenableFuture; +import org.springframework.util.concurrent.ListenableFutureTask; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.async.DeferredResult; @@ -96,11 +98,29 @@ public class AsyncTests { .andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}")); } + @Test + public void testListenableFuture() throws Exception { + MvcResult mvcResult = this.mockMvc.perform(get("/1").param("listenableFuture", "true")) + .andExpect(request().asyncStarted()) + .andReturn(); + + 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}")); + } + @Controller private static class AsyncController { - private Collection> deferredResults = new CopyOnWriteArrayList>(); + private Collection> deferredResults = + new CopyOnWriteArrayList>(); + + private Collection> futureTasks = + new CopyOnWriteArrayList>(); @RequestMapping(value="/{id}", params="callable", produces="application/json") @@ -130,11 +150,28 @@ public class AsyncTests { return deferredResult; } + @RequestMapping(value="/{id}", params="listenableFuture", produces="application/json") + @ResponseBody + public ListenableFuture getListenableFuture() { + ListenableFutureTask futureTask = new ListenableFutureTask(new Callable() { + @Override + public Person call() throws Exception { + return new Person("Joe"); + } + }); + this.futureTasks.add(futureTask); + return futureTask; + } + public void onMessage(String name) { for (DeferredResult deferredResult : this.deferredResults) { deferredResult.setResult(new Person(name)); this.deferredResults.remove(deferredResult); } + for (ListenableFutureTask futureTask : this.futureTasks) { + futureTask.run(); + this.futureTasks.remove(futureTask); + } } } 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 676464f1664..ccbcd55311a 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 @@ -204,6 +204,9 @@ import java.util.concurrent.Callable; *
  • A {@link org.springframework.web.context.request.async.DeferredResult} * 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.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. *
  • {@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/ListenableFutureReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java new file mode 100644 index 00000000000..db7923b94f8 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2014 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.core.MethodParameter; +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; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * Handles return values of type + * {@link org.springframework.util.concurrent.ListenableFuture}. + * + * @author Rossen Stoyanchev + * @since 4.1 + */ +public class ListenableFutureReturnValueHandler implements HandlerMethodReturnValueHandler { + + @Override + public boolean supportsReturnType(MethodParameter returnType) { + Class paramType = returnType.getParameterType(); + return ListenableFuture.class.isAssignableFrom(paramType); + } + + @Override + public void handleReturnValue(Object returnValue, + MethodParameter returnType, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest) throws Exception { + + if (returnValue == null) { + mavContainer.setRequestHandled(true); + return; + } + + DeferredResult deferredResult = new DeferredResult(); + WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer); + + ListenableFuture future = (ListenableFuture) returnValue; + future.addCallback(new ListenableFutureCallback() { + @Override + public void onSuccess(Object result) { + deferredResult.setResult(result); + } + @Override + public void onFailure(Throwable ex) { + deferredResult.setErrorResult(ex); + } + }); + } + +} 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 4d951c777d3..4f8ee6599ec 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 @@ -588,6 +588,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter handlers.add(new CallableMethodReturnValueHandler()); handlers.add(new DeferredResultMethodReturnValueHandler()); handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory)); + handlers.add(new ListenableFutureReturnValueHandler()); // Annotation-based return value types handlers.add(new ModelAttributeMethodProcessor(false)); diff --git a/src/asciidoc/index.adoc b/src/asciidoc/index.adoc index a63ba5428ac..425bf061799 100644 --- a/src/asciidoc/index.adoc +++ b/src/asciidoc/index.adoc @@ -29143,6 +29143,8 @@ The following are the supported return types: asynchronously in a thread managed by Spring MVC. * A `DeferredResult` can be returned when the application wants to produce the return value from a thread of its own choosing. +* A `ListenableFuture` can be returned when the application wants to produce the return + value from a thread of its own choosing. * 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