Reactive AbstractMessageMethodHandler

See gh-21987
This commit is contained in:
Rossen Stoyanchev
2019-01-25 11:49:06 -05:00
parent e3e1ffc986
commit 421090ca35
8 changed files with 1076 additions and 44 deletions

View File

@@ -20,7 +20,6 @@ import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -32,7 +31,6 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.MethodIntrospector;
import org.springframework.messaging.Message;
import org.springframework.messaging.converter.SimpleMessageConverter;
import org.springframework.messaging.handler.DestinationPatternsMessageCondition;
@@ -40,8 +38,8 @@ import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.util.ReflectionUtils.MethodFilter;
import static org.junit.Assert.*;
@@ -90,7 +88,7 @@ public class MethodMessageHandlerTests {
}
@Test
public void antPatchMatchWildcard() throws Exception {
public void patternMatch() throws Exception {
Method method = this.testController.getClass().getMethod("handlerPathMatchWildcard");
this.messageHandler.registerHandlerMethod(this.testController, method, "/handlerPathMatch*");
@@ -101,7 +99,7 @@ public class MethodMessageHandlerTests {
}
@Test
public void bestMatchWildcard() throws Exception {
public void bestMatch() throws Exception {
Method method = this.testController.getClass().getMethod("bestMatch");
this.messageHandler.registerHandlerMethod(this.testController, method, "/bestmatch/{foo}/path");
@@ -124,7 +122,7 @@ public class MethodMessageHandlerTests {
}
@Test
public void exceptionHandled() {
public void handleException() {
this.messageHandler.handleMessage(toDestination("/test/handlerThrowsExc"));
@@ -186,6 +184,7 @@ public class MethodMessageHandlerTests {
private PathMatcher pathMatcher = new AntPathMatcher();
public void registerHandler(Object handler) {
super.detectHandlerMethods(handler);
}
@@ -239,55 +238,24 @@ public class MethodMessageHandlerTests {
@Override
protected String getMatchingMapping(String mapping, Message<?> message) {
String destination = getLookupDestination(getDestination(message));
if (mapping.equals(destination) || this.pathMatcher.match(mapping, destination)) {
return mapping;
}
return null;
Assert.notNull(destination, "No destination");
return mapping.equals(destination) || this.pathMatcher.match(mapping, destination) ? mapping : null;
}
@Override
protected Comparator<String> getMappingComparator(final Message<?> message) {
return new Comparator<String>() {
@Override
public int compare(String info1, String info2) {
DestinationPatternsMessageCondition cond1 = new DestinationPatternsMessageCondition(info1);
DestinationPatternsMessageCondition cond2 = new DestinationPatternsMessageCondition(info2);
return cond1.compareTo(cond2, message);
}
return (info1, info2) -> {
DestinationPatternsMessageCondition cond1 = new DestinationPatternsMessageCondition(info1);
DestinationPatternsMessageCondition cond2 = new DestinationPatternsMessageCondition(info2);
return cond1.compareTo(cond2, message);
};
}
@Override
protected AbstractExceptionHandlerMethodResolver createExceptionHandlerMethodResolverFor(Class<?> beanType) {
return new TestExceptionHandlerMethodResolver(beanType);
return new TestExceptionResolver(beanType);
}
}
private static class TestExceptionHandlerMethodResolver extends AbstractExceptionHandlerMethodResolver {
public TestExceptionHandlerMethodResolver(Class<?> handlerType) {
super(initExceptionMappings(handlerType));
}
private static Map<Class<? extends Throwable>, Method> initExceptionMappings(Class<?> handlerType) {
Map<Class<? extends Throwable>, Method> result = new HashMap<>();
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHOD_FILTER)) {
for (Class<? extends Throwable> exception : getExceptionsFromMethodSignature(method)) {
result.put(exception, method);
}
}
return result;
}
public final static MethodFilter EXCEPTION_HANDLER_METHOD_FILTER = new MethodFilter() {
@Override
public boolean matches(Method method) {
return method.getName().contains("Exception");
}
};
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.MethodIntrospector;
import org.springframework.util.ReflectionUtils;
/**
* Sub-class for {@link AbstractExceptionHandlerMethodResolver} for testing.
* @author Rossen Stoyanchev
*/
public class TestExceptionResolver extends AbstractExceptionHandlerMethodResolver {
private final static ReflectionUtils.MethodFilter EXCEPTION_HANDLER_METHOD_FILTER =
method -> method.getName().matches("handle[\\w]*Exception");
public TestExceptionResolver(Class<?> handlerType) {
super(initExceptionMappings(handlerType));
}
private static Map<Class<? extends Throwable>, Method> initExceptionMappings(Class<?> handlerType) {
Map<Class<? extends Throwable>, Method> result = new HashMap<>();
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHOD_FILTER)) {
for (Class<? extends Throwable> exception : getExceptionsFromMethodSignature(method)) {
result.put(exception, method);
}
}
return result;
}
}

View File

@@ -0,0 +1,261 @@
/*
* Copyright 2002-2018 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.lang.reflect.Method;
import java.time.Duration;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import org.hamcrest.Matchers;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.DestinationPatternsMessageCondition;
import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver;
import org.springframework.messaging.handler.invocation.TestExceptionResolver;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.PathMatcher;
import static org.junit.Assert.*;
/**
* Unit tests for {@link AbstractMethodMessageHandler}.
* @author Rossen Stoyanchev
*/
public class MethodMessageHandlerTests {
@Test(expected = IllegalStateException.class)
public void duplicateMapping() {
initMethodMessageHandler(DuplicateMappingsController.class);
}
@Test
public void registeredMappings() {
TestMethodMessageHandler messageHandler = initMethodMessageHandler(TestController.class);
Map<String, HandlerMethod> mappings = messageHandler.getHandlerMethods();
assertEquals(5, mappings.keySet().size());
assertThat(mappings.keySet(), Matchers.containsInAnyOrder(
"/handleMessage", "/handleMessageWithArgument", "/handleMessageAndThrow",
"/handleMessageMatch1", "/handleMessageMatch2"));
}
@Test
public void bestMatch() throws NoSuchMethodException {
TestMethodMessageHandler handler = new TestMethodMessageHandler();
TestController controller = new TestController();
handler.register(controller, TestController.class.getMethod("handleMessageMatch1"), "/bestmatch/{foo}/path");
handler.register(controller, TestController.class.getMethod("handleMessageMatch2"), "/bestmatch/*/*");
handler.afterPropertiesSet();
Message<?> message = new GenericMessage<>("body", Collections.singletonMap(
DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/bestmatch/bar/path"));
handler.handleMessage(message).block(Duration.ofSeconds(5));
StepVerifier.create((Mono<Object>) handler.getLastReturnValue())
.expectNext("handleMessageMatch1")
.verifyComplete();
}
@Test
public void argumentResolution() {
ArgumentResolverConfigurer configurer = new ArgumentResolverConfigurer();
configurer.addCustomResolver(new StubArgumentResolver(String.class, "foo"));
TestMethodMessageHandler handler = initMethodMessageHandler(
theHandler -> theHandler.setArgumentResolverConfigurer(configurer),
TestController.class);
Message<?> message = new GenericMessage<>("body", Collections.singletonMap(
DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/handleMessageWithArgument"));
handler.handleMessage(message).block(Duration.ofSeconds(5));
StepVerifier.create((Mono<Object>) handler.getLastReturnValue())
.expectNext("handleMessageWithArgument,payload=foo")
.verifyComplete();
}
@Test
public void handleException() {
TestMethodMessageHandler handler = initMethodMessageHandler(TestController.class);
Message<?> message = new GenericMessage<>("body", Collections.singletonMap(
DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, "/handleMessageAndThrow"));
handler.handleMessage(message).block(Duration.ofSeconds(5));
StepVerifier.create((Mono<Object>) handler.getLastReturnValue())
.expectNext("handleIllegalStateException,ex=rejected")
.verifyComplete();
}
private TestMethodMessageHandler initMethodMessageHandler(Class<?>... handlerTypes) {
return initMethodMessageHandler(handler -> {}, handlerTypes);
}
private TestMethodMessageHandler initMethodMessageHandler(
Consumer<TestMethodMessageHandler> customizer, Class<?>... handlerTypes) {
StaticApplicationContext context = new StaticApplicationContext();
for (Class<?> handlerType : handlerTypes) {
String beanName = ClassUtils.getShortNameAsProperty(handlerType);
context.registerPrototype(beanName, handlerType);
}
TestMethodMessageHandler messageHandler = new TestMethodMessageHandler();
messageHandler.setApplicationContext(context);
customizer.accept(messageHandler);
messageHandler.afterPropertiesSet();
return messageHandler;
}
@SuppressWarnings("unused")
private static class TestController {
public Mono<String> handleMessage() {
return delay("handleMessage");
}
@SuppressWarnings("rawtypes")
public Mono<String> handleMessageWithArgument(String payload) {
return delay("handleMessageWithArgument,payload=" + payload);
}
public Mono<Void> handleMessageAndThrow() {
return Mono.delay(Duration.ofMillis(10))
.flatMap(aLong -> Mono.error(new IllegalStateException("rejected")));
}
public Mono<String> handleMessageMatch1() {
return delay("handleMessageMatch1");
}
public Mono<String> handleMessageMatch2() {
return delay("handleMessageMatch2");
}
public Mono<String> handleIllegalStateException(IllegalStateException ex) {
return delay("handleIllegalStateException,ex=" + ex.getMessage());
}
private Mono<String> delay(String value) {
return Mono.delay(Duration.ofMillis(10)).map(aLong -> value);
}
}
@SuppressWarnings("unused")
private static class DuplicateMappingsController {
void handleMessageFoo() { }
void handleMessageFoo(String foo) { }
}
private static class TestMethodMessageHandler extends AbstractMethodMessageHandler<String> {
private final TestReturnValueHandler returnValueHandler = new TestReturnValueHandler();
private PathMatcher pathMatcher = new AntPathMatcher();
@Override
protected List<? extends HandlerMethodArgumentResolver> initArgumentResolvers() {
return Collections.emptyList();
}
@Override
protected List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers() {
return Collections.singletonList(this.returnValueHandler);
}
@Nullable
public Object getLastReturnValue() {
return this.returnValueHandler.getLastReturnValue();
}
public void register(Object handler, Method method, String mapping) {
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();
if (methodName.startsWith("handleMessage")) {
return "/" + methodName;
}
return null;
}
@Override
protected Set<String> getDirectLookupMappings(String mapping) {
return Collections.singleton(mapping);
}
@Override
@Nullable
protected String getDestination(Message<?> message) {
return (String) message.getHeaders().get(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER);
}
@Override
protected String getMatchingMapping(String mapping, Message<?> message) {
String destination = getDestination(message);
Assert.notNull(destination, "No destination");
return mapping.equals(destination) || this.pathMatcher.match(mapping, destination) ? mapping : null;
}
@Override
protected Comparator<String> getMappingComparator(Message<?> message) {
return (info1, info2) -> {
DestinationPatternsMessageCondition cond1 = new DestinationPatternsMessageCondition(info1);
DestinationPatternsMessageCondition cond2 = new DestinationPatternsMessageCondition(info2);
return cond1.compareTo(cond2, message);
};
}
@Override
protected AbstractExceptionHandlerMethodResolver createExceptionMethodResolverFor(Class<?> beanType) {
return new TestExceptionResolver(beanType);
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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 reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
/**
* Return value handler that simply stores the last return value.
* @author Rossen Stoyanchev
*/
public class TestReturnValueHandler implements HandlerMethodReturnValueHandler {
@Nullable
private Object lastReturnValue;
@Nullable
public Object getLastReturnValue() {
return this.lastReturnValue;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return true;
}
@Override
public Mono<Void> handleReturnValue(@Nullable Object value, MethodParameter returnType, Message<?> message) {
this.lastReturnValue = value;
return Mono.empty();
}
}