Payload encoding/decoding and handling refinements

See gh-21987
This commit is contained in:
Rossen Stoyanchev
2019-02-14 17:04:20 -05:00
parent 33682d74c2
commit f2bb95ba7b
11 changed files with 371 additions and 253 deletions

View File

@@ -19,15 +19,14 @@ import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.config.EmbeddedValueResolver;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.Decoder;
@@ -38,18 +37,18 @@ import org.springframework.core.env.PropertySource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.ReactiveSubscribableChannel;
import org.springframework.messaging.handler.DestinationPatternsMessageCondition;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.invocation.reactive.AbstractEncoderMethodReturnValueHandler;
import org.springframework.messaging.handler.invocation.reactive.TestEncoderMethodReturnValueHandler;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.stereotype.Controller;
import static java.nio.charset.StandardCharsets.*;
import static org.junit.Assert.*;
import static org.springframework.core.io.buffer.support.DataBufferTestUtils.*;
import static org.mockito.Mockito.*;
/**
* Unit tests for {@link MessageMappingMessageHandler}.
@@ -61,51 +60,64 @@ public class MessageMappingMessageHandlerTests {
private static final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
private TestEncoderReturnValueHandler returnValueHandler;
private TestEncoderMethodReturnValueHandler returnValueHandler;
@Test
public void handleString() {
MessageMappingMessageHandler messsageHandler = initMesssageHandler();
messsageHandler.handleMessage(message("/string", "abcdef")).block(Duration.ofSeconds(5));
messsageHandler.handleMessage(message("string", "abcdef")).block(Duration.ofSeconds(5));
verifyOutputContent(Collections.singletonList("abcdef::response"));
}
@Test
public void handleMonoString() {
MessageMappingMessageHandler messsageHandler = initMesssageHandler();
messsageHandler.handleMessage(message("/monoString", "abcdef")).block(Duration.ofSeconds(5));
messsageHandler.handleMessage(message("monoString", "abcdef")).block(Duration.ofSeconds(5));
verifyOutputContent(Collections.singletonList("abcdef::response"));
}
@Test
public void handleFluxString() {
MessageMappingMessageHandler messsageHandler = initMesssageHandler();
messsageHandler.handleMessage(message("/fluxString", "abc\ndef\nghi")).block(Duration.ofSeconds(5));
messsageHandler.handleMessage(message("fluxString", "abc\ndef\nghi")).block(Duration.ofSeconds(5));
verifyOutputContent(Arrays.asList("abc::response", "def::response", "ghi::response"));
}
@Test
public void handleWithPlaceholderInMapping() {
MessageMappingMessageHandler messsageHandler = initMesssageHandler();
messsageHandler.handleMessage(message("/path123", "abcdef")).block(Duration.ofSeconds(5));
messsageHandler.handleMessage(message("path123", "abcdef")).block(Duration.ofSeconds(5));
verifyOutputContent(Collections.singletonList("abcdef::response"));
}
@Test
public void handleException() {
MessageMappingMessageHandler messsageHandler = initMesssageHandler();
messsageHandler.handleMessage(message("/exception", "abc")).block(Duration.ofSeconds(5));
messsageHandler.handleMessage(message("exception", "abc")).block(Duration.ofSeconds(5));
verifyOutputContent(Collections.singletonList("rejected::handled"));
}
@Test
public void handleErrorSignal() {
MessageMappingMessageHandler messsageHandler = initMesssageHandler();
messsageHandler.handleMessage(message("/errorSignal", "abc")).block(Duration.ofSeconds(5));
messsageHandler.handleMessage(message("errorSignal", "abc")).block(Duration.ofSeconds(5));
verifyOutputContent(Collections.singletonList("rejected::handled"));
}
@Test
public void unhandledExceptionShouldFlowThrough() {
GenericMessage<?> message = new GenericMessage<>(new Object(),
Collections.singletonMap(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "string"));
StepVerifier.create(initMesssageHandler().handleMessage(message))
.expectErrorSatisfies(ex -> assertTrue(
"Actual: " + ex.getMessage(),
ex.getMessage().startsWith("Could not resolve method parameter at index 0")))
.verify(Duration.ofSeconds(5));
}
private MessageMappingMessageHandler initMesssageHandler() {
@@ -113,7 +125,7 @@ public class MessageMappingMessageHandlerTests {
List<Encoder<?>> encoders = Collections.singletonList(CharSequenceEncoder.allMimeTypes());
ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance();
this.returnValueHandler = new TestEncoderReturnValueHandler(encoders, registry);
this.returnValueHandler = new TestEncoderMethodReturnValueHandler(encoders, registry);
PropertySource<?> source = new MapPropertySource("test", Collections.singletonMap("path", "path123"));
@@ -122,11 +134,13 @@ public class MessageMappingMessageHandlerTests {
context.registerSingleton("testController", TestController.class);
context.refresh();
MessageMappingMessageHandler messageHandler = new MessageMappingMessageHandler();
ReactiveSubscribableChannel channel = mock(ReactiveSubscribableChannel.class);
MessageMappingMessageHandler messageHandler = new MessageMappingMessageHandler(channel);
messageHandler.getReturnValueHandlerConfigurer().addCustomHandler(this.returnValueHandler);
messageHandler.setApplicationContext(context);
messageHandler.setEmbeddedValueResolver(new EmbeddedValueResolver(context.getBeanFactory()));
messageHandler.setDecoders(decoders);
messageHandler.setEncoderReturnValueHandler(this.returnValueHandler);
messageHandler.afterPropertiesSet();
return messageHandler;
@@ -134,7 +148,7 @@ public class MessageMappingMessageHandlerTests {
private Message<?> message(String destination, String... content) {
return new GenericMessage<>(
Flux.fromIterable(Arrays.stream(content).map(this::toDataBuffer).collect(Collectors.toList())),
Flux.fromIterable(Arrays.asList(content)).map(payload -> toDataBuffer(payload)),
Collections.singletonMap(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, destination));
}
@@ -143,42 +157,40 @@ public class MessageMappingMessageHandlerTests {
}
private void verifyOutputContent(List<String> expected) {
List<DataBuffer> buffers = this.returnValueHandler.getOutputContent();
assertNotNull("No output: no matching handler method?", buffers);
List<String> actual = buffers.stream().map(buffer -> dumpString(buffer, UTF_8)).collect(Collectors.toList());
assertEquals(expected, actual);
Flux<String> result = this.returnValueHandler.getContentAsStrings();
StepVerifier.create(result.collectList()).expectNext(expected).verifyComplete();
}
@Controller
static class TestController {
@MessageMapping("/string")
@MessageMapping("string")
String handleString(String payload) {
return payload + "::response";
}
@MessageMapping("/monoString")
@MessageMapping("monoString")
Mono<String> handleMonoString(Mono<String> payload) {
return payload.map(s -> s + "::response").delayElement(Duration.ofMillis(10));
}
@MessageMapping("/fluxString")
@MessageMapping("fluxString")
Flux<String> handleFluxString(Flux<String> payload) {
return payload.map(s -> s + "::response").delayElements(Duration.ofMillis(10));
}
@MessageMapping("/${path}")
@MessageMapping("${path}")
String handleWithPlaceholder(String payload) {
return payload + "::response";
}
@MessageMapping("/exception")
@MessageMapping("exception")
String handleAndThrow() {
throw new IllegalArgumentException("rejected");
}
@MessageMapping("/errorSignal")
@MessageMapping("errorSignal")
Mono<String> handleAndSignalError() {
return Mono.delay(Duration.ofMillis(10))
.flatMap(aLong -> Mono.error(new IllegalArgumentException("rejected")));
@@ -190,29 +202,4 @@ public class MessageMappingMessageHandlerTests {
}
}
private static class TestEncoderReturnValueHandler extends AbstractEncoderMethodReturnValueHandler {
@Nullable
private volatile List<DataBuffer> outputContent;
TestEncoderReturnValueHandler(List<Encoder<?>> encoders, ReactiveAdapterRegistry registry) {
super(encoders, registry);
}
@Nullable
public List<DataBuffer> getOutputContent() {
return this.outputContent;
}
@Override
protected Mono<Void> handleEncodedContent(
Flux<DataBuffer> encodedContent, MethodParameter returnType, Message<?> message) {
return encodedContent.collectList().doOnNext(buffers -> this.outputContent = buffers).then();
}
}
}

View File

@@ -101,8 +101,8 @@ public class PayloadMethodArgumentResolverTests {
public void stringMono() {
String body = "foo";
MethodParameter param = this.testMethod.arg(ResolvableType.forClassWithGenerics(Mono.class, String.class));
Mono<DataBuffer> value = Mono.delay(Duration.ofMillis(10)).map(aLong -> toDataBuffer(body));
Mono<Object> mono = resolveValue(param, value, null);
Mono<Object> mono = resolveValue(param,
Mono.delay(Duration.ofMillis(10)).map(aLong -> toDataBuffer(body)), null);
assertEquals(body, mono.block());
}
@@ -112,8 +112,8 @@ public class PayloadMethodArgumentResolverTests {
List<String> body = Arrays.asList("foo", "bar");
ResolvableType type = ResolvableType.forClassWithGenerics(Flux.class, String.class);
MethodParameter param = this.testMethod.arg(type);
Flux<Object> flux = resolveValue(param, Flux.fromIterable(body)
.delayElements(Duration.ofMillis(10)).map(value -> toDataBuffer(value + "\n")), null);
Flux<Object> flux = resolveValue(param,
Flux.fromIterable(body).delayElements(Duration.ofMillis(10)).map(this::toDataBuffer), null);
assertEquals(body, flux.collectList().block());
}
@@ -141,7 +141,7 @@ public class PayloadMethodArgumentResolverTests {
public void validateStringFlux() {
ResolvableType type = ResolvableType.forClassWithGenerics(Flux.class, String.class);
MethodParameter param = this.testMethod.arg(type);
Flux<Object> flux = resolveValue(param, Flux.just(toDataBuffer("12345678\n12345")), new TestValidator());
Flux<Object> flux = resolveValue(param, Mono.just(toDataBuffer("12345678\n12345")), new TestValidator());
StepVerifier.create(flux)
.expectNext("12345678")

View File

@@ -16,7 +16,6 @@
package org.springframework.messaging.handler.invocation.reactive;
import java.util.Collections;
import java.util.List;
import io.reactivex.Completable;
import org.junit.Test;
@@ -27,15 +26,10 @@ import reactor.test.StepVerifier;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import static java.nio.charset.StandardCharsets.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.springframework.messaging.handler.invocation.ResolvableMethod.*;
/**
@@ -49,41 +43,43 @@ public class EncoderMethodReturnValueHandlerTests {
Collections.singletonList(CharSequenceEncoder.textPlainOnly()),
ReactiveAdapterRegistry.getSharedInstance());
private final Message<?> message = mock(Message.class);
private final Message<?> message = new GenericMessage<>("shouldn't matter");
@Test
public void stringReturnValue() {
MethodParameter parameter = on(TestController.class).resolveReturnType(String.class);
this.handler.handleReturnValue("foo", parameter, message).block();
Flux<DataBuffer> result = this.handler.encodedContent;
this.handler.handleReturnValue("foo", parameter, this.message).block();
Flux<String> result = this.handler.getContentAsStrings();
StepVerifier.create(result)
.consumeNextWith(buffer -> assertEquals("foo", DataBufferTestUtils.dumpString(buffer, UTF_8)))
.verifyComplete();
StepVerifier.create(result).expectNext("foo").verifyComplete();
}
@Test
public void objectReturnValue() {
MethodParameter parameter = on(TestController.class).resolveReturnType(Object.class);
this.handler.handleReturnValue("foo", parameter, message).block();
Flux<DataBuffer> result = this.handler.encodedContent;
this.handler.handleReturnValue("foo", parameter, this.message).block();
Flux<String> result = this.handler.getContentAsStrings();
StepVerifier.create(result)
.consumeNextWith(buffer -> assertEquals("foo", DataBufferTestUtils.dumpString(buffer, UTF_8)))
.verifyComplete();
StepVerifier.create(result).expectNext("foo").verifyComplete();
}
@Test
public void fluxStringReturnValue() {
MethodParameter parameter = on(TestController.class).resolveReturnType(Flux.class, String.class);
this.handler.handleReturnValue(Flux.just("foo", "bar"), parameter, message).block();
Flux<DataBuffer> result = this.handler.encodedContent;
this.handler.handleReturnValue(Flux.just("foo", "bar"), parameter, this.message).block();
Flux<String> result = this.handler.getContentAsStrings();
StepVerifier.create(result)
.consumeNextWith(buffer -> assertEquals("foo", DataBufferTestUtils.dumpString(buffer, UTF_8)))
.consumeNextWith(buffer -> assertEquals("bar", DataBufferTestUtils.dumpString(buffer, UTF_8)))
.verifyComplete();
StepVerifier.create(result).expectNext("foo").expectNext("bar").verifyComplete();
}
@Test
public void fluxObjectReturnValue() {
MethodParameter parameter = on(TestController.class).resolveReturnType(Flux.class, Object.class);
this.handler.handleReturnValue(Flux.just("foo", "bar"), parameter, this.message).block();
Flux<String> result = this.handler.getContentAsStrings();
StepVerifier.create(result).expectNext("foo").expectNext("bar").verifyComplete();
}
@Test
@@ -91,23 +87,19 @@ public class EncoderMethodReturnValueHandlerTests {
testVoidReturnType(null, on(TestController.class).resolveReturnType(void.class));
testVoidReturnType(Mono.empty(), on(TestController.class).resolveReturnType(Mono.class, Void.class));
testVoidReturnType(Completable.complete(), on(TestController.class).resolveReturnType(Completable.class));
}
private void testVoidReturnType(@Nullable Object value, MethodParameter bodyParameter) {
this.handler.handleReturnValue(value, bodyParameter, message).block();
Flux<DataBuffer> result = this.handler.encodedContent;
this.handler.handleReturnValue(value, bodyParameter, this.message).block();
Flux<String> result = this.handler.getContentAsStrings();
StepVerifier.create(result).expectComplete().verify();
}
@Test
public void noEncoder() {
MethodParameter parameter = on(TestController.class).resolveReturnType(Object.class);
this.handler.handleReturnValue(new Object(), parameter, message).block();
Flux<DataBuffer> result = this.handler.encodedContent;
StepVerifier.create(result)
.expectErrorMessage("No encoder for method 'object' parameter -1")
StepVerifier.create(this.handler.handleReturnValue(new Object(), parameter, this.message))
.expectErrorMessage("No encoder for java.lang.Object, current value type is class java.lang.Object")
.verify();
}
@@ -121,6 +113,8 @@ public class EncoderMethodReturnValueHandlerTests {
Flux<String> fluxString() { return null; }
Flux<Object> fluxObject() { return null; }
void voidReturn() { }
Mono<Void> monoVoid() { return null; }
@@ -128,27 +122,4 @@ public class EncoderMethodReturnValueHandlerTests {
Completable completable() { return null; }
}
private static class TestEncoderMethodReturnValueHandler extends AbstractEncoderMethodReturnValueHandler {
private Flux<DataBuffer> encodedContent;
public Flux<DataBuffer> getEncodedContent() {
return this.encodedContent;
}
protected TestEncoderMethodReturnValueHandler(List<Encoder<?>> encoders, ReactiveAdapterRegistry registry) {
super(encoders, registry);
}
@Override
protected Mono<Void> handleEncodedContent(
Flux<DataBuffer> encodedContent, MethodParameter returnType, Message<?> message) {
this.encodedContent = encodedContent;
return Mono.empty();
}
}
}

View File

@@ -192,6 +192,11 @@ public class MethodMessageHandlerTests {
private PathMatcher pathMatcher = new AntPathMatcher();
public TestMethodMessageHandler() {
setHandlerPredicate(handlerType -> handlerType.getName().endsWith("Controller"));
}
@Override
protected List<? extends HandlerMethodArgumentResolver> initArgumentResolvers() {
return Collections.emptyList();
@@ -211,11 +216,6 @@ public class MethodMessageHandlerTests {
super.registerHandlerMethod(handler, method, mapping);
}
@Override
protected boolean isHandler(Class<?> handlerType) {
return handlerType.getName().endsWith("Controller");
}
@Override
protected String getMappingForMethod(Method method, Class<?> handlerType) {
String methodName = method.getName();

View File

@@ -0,0 +1,63 @@
/*
* 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.
* 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.messaging.handler.invocation.reactive;
import java.util.List;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.messaging.Message;
import static java.nio.charset.StandardCharsets.*;
/**
* Implementation of {@link AbstractEncoderMethodReturnValueHandler} for tests.
* "Handles" by storing encoded return values.
*
* @author Rossen Stoyanchev
*/
public class TestEncoderMethodReturnValueHandler extends AbstractEncoderMethodReturnValueHandler {
private Flux<DataBuffer> encodedContent;
public TestEncoderMethodReturnValueHandler(List<Encoder<?>> encoders, ReactiveAdapterRegistry registry) {
super(encoders, registry);
}
public Flux<DataBuffer> getContent() {
return this.encodedContent;
}
public Flux<String> getContentAsStrings() {
return this.encodedContent.map(buffer -> DataBufferTestUtils.dumpString(buffer, UTF_8));
}
@Override
protected Mono<Void> handleEncodedContent(
Flux<DataBuffer> encodedContent, MethodParameter returnType, Message<?> message) {
this.encodedContent = encodedContent.cache();
return this.encodedContent.then();
}
}