Skip to content

Commit 5b0a0f4

Browse files
committed
Support CompletableFuture as alternative to DeferredResult in MVC
Issue: SPR-12597
1 parent 72894b2 commit 5b0a0f4

File tree

4 files changed

+112
-2
lines changed

4 files changed

+112
-2
lines changed

spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import java.util.Collection;
2424
import java.util.concurrent.Callable;
25+
import java.util.concurrent.CompletableFuture;
2526
import java.util.concurrent.CopyOnWriteArrayList;
2627

2728
import org.junit.Before;
@@ -43,6 +44,7 @@
4344
* Tests with asynchronous request handling.
4445
*
4546
* @author Rossen Stoyanchev
47+
* @author Sebastien Deleuze
4648
*/
4749
public class AsyncTests {
4850

@@ -112,9 +114,21 @@ public void testListenableFuture() throws Exception {
112114
.andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"));
113115
}
114116

115-
// SPR-12735
117+
@Test // SPR-12597
118+
public void testCompletableFuture() throws Exception {
119+
MvcResult mvcResult = this.mockMvc.perform(get("/1").param("completableFuture", "true"))
120+
.andExpect(request().asyncStarted())
121+
.andReturn();
116122

117-
@Test
123+
this.asyncController.onMessage("Joe");
124+
125+
this.mockMvc.perform(asyncDispatch(mvcResult))
126+
.andExpect(status().isOk())
127+
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
128+
.andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"));
129+
}
130+
131+
@Test // SPR-12735
118132
public void testPrintAsyncResult() throws Exception {
119133
MvcResult mvcResult = this.mockMvc.perform(get("/1").param("deferredResult", "true"))
120134
.andDo(print())
@@ -182,6 +196,14 @@ public Person call() throws Exception {
182196
return futureTask;
183197
}
184198

199+
@RequestMapping(value="/{id}", params="completableFuture", produces="application/json")
200+
@ResponseBody
201+
public CompletableFuture<Person> getCompletableFuture() {
202+
CompletableFuture<Person> future = new CompletableFuture<Person>();
203+
future.complete(new Person("Joe"));
204+
return future;
205+
}
206+
185207
public void onMessage(String name) {
186208
for (DeferredResult<Person> deferredResult : this.deferredResults) {
187209
deferredResult.setResult(new Person(name));

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@
213213
* <li>A {@link org.springframework.util.concurrent.ListenableFuture}
214214
* which the application uses to produce a return value in a separate
215215
* thread of its own choosing, as an alternative to returning a Callable.
216+
* <li>A {@link java.util.concurrent.CompletionStage} (implemented by
217+
* {@link java.util.concurrent.CompletableFuture} for example)
218+
* which the application uses to produce a return value in a separate
219+
* thread of its own choosing, as an alternative to returning a Callable.
216220
* <li>A {@link org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter}
217221
* can be used to write multiple objects to the response asynchronously;
218222
* also supported as the body within {@code ResponseEntity}.</li>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
17+
package org.springframework.web.servlet.mvc.method.annotation;
18+
19+
import java.util.concurrent.CompletionStage;
20+
import java.util.function.Consumer;
21+
import java.util.function.Function;
22+
23+
import org.springframework.core.MethodParameter;
24+
import org.springframework.lang.UsesJava8;
25+
import org.springframework.web.context.request.NativeWebRequest;
26+
import org.springframework.web.context.request.async.DeferredResult;
27+
import org.springframework.web.context.request.async.WebAsyncUtils;
28+
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
29+
import org.springframework.web.method.support.ModelAndViewContainer;
30+
31+
/**
32+
* Handles return values of type {@link CompletionStage} (implemented by
33+
* {@link java.util.concurrent.CompletableFuture} for example).
34+
*
35+
* @author Sebastien Deleuze
36+
* @since 4.2
37+
*/
38+
@UsesJava8
39+
public class CompletionStageReturnValueHandler implements HandlerMethodReturnValueHandler {
40+
41+
@Override
42+
public boolean supportsReturnType(MethodParameter returnType) {
43+
return CompletionStage.class.isAssignableFrom(returnType.getParameterType());
44+
}
45+
46+
@Override
47+
public void handleReturnValue(Object returnValue, MethodParameter returnType,
48+
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
49+
50+
if (returnValue == null) {
51+
mavContainer.setRequestHandled(true);
52+
return;
53+
}
54+
55+
final DeferredResult<Object> deferredResult = new DeferredResult<Object>();
56+
WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer);
57+
58+
@SuppressWarnings("unchecked")
59+
CompletionStage<Object> future = (CompletionStage<Object>) returnValue;
60+
future.thenAccept(new Consumer<Object>() {
61+
@Override
62+
public void accept(Object result) {
63+
deferredResult.setResult(result);
64+
}
65+
});
66+
future.exceptionally(new Function<Throwable, Object>() {
67+
@Override
68+
public Object apply(Throwable ex) {
69+
deferredResult.setErrorResult(ex);
70+
return null;
71+
}
72+
});
73+
74+
}
75+
76+
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
4848
import org.springframework.ui.ModelMap;
4949
import org.springframework.util.Assert;
50+
import org.springframework.util.ClassUtils;
5051
import org.springframework.util.CollectionUtils;
5152
import org.springframework.util.ReflectionUtils.MethodFilter;
5253
import org.springframework.web.accept.ContentNegotiationManager;
@@ -115,6 +116,10 @@
115116
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
116117
implements BeanFactoryAware, InitializingBean {
117118

119+
private static final boolean completionStagePresent = ClassUtils.isPresent("java.util.concurrent.CompletionStage",
120+
RequestMappingHandlerAdapter.class.getClassLoader());
121+
122+
118123
private List<HandlerMethodArgumentResolver> customArgumentResolvers;
119124

120125
private HandlerMethodArgumentResolverComposite argumentResolvers;
@@ -653,6 +658,9 @@ private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
653658
handlers.add(new DeferredResultMethodReturnValueHandler());
654659
handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
655660
handlers.add(new ListenableFutureReturnValueHandler());
661+
if (completionStagePresent) {
662+
handlers.add(new CompletionStageReturnValueHandler());
663+
}
656664

657665
// Annotation-based return value types
658666
handlers.add(new ModelAttributeMethodProcessor(false));

0 commit comments

Comments
 (0)