From 50d93d37943f08b692f740a563eff3030165a62c Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 15 Feb 2017 23:16:11 +0100 Subject: [PATCH] Consistently support CompletionStage next to CompletableFuture Issue: SPR-15258 --- .../CompletableToListenableFutureAdapter.java | 18 ++++++++++++++++-- .../handler/annotation/MessageMapping.java | 10 +++++++--- .../CompletableFutureReturnValueHandler.java | 11 +++++++---- ...impAnnotationMethodMessageHandlerTests.java | 18 ++++++++++-------- src/asciidoc/web-mvc.adoc | 4 ++-- src/asciidoc/web-websocket.adoc | 10 +++++++--- 6 files changed, 49 insertions(+), 22 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java index a0de053e1b..2629aef175 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 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. @@ -17,15 +17,18 @@ package org.springframework.util.concurrent; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.BiFunction; /** - * Adapts a {@link CompletableFuture} into a {@link ListenableFuture}. + * Adapts a {@link CompletableFuture} or {@link CompletionStage} into a + * Spring {@link ListenableFuture}. * * @author Sebastien Deleuze + * @author Juergen Hoeller * @since 4.2 */ public class CompletableToListenableFutureAdapter implements ListenableFuture { @@ -35,6 +38,17 @@ public class CompletableToListenableFutureAdapter implements ListenableFuture private final ListenableFutureCallbackRegistry callbacks = new ListenableFutureCallbackRegistry<>(); + /** + * Create a new adapter for the given {@link CompletionStage}. + * @since 4.3.7 + */ + public CompletableToListenableFutureAdapter(CompletionStage completionStage) { + this(completionStage.toCompletableFuture()); + } + + /** + * Create a new adapter for the given {@link CompletableFuture}. + */ public CompletableToListenableFutureAdapter(CompletableFuture completableFuture) { this.completableFuture = completableFuture; this.completableFuture.handle(new BiFunction() { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMapping.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMapping.java index ad0d4fca60..2ffe472134 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMapping.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2017 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. @@ -66,8 +66,12 @@ import org.springframework.messaging.Message; * HTTP handshake that initiates WebSocket sessions. * * - *

By default the return value is wrapped as a message and sent to the destination - * specified with an {@link SendTo @SendTo} method-level annotation. + *

A return value will get wrapped as a message and sent to a default response + * destination or to a custom destination specified with an {@link SendTo @SendTo} + * method-level annotation. Such a response may also be provided asynchronously + * via a {@link org.springframework.util.concurrent.ListenableFuture} return type + * or a corresponding JDK 8 {@link java.util.concurrent.CompletableFuture} / + * {@link java.util.concurrent.CompletionStage} handle. * *

STOMP over WebSocket

*

An {@link SendTo @SendTo} annotation is not strictly required — by default diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/CompletableFutureReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/CompletableFutureReturnValueHandler.java index 8fb0ff59c8..a452abbc92 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/CompletableFutureReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/CompletableFutureReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 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. @@ -17,28 +17,31 @@ package org.springframework.messaging.handler.invocation; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import org.springframework.core.MethodParameter; import org.springframework.util.concurrent.CompletableToListenableFutureAdapter; import org.springframework.util.concurrent.ListenableFuture; /** - * Support for {@link CompletableFuture} as a return value type. + * Support for {@link CompletableFuture} (and as of 4.3.7 also {@link CompletionStage}) + * as a return value type. * * @author Sebastien Deleuze + * @author Juergen Hoeller * @since 4.2 */ public class CompletableFutureReturnValueHandler extends AbstractAsyncReturnValueHandler { @Override public boolean supportsReturnType(MethodParameter returnType) { - return CompletableFuture.class.isAssignableFrom(returnType.getParameterType()); + return CompletionStage.class.isAssignableFrom(returnType.getParameterType()); } @Override @SuppressWarnings("unchecked") public ListenableFuture toListenableFuture(Object returnValue, MethodParameter returnType) { - return new CompletableToListenableFutureAdapter<>((CompletableFuture) returnValue); + return new CompletableToListenableFutureAdapter<>((CompletionStage) returnValue); } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java index 00218df6dc..0d95859432 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 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. @@ -270,7 +270,7 @@ public class SimpAnnotationMethodMessageHandlerTests { this.messageHandler.handleMessage(message); controller.future.run(); - assertTrue(controller.exceptionCatched); + assertTrue(controller.exceptionCaught); } @Test @@ -340,7 +340,6 @@ public class SimpAnnotationMethodMessageHandlerTests { } - private static class TestSimpAnnotationMethodMessageHandler extends SimpAnnotationMethodMessageHandler { public TestSimpAnnotationMethodMessageHandler(SimpMessageSendingOperations brokerTemplate, @@ -434,6 +433,7 @@ public class SimpAnnotationMethodMessageHandlerTests { } } + @Controller @MessageMapping("pre") private static class DotPathSeparatorController { @@ -447,12 +447,14 @@ public class SimpAnnotationMethodMessageHandlerTests { } } + @Controller @MessageMapping("listenable-future") private static class ListenableFutureController { private ListenableFutureTask future; - private boolean exceptionCatched = false; + + private boolean exceptionCaught = false; @MessageMapping("success") public ListenableFutureTask handleListenableFuture() { @@ -470,11 +472,11 @@ public class SimpAnnotationMethodMessageHandlerTests { @MessageExceptionHandler(IllegalStateException.class) public void handleValidationException() { - this.exceptionCatched = true; + this.exceptionCaught = true; } - } + @Controller private static class CompletableFutureController { @@ -492,14 +494,14 @@ public class SimpAnnotationMethodMessageHandlerTests { public void handleValidationException() { this.exceptionCaught = true; } - } + private static class StringTestValidator implements Validator { private final String invalidValue; - private StringTestValidator(String invalidValue) { + public StringTestValidator(String invalidValue) { this.invalidValue = invalidValue; } diff --git a/src/asciidoc/web-mvc.adoc b/src/asciidoc/web-mvc.adoc index 915ef6726f..83e1ae6a59 100644 --- a/src/asciidoc/web-mvc.adoc +++ b/src/asciidoc/web-mvc.adoc @@ -1439,8 +1439,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. +* A `ListenableFuture` or `CompletableFuture`/`CompletionStage` can be returned + when the application wants to produce the value from a thread pool submission. * A `ResponseBodyEmitter` can be returned to write multiple objects to the response asynchronously; also supported as the body within a `ResponseEntity`. * An `SseEmitter` can be returned to write Server-Sent Events to the response diff --git a/src/asciidoc/web-websocket.adoc b/src/asciidoc/web-websocket.adoc index 7290647f85..05aadcd3e5 100644 --- a/src/asciidoc/web-websocket.adoc +++ b/src/asciidoc/web-websocket.adoc @@ -1354,7 +1354,7 @@ the declared method argument type as necessary. * `java.security.Principal` method arguments reflecting the user logged in at the time of the WebSocket HTTP handshake. -The return value from an `@MessageMapping` method is converted with a +A return value from an `@MessageMapping` method will be converted with a `org.springframework.messaging.converter.MessageConverter` and used as the body of a new message that is then sent, by default, to the `"brokerChannel"` with the same destination as the client message but using the prefix `"/topic"` by @@ -1362,8 +1362,12 @@ default. An `@SendTo` message level annotation can be used to specify any other destination instead. It can also be set a class-level to share a common destination. -An `@SubscribeMapping` annotation can also be used to map subscription requests -to `@Controller` methods. It is supported on the method level, but can also be +A response message may also be provided asynchronously via a `ListenableFuture` +or `CompletableFuture`/`CompletionStage` return type signature, analogous to +deferred results in an MVC handler method. + +A `@SubscribeMapping` annotation can be used to map subscription requests to +`@Controller` methods. It is supported on the method level, but can also be combined with a type level `@MessageMapping` annotation that expresses shared mappings across all message handling methods within the same controller.