@MessageExceptionHandler supports error signal

Before this change if a controller method returned a Publisher whose
first signal was an error, the error signal would not be delegated to
a @MessageExceptionHandler method as expected.

To make this work for now we use a package private local copy of the
ChannelSendOperator from spring-web.

See gh-21987
This commit is contained in:
Rossen Stoyanchev
2019-02-21 18:08:30 -05:00
parent d6f4ec8c33
commit 4e1c0c6826
5 changed files with 463 additions and 9 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -26,6 +26,7 @@ import java.util.function.Consumer;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -63,7 +64,7 @@ public class MethodMessageHandlerTests {
assertEquals(5, mappings.keySet().size());
assertThat(mappings.keySet(), Matchers.containsInAnyOrder(
"/handleMessage", "/handleMessageWithArgument", "/handleMessageAndThrow",
"/handleMessage", "/handleMessageWithArgument", "/handleMessageWithError",
"/handleMessageMatch1", "/handleMessageMatch2"));
}
@@ -80,7 +81,7 @@ public class MethodMessageHandlerTests {
handler.handleMessage(message).block(Duration.ofSeconds(5));
StepVerifier.create((Mono<Object>) handler.getLastReturnValue())
StepVerifier.create((Publisher<Object>) handler.getLastReturnValue())
.expectNext("handleMessageMatch1")
.verifyComplete();
}
@@ -100,7 +101,7 @@ public class MethodMessageHandlerTests {
handler.handleMessage(message).block(Duration.ofSeconds(5));
StepVerifier.create((Mono<Object>) handler.getLastReturnValue())
StepVerifier.create((Publisher<Object>) handler.getLastReturnValue())
.expectNext("handleMessageWithArgument,payload=foo")
.verifyComplete();
}
@@ -111,11 +112,11 @@ public class MethodMessageHandlerTests {
TestMethodMessageHandler handler = initMethodMessageHandler(TestController.class);
Message<?> message = new GenericMessage<>("body", Collections.singletonMap(
DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/handleMessageAndThrow"));
DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/handleMessageWithError"));
handler.handleMessage(message).block(Duration.ofSeconds(5));
StepVerifier.create((Mono<Object>) handler.getLastReturnValue())
StepVerifier.create((Publisher<Object>) handler.getLastReturnValue())
.expectNext("handleIllegalStateException,ex=rejected")
.verifyComplete();
}
@@ -153,7 +154,7 @@ public class MethodMessageHandlerTests {
return delay("handleMessageWithArgument,payload=" + payload);
}
public Mono<Void> handleMessageAndThrow() {
public Mono<String> handleMessageWithError() {
return Mono.delay(Duration.ofMillis(10))
.flatMap(aLong -> Mono.error(new IllegalStateException("rejected")));
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.messaging.handler.invocation.reactive;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
@@ -43,7 +44,14 @@ public class TestReturnValueHandler implements HandlerMethodReturnValueHandler {
}
@Override
@SuppressWarnings("unchecked")
public Mono<Void> handleReturnValue(@Nullable Object value, MethodParameter returnType, Message<?> message) {
return value instanceof Publisher ?
new ChannelSendOperator((Publisher) value, this::saveValue) :
saveValue(value);
}
private Mono<Void> saveValue(@Nullable Object value) {
this.lastReturnValue = value;
return Mono.empty();
}

View File

@@ -35,9 +35,9 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.ReactiveMessageChannel;
import org.springframework.messaging.ReactiveSubscribableChannel;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.support.DefaultReactiveMessageChannel;
import org.springframework.stereotype.Controller;
@@ -170,6 +170,26 @@ public class RSocketClientToServerIntegrationTests {
.verifyComplete();
}
@Test
public void handleWithThrownException() {
Mono<String> result = requester.route("thrown-exception").data("a").retrieveMono(String.class);
StepVerifier.create(result)
.expectNext("Invalid input error handled")
.verifyComplete();
}
@Test
public void handleWithErrorSignal() {
Mono<String> result = requester.route("error-signal").data("a").retrieveMono(String.class);
StepVerifier.create(result)
.expectNext("Invalid input error handled")
.verifyComplete();
}
@Test
public void noMatchingRoute() {
Mono<String> result = requester.route("invalid").data("anything").retrieveMono(String.class);
@@ -208,6 +228,20 @@ public class RSocketClientToServerIntegrationTests {
return payloads.delayElements(Duration.ofMillis(10)).map(payload -> payload + " async");
}
@MessageMapping("thrown-exception")
Mono<String> handleAndThrow(String payload) {
throw new IllegalArgumentException("Invalid input error");
}
@MessageMapping("error-signal")
Mono<String> handleAndReturnError(String payload) {
return Mono.error(new IllegalArgumentException("Invalid input error"));
}
@MessageExceptionHandler
Mono<String> handleException(IllegalArgumentException ex) {
return Mono.delay(Duration.ofMillis(10)).map(aLong -> ex.getMessage() + " handled");
}
}