Initial support for RSocket in spring-messaging

This commit is contained in:
Rossen Stoyanchev 2019-03-04 23:36:27 -05:00
commit 1ec3261062
72 changed files with 8281 additions and 313 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.
@ -105,13 +105,15 @@ public abstract class AbstractDataBufferAllocatingTestCase {
*/
protected void waitForDataBufferRelease(Duration duration) throws InterruptedException {
Instant start = Instant.now();
while (Instant.now().isBefore(start.plus(duration))) {
while (true) {
try {
verifyAllocations();
break;
}
catch (AssertionError ex) {
// ignore;
if (Instant.now().isAfter(start.plus(duration))) {
throw ex;
}
}
Thread.sleep(50);
}

View File

@ -7,12 +7,15 @@ dependencyManagement {
}
}
def rsocketVersion = "0.11.17"
dependencies {
compile(project(":spring-beans"))
compile(project(":spring-core"))
optional(project(":spring-context"))
optional(project(":spring-oxm"))
optional("io.projectreactor.netty:reactor-netty")
optional("io.rsocket:rsocket-core:${rsocketVersion}")
optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}")
optional("javax.xml.bind:jaxb-api:2.3.1")
testCompile("javax.inject:javax.inject-tck:1")
@ -24,6 +27,9 @@ dependencies {
exclude group: "org.springframework", module: "spring-context"
}
testCompile("org.apache.activemq:activemq-stomp:5.8.0")
testCompile("io.projectreactor:reactor-test")
testCompile "io.reactivex.rxjava2:rxjava:${rxjava2Version}"
testCompile("io.rsocket:rsocket-transport-netty:${rsocketVersion}")
testCompile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
testCompile("org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}")
testCompile("org.xmlunit:xmlunit-matchers:2.6.2")

View File

@ -0,0 +1,36 @@
/*
* 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;
import reactor.core.publisher.Mono;
/**
* Reactive contract for handling a {@link Message}.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
@FunctionalInterface
public interface ReactiveMessageHandler {
/**
* Handle the given message.
* @param message the message to be handled
* @return a completion {@link Mono} for the result of the message handling.
*/
Mono<Void> handleMessage(Message<?> message);
}

View File

@ -0,0 +1,160 @@
/*
* 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;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
/**
* Composite {@link MessageCondition} that delegates to other message conditions.
*
* <p>For {@link #combine} and {@link #compareTo} it is expected that the "other"
* composite contains the same number, type, and order of message conditions.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public class CompositeMessageCondition implements MessageCondition<CompositeMessageCondition> {
private final List<MessageCondition<?>> messageConditions;
public CompositeMessageCondition(MessageCondition<?>... messageConditions) {
this(Arrays.asList(messageConditions));
}
private CompositeMessageCondition(List<MessageCondition<?>> messageConditions) {
Assert.notEmpty(messageConditions, "No message conditions");
this.messageConditions = messageConditions;
}
public List<MessageCondition<?>> getMessageConditions() {
return this.messageConditions;
}
@SuppressWarnings("unchecked")
public <T extends MessageCondition<T>> T getCondition(Class<T> messageConditionType) {
for (MessageCondition<?> condition : this.messageConditions) {
if (messageConditionType.isAssignableFrom(condition.getClass())) {
return (T) condition;
}
}
throw new IllegalStateException("No condition of type: " + messageConditionType);
}
@Override
public CompositeMessageCondition combine(CompositeMessageCondition other) {
checkCompatible(other);
List<MessageCondition<?>> result = new ArrayList<>(this.messageConditions.size());
for (int i = 0; i < this.messageConditions.size(); i++) {
result.add(combine(getMessageConditions().get(i), other.getMessageConditions().get(i)));
}
return new CompositeMessageCondition(result);
}
@SuppressWarnings("unchecked")
private <T extends MessageCondition<T>> T combine(MessageCondition<?> first, MessageCondition<?> second) {
return ((T) first).combine((T) second);
}
@Override
public CompositeMessageCondition getMatchingCondition(Message<?> message) {
List<MessageCondition<?>> result = new ArrayList<>(this.messageConditions.size());
for (MessageCondition<?> condition : this.messageConditions) {
MessageCondition<?> matchingCondition = (MessageCondition<?>) condition.getMatchingCondition(message);
if (matchingCondition == null) {
return null;
}
result.add(matchingCondition);
}
return new CompositeMessageCondition(result);
}
@Override
public int compareTo(CompositeMessageCondition other, Message<?> message) {
checkCompatible(other);
List<MessageCondition<?>> otherConditions = other.getMessageConditions();
for (int i = 0; i < this.messageConditions.size(); i++) {
int result = compare (this.messageConditions.get(i), otherConditions.get(i), message);
if (result != 0) {
return result;
}
}
return 0;
}
@SuppressWarnings("unchecked")
private <T extends MessageCondition<T>> int compare(
MessageCondition<?> first, MessageCondition<?> second, Message<?> message) {
return ((T) first).compareTo((T) second, message);
}
private void checkCompatible(CompositeMessageCondition other) {
List<MessageCondition<?>> others = other.getMessageConditions();
for (int i = 0; i < this.messageConditions.size(); i++) {
if (i < others.size()) {
if (this.messageConditions.get(i).getClass().equals(others.get(i).getClass())) {
continue;
}
}
throw new IllegalArgumentException("Mismatched CompositeMessageCondition: " +
this.messageConditions + " vs " + others);
}
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof CompositeMessageCondition)) {
return false;
}
CompositeMessageCondition otherComposite = (CompositeMessageCondition) other;
checkCompatible(otherComposite);
List<MessageCondition<?>> otherConditions = otherComposite.getMessageConditions();
for (int i = 0; i < this.messageConditions.size(); i++) {
if (!this.messageConditions.get(i).equals(otherConditions.get(i))) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
int hashCode = 0;
for (MessageCondition<?> condition : this.messageConditions) {
hashCode += condition.hashCode() * 31;
}
return hashCode;
}
@Override
public String toString() {
return this.messageConditions.stream().map(Object::toString).collect(Collectors.joining(",", "{", "}"));
}
}

View File

@ -298,7 +298,7 @@ public class HandlerMethod {
*/
public String getShortLogMessage() {
int args = this.method.getParameterCount();
return getBeanType().getName() + "#" + this.method.getName() + "[" + args + " args]";
return getBeanType().getSimpleName() + "#" + this.method.getName() + "[" + args + " args]";
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 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,7 +26,6 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.ValueConstants;
@ -34,24 +33,20 @@ import org.springframework.messaging.handler.invocation.HandlerMethodArgumentRes
import org.springframework.util.ClassUtils;
/**
* Abstract base class for resolving method arguments from a named value. Message headers,
* and path variables are examples of named values. Each may have a name, a required flag,
* and a default value.
* Abstract base class to resolve method arguments from a named value, e.g.
* message headers or destination variables. Named values could have one or more
* of a name, a required flag, and a default value.
*
* <p>Subclasses define how to do the following:
* <ul>
* <li>Obtain named value information for a method parameter
* <li>Resolve names into argument values
* <li>Handle missing argument values when argument values are required
* <li>Optionally handle a resolved value
* </ul>
* <p>Subclasses only need to define specific steps such as how to obtain named
* value details from a method parameter, how to resolve to argument values, or
* how to handle missing values.
*
* <p>A default value string can contain ${...} placeholders and Spring Expression
* Language {@code #{...}} expressions. For this to work a {@link ConfigurableBeanFactory}
* must be supplied to the class constructor.
* <p>A default value string can contain ${...} placeholders and Spring
* Expression Language {@code #{...}} expressions which will be resolved if a
* {@link ConfigurableBeanFactory} is supplied to the class constructor.
*
* <p>A {@link ConversionService} may be used to apply type conversion to the resolved
* argument value if it doesn't match the method parameter type.
* <p>A {@link ConversionService} is used to to convert resolved String argument
* value to the expected target method parameter type.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
@ -61,8 +56,10 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
private final ConversionService conversionService;
@Nullable
private final ConfigurableBeanFactory configurableBeanFactory;
@Nullable
private final BeanExpressionContext expressionContext;
private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);
@ -70,28 +67,27 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
/**
* Constructor with a {@link ConversionService} and a {@link BeanFactory}.
* @param cs conversion service for converting values to match the
* target method parameter type
* @param beanFactory a bean factory to use for resolving {@code ${...}} placeholder
* and {@code #{...}} SpEL expressions in default values, or {@code null} if default
* values are not expected to contain expressions
* @param conversionService conversion service for converting String values
* to the target method parameter type
* @param beanFactory a bean factory for resolving {@code ${...}}
* placeholders and {@code #{...}} SpEL expressions in default values
*/
protected AbstractNamedValueMethodArgumentResolver(ConversionService cs,
protected AbstractNamedValueMethodArgumentResolver(ConversionService conversionService,
@Nullable ConfigurableBeanFactory beanFactory) {
this.conversionService = (cs != null ? cs : DefaultConversionService.getSharedInstance());
this.conversionService = conversionService;
this.configurableBeanFactory = beanFactory;
this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null);
}
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
Object resolvedName = resolveStringValue(namedValueInfo.name);
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
@ -100,7 +96,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
Object arg = resolveArgumentInternal(nestedParameter, message, resolvedName.toString());
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveStringValue(namedValueInfo.defaultValue);
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, message);
@ -108,7 +104,7 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveStringValue(namedValueInfo.defaultValue);
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
if (parameter != nestedParameter || !ClassUtils.isAssignableValue(parameter.getParameterType(), arg)) {
@ -134,35 +130,40 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
}
/**
* Create the {@link NamedValueInfo} object for the given method parameter. Implementations typically
* retrieve the method annotation by means of {@link MethodParameter#getParameterAnnotation(Class)}.
* Create the {@link NamedValueInfo} object for the given method parameter.
* Implementations typically retrieve the method annotation by means of
* {@link MethodParameter#getParameterAnnotation(Class)}.
* @param parameter the method parameter
* @return the named value information
*/
protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);
/**
* Create a new NamedValueInfo based on the given NamedValueInfo with sanitized values.
* Fall back on the parameter name from the class file if necessary and
* replace {@link ValueConstants#DEFAULT_NONE} with null.
*/
private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) {
String name = info.name;
if (info.name.isEmpty()) {
name = parameter.getParameterName();
if (name == null) {
throw new IllegalArgumentException("Name for argument type [" + parameter.getParameterType().getName() +
"] not available, and parameter name information not found in class file either.");
Class<?> type = parameter.getParameterType();
throw new IllegalArgumentException(
"Name for argument of type [" + type.getName() + "] not specified, " +
"and parameter name information not found in class file either.");
}
}
String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
return new NamedValueInfo(name, info.required, defaultValue);
return new NamedValueInfo(name, info.required,
ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
}
/**
* Resolve the given annotation-specified value,
* potentially containing placeholders and expressions.
*/
private Object resolveStringValue(String value) {
if (this.configurableBeanFactory == null) {
@Nullable
private Object resolveEmbeddedValuesAndExpressions(String value) {
if (this.configurableBeanFactory == null || this.expressionContext == null) {
return value;
}
String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value);
@ -186,19 +187,21 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
throws Exception;
/**
* Invoked when a named value is required, but
* {@link #resolveArgumentInternal(MethodParameter, Message, String)} returned {@code null} and
* there is no default value. Subclasses typically throw an exception in this case.
* Invoked when a value is required, but {@link #resolveArgumentInternal}
* returned {@code null} and there is no default value. Sub-classes can
* throw an appropriate exception for this case.
* @param name the name for the value
* @param parameter the method parameter
* @param parameter the target method parameter
* @param message the message being processed
*/
protected abstract void handleMissingValue(String name, MethodParameter parameter, Message<?> message);
/**
* A {@code null} results in a {@code false} value for {@code boolean}s or an
* exception for other primitives.
* One last chance to handle a possible null value.
* Specifically for booleans method parameters, use {@link Boolean#FALSE}.
* Also raise an ISE for primitive types.
*/
@Nullable
private Object handleNullValue(String name, @Nullable Object value, Class<?> paramType) {
if (value == null) {
if (Boolean.TYPE.equals(paramType)) {
@ -221,13 +224,13 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
* @param parameter the argument parameter type
* @param message the message
*/
protected void handleResolvedValue(Object arg, String name, MethodParameter parameter, Message<?> message) {
protected void handleResolvedValue(
@Nullable Object arg, String name, MethodParameter parameter, Message<?> message) {
}
/**
* Represents the information about a named value, including name, whether it's
* required and a default value.
* Represents a named value declaration.
*/
protected static class NamedValueInfo {
@ -235,9 +238,10 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle
private final boolean required;
@Nullable
private final String defaultValue;
protected NamedValueInfo(String name, boolean required, String defaultValue) {
protected NamedValueInfo(String name, boolean required, @Nullable String defaultValue) {
this.name = name;
this.required = required;
this.defaultValue = defaultValue;

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,11 +26,13 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.lang.Nullable;
import org.springframework.messaging.converter.GenericMessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import org.springframework.util.Assert;
import org.springframework.validation.Validator;
/**
@ -60,15 +62,19 @@ public class DefaultMessageHandlerMethodFactory
private ConversionService conversionService = new DefaultFormattingConversionService();
@Nullable
private MessageConverter messageConverter;
@Nullable
private Validator validator;
@Nullable
private List<HandlerMethodArgumentResolver> customArgumentResolvers;
private final HandlerMethodArgumentResolverComposite argumentResolvers =
new HandlerMethodArgumentResolverComposite();
@Nullable
private BeanFactory beanFactory;
@ -114,6 +120,7 @@ public class DefaultMessageHandlerMethodFactory
* the ones configured by default. This is an advanced option. For most use cases
* it should be sufficient to use {@link #setCustomArgumentResolvers(java.util.List)}.
*/
@SuppressWarnings("ConstantConditions")
public void setArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
if (argumentResolvers == null) {
this.argumentResolvers.clear();
@ -151,11 +158,11 @@ public class DefaultMessageHandlerMethodFactory
protected List<HandlerMethodArgumentResolver> initArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
ConfigurableBeanFactory cbf = (this.beanFactory instanceof ConfigurableBeanFactory ?
ConfigurableBeanFactory beanFactory = (this.beanFactory instanceof ConfigurableBeanFactory ?
(ConfigurableBeanFactory) this.beanFactory : null);
// Annotation-based argument resolution
resolvers.add(new HeaderMethodArgumentResolver(this.conversionService, cbf));
resolvers.add(new HeaderMethodArgumentResolver(this.conversionService, beanFactory));
resolvers.add(new HeadersMethodArgumentResolver());
// Type-based argument resolution
@ -164,6 +171,8 @@ public class DefaultMessageHandlerMethodFactory
if (this.customArgumentResolvers != null) {
resolvers.addAll(this.customArgumentResolvers);
}
Assert.notNull(this.messageConverter, "MessageConverter not configured");
resolvers.add(new PayloadArgumentResolver(this.messageConverter, this.validator));
return resolvers;

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.
@ -23,28 +23,26 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.ValueConstants;
import org.springframework.util.Assert;
/**
* Resolves method parameters annotated with
* {@link org.springframework.messaging.handler.annotation.DestinationVariable @DestinationVariable}.
* Resolve for {@link DestinationVariable @DestinationVariable} method parameters.
*
* @author Brian Clozel
* @since 4.0
*/
public class DestinationVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
/**
* The name of the header used to for template variables.
*/
/** The name of the header used to for template variables. */
public static final String DESTINATION_TEMPLATE_VARIABLES_HEADER =
DestinationVariableMethodArgumentResolver.class.getSimpleName() + ".templateVariables";
public DestinationVariableMethodArgumentResolver(ConversionService cs) {
super(cs, null);
public DestinationVariableMethodArgumentResolver(ConversionService conversionService) {
super(conversionService, null);
}
@ -55,26 +53,24 @@ public class DestinationVariableMethodArgumentResolver extends AbstractNamedValu
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
DestinationVariable annotation = parameter.getParameterAnnotation(DestinationVariable.class);
Assert.state(annotation != null, "No DestinationVariable annotation");
return new DestinationVariableNamedValueInfo(annotation);
DestinationVariable annot = parameter.getParameterAnnotation(DestinationVariable.class);
Assert.state(annot != null, "No DestinationVariable annotation");
return new DestinationVariableNamedValueInfo(annot);
}
@Override
@Nullable
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name)
throws Exception {
@SuppressWarnings("unchecked")
Map<String, String> vars =
(Map<String, String>) message.getHeaders().get(DESTINATION_TEMPLATE_VARIABLES_HEADER);
return (vars != null ? vars.get(name) : null);
@SuppressWarnings("unchecked")
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name) {
MessageHeaders headers = message.getHeaders();
Map<String, String> vars = (Map<String, String>) headers.get(DESTINATION_TEMPLATE_VARIABLES_HEADER);
return vars != null ? vars.get(name) : null;
}
@Override
protected void handleMissingValue(String name, MethodParameter parameter, Message<?> message) {
throw new MessageHandlingException(message, "Missing path template variable '" + name +
"' for method parameter type [" + parameter.getParameterType() + "]");
throw new MessageHandlingException(message, "Missing path template variable '" + name + "' " +
"for method parameter type [" + parameter.getParameterType() + "]");
}

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.
@ -33,18 +33,25 @@ import org.springframework.messaging.support.NativeMessageHeaderAccessor;
import org.springframework.util.Assert;
/**
* Resolves method parameters annotated with {@link Header @Header}.
* Resolver for {@link Header @Header} arguments. Headers are resolved from
* either the top-level header map or the nested
* {@link NativeMessageHeaderAccessor native} header map.
*
* @author Rossen Stoyanchev
* @since 4.0
*
* @see HeadersMethodArgumentResolver
* @see NativeMessageHeaderAccessor
*/
public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
private static final Log logger = LogFactory.getLog(HeaderMethodArgumentResolver.class);
public HeaderMethodArgumentResolver(ConversionService cs, ConfigurableBeanFactory beanFactory) {
super(cs, beanFactory);
public HeaderMethodArgumentResolver(
ConversionService conversionService, @Nullable ConfigurableBeanFactory beanFactory) {
super(conversionService, beanFactory);
}
@ -55,9 +62,9 @@ public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgume
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
Header annotation = parameter.getParameterAnnotation(Header.class);
Assert.state(annotation != null, "No Header annotation");
return new HeaderNamedValueInfo(annotation);
Header annot = parameter.getParameterAnnotation(Header.class);
Assert.state(annot != null, "No Header annotation");
return new HeaderNamedValueInfo(annot);
}
@Override
@ -70,10 +77,9 @@ public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgume
if (headerValue != null && nativeHeaderValue != null) {
if (logger.isDebugEnabled()) {
logger.debug("Message headers contain two values for the same header '" + name + "', " +
"one in the top level header map and a second in the nested map with native headers. " +
"Using the value from top level map. " +
"Use 'nativeHeader.myHeader' to resolve to the value from the nested native header map.");
logger.debug("A value was found for '" + name + "', in both the top level header map " +
"and also in the nested map for native headers. Using the value from top level map. " +
"Use 'nativeHeader.myHeader' to resolve the native header.");
}
}
@ -94,9 +100,9 @@ public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgume
}
@SuppressWarnings("unchecked")
@Nullable
private Map<String, List<String>> getNativeHeaders(Message<?> message) {
return (Map<String, List<String>>) message.getHeaders().get(
NativeMessageHeaderAccessor.NATIVE_HEADERS);
return (Map<String, List<String>>) message.getHeaders().get(NativeMessageHeaderAccessor.NATIVE_HEADERS);
}
@Override

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.
@ -29,12 +29,11 @@ import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.util.ReflectionUtils;
/**
* {@link HandlerMethodArgumentResolver} for header method parameters. Resolves the
* following method parameters:
* Argument resolver for headers. Resolves the following method parameters:
* <ul>
* <li>Parameters assignable to {@link Map} annotated with {@link Headers @Headers}
* <li>Parameters of type {@link MessageHeaders}
* <li>Parameters assignable to {@link MessageHeaderAccessor}
* <li>{@link Headers @Headers} {@link Map}
* <li>{@link MessageHeaders}
* <li>{@link MessageHeaderAccessor}
* </ul>
*
* @author Rossen Stoyanchev
@ -58,7 +57,7 @@ public class HeadersMethodArgumentResolver implements HandlerMethodArgumentResol
}
else if (MessageHeaderAccessor.class == paramType) {
MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class);
return (accessor != null ? accessor : new MessageHeaderAccessor(message));
return accessor != null ? accessor : new MessageHeaderAccessor(message);
}
else if (MessageHeaderAccessor.class.isAssignableFrom(paramType)) {
MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class);
@ -75,9 +74,8 @@ public class HeadersMethodArgumentResolver implements HandlerMethodArgumentResol
}
}
else {
throw new IllegalStateException(
"Unexpected method parameter type " + paramType + "in method " + parameter.getMethod() + ". "
+ "@Headers method arguments must be assignable to java.util.Map.");
throw new IllegalStateException("Unexpected parameter of type " + paramType +
" in method " + parameter.getMethod() + ". ");
}
}

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.
@ -43,6 +43,7 @@ import org.springframework.util.StringUtils;
*/
public class MessageMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Nullable
private final MessageConverter converter;

View File

@ -55,6 +55,7 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver {
private final MessageConverter converter;
@Nullable
private final Validator validator;
private final boolean useDefaultResolution;
@ -76,7 +77,7 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver {
* @param messageConverter the MessageConverter to use (required)
* @param validator the Validator to use (optional)
*/
public PayloadArgumentResolver(MessageConverter messageConverter, Validator validator) {
public PayloadArgumentResolver(MessageConverter messageConverter, @Nullable Validator validator) {
this(messageConverter, validator, true);
}
@ -89,7 +90,7 @@ public class PayloadArgumentResolver implements HandlerMethodArgumentResolver {
* all parameters; if "false" then only arguments with the {@code @Payload}
* annotation are supported.
*/
public PayloadArgumentResolver(MessageConverter messageConverter, Validator validator,
public PayloadArgumentResolver(MessageConverter messageConverter, @Nullable Validator validator,
boolean useDefaultResolution) {
Assert.notNull(messageConverter, "MessageConverter must not be null");

View File

@ -1,4 +1,9 @@
/**
* Support classes for working with annotated message-handling methods.
*/
@NonNullApi
@NonNullFields
package org.springframework.messaging.handler.annotation.support;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@ -0,0 +1,235 @@
/*
* 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.annotation.support.reactive;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.BeanExpressionResolver;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.ValueConstants;
import org.springframework.messaging.handler.invocation.reactive.SyncHandlerMethodArgumentResolver;
import org.springframework.util.ClassUtils;
/**
* Abstract base class to resolve method arguments from a named value, e.g.
* message headers or destination variables. Named values could have one or more
* of a name, a required flag, and a default value.
*
* <p>Subclasses only need to define specific steps such as how to obtain named
* value details from a method parameter, how to resolve to argument values, or
* how to handle missing values.
*
* <p>A default value string can contain ${...} placeholders and Spring
* Expression Language {@code #{...}} expressions which will be resolved if a
* {@link ConfigurableBeanFactory} is supplied to the class constructor.
*
* <p>A {@link ConversionService} is used to to convert resolved String argument
* value to the expected target method parameter type.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public abstract class AbstractNamedValueMethodArgumentResolver implements SyncHandlerMethodArgumentResolver {
private final ConversionService conversionService;
@Nullable
private final ConfigurableBeanFactory configurableBeanFactory;
@Nullable
private final BeanExpressionContext expressionContext;
private final Map<MethodParameter, NamedValueInfo> namedValueInfoCache = new ConcurrentHashMap<>(256);
/**
* Constructor with a {@link ConversionService} and a {@link BeanFactory}.
* @param conversionService conversion service for converting String values
* to the target method parameter type
* @param beanFactory a bean factory for resolving {@code ${...}}
* placeholders and {@code #{...}} SpEL expressions in default values
*/
protected AbstractNamedValueMethodArgumentResolver(ConversionService conversionService,
@Nullable ConfigurableBeanFactory beanFactory) {
this.conversionService = conversionService;
this.configurableBeanFactory = beanFactory;
this.expressionContext = (beanFactory != null ? new BeanExpressionContext(beanFactory, null) : null);
}
@Override
public Object resolveArgumentValue(MethodParameter parameter, Message<?> message) {
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
Object arg = resolveArgumentInternal(nestedParameter, message, resolvedName.toString());
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, message);
}
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
if (parameter != nestedParameter || !ClassUtils.isAssignableValue(parameter.getParameterType(), arg)) {
arg = this.conversionService.convert(arg, TypeDescriptor.forObject(arg), new TypeDescriptor(parameter));
}
return arg;
}
/**
* Obtain the named value for the given method parameter.
*/
private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
if (namedValueInfo == null) {
namedValueInfo = createNamedValueInfo(parameter);
namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
this.namedValueInfoCache.put(parameter, namedValueInfo);
}
return namedValueInfo;
}
/**
* Create the {@link NamedValueInfo} object for the given method parameter.
* Implementations typically retrieve the method annotation by means of
* {@link MethodParameter#getParameterAnnotation(Class)}.
* @param parameter the method parameter
* @return the named value information
*/
protected abstract NamedValueInfo createNamedValueInfo(MethodParameter parameter);
/**
* Fall back on the parameter name from the class file if necessary and
* replace {@link ValueConstants#DEFAULT_NONE} with null.
*/
private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) {
String name = info.name;
if (info.name.isEmpty()) {
name = parameter.getParameterName();
if (name == null) {
Class<?> type = parameter.getParameterType();
throw new IllegalArgumentException(
"Name for argument of type [" + type.getName() + "] not specified, " +
"and parameter name information not found in class file either.");
}
}
return new NamedValueInfo(name, info.required,
ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue);
}
/**
* Resolve the given annotation-specified value,
* potentially containing placeholders and expressions.
*/
@Nullable
private Object resolveEmbeddedValuesAndExpressions(String value) {
if (this.configurableBeanFactory == null || this.expressionContext == null) {
return value;
}
String placeholdersResolved = this.configurableBeanFactory.resolveEmbeddedValue(value);
BeanExpressionResolver exprResolver = this.configurableBeanFactory.getBeanExpressionResolver();
if (exprResolver == null) {
return value;
}
return exprResolver.evaluate(placeholdersResolved, this.expressionContext);
}
/**
* Resolves the given parameter type and value name into an argument value.
* @param parameter the method parameter to resolve to an argument value
* @param message the current request
* @param name the name of the value being resolved
* @return the resolved argument. May be {@code null}
*/
@Nullable
protected abstract Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name);
/**
* Invoked when a value is required, but {@link #resolveArgumentInternal}
* returned {@code null} and there is no default value. Sub-classes can
* throw an appropriate exception for this case.
* @param name the name for the value
* @param parameter the target method parameter
* @param message the message being processed
*/
protected abstract void handleMissingValue(String name, MethodParameter parameter, Message<?> message);
/**
* One last chance to handle a possible null value.
* Specifically for booleans method parameters, use {@link Boolean#FALSE}.
* Also raise an ISE for primitive types.
*/
@Nullable
private Object handleNullValue(String name, @Nullable Object value, Class<?> paramType) {
if (value == null) {
if (Boolean.TYPE.equals(paramType)) {
return Boolean.FALSE;
}
else if (paramType.isPrimitive()) {
throw new IllegalStateException("Optional " + paramType + " parameter '" + name +
"' is present but cannot be translated into a null value due to being " +
"declared as a primitive type. Consider declaring it as object wrapper " +
"for the corresponding primitive type.");
}
}
return value;
}
/**
* Represents a named value declaration.
*/
protected static class NamedValueInfo {
private final String name;
private final boolean required;
@Nullable
private final String defaultValue;
protected NamedValueInfo(String name, boolean required, @Nullable String defaultValue) {
this.name = name;
this.required = required;
this.defaultValue = defaultValue;
}
}
}

View File

@ -0,0 +1,84 @@
/*
* 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.annotation.support.reactive;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.ValueConstants;
import org.springframework.util.Assert;
/**
* Resolve for {@link DestinationVariable @DestinationVariable} method parameters.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public class DestinationVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
/** The name of the header used to for template variables. */
public static final String DESTINATION_TEMPLATE_VARIABLES_HEADER =
DestinationVariableMethodArgumentResolver.class.getSimpleName() + ".templateVariables";
public DestinationVariableMethodArgumentResolver(ConversionService conversionService) {
super(conversionService, null);
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(DestinationVariable.class);
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
DestinationVariable annot = parameter.getParameterAnnotation(DestinationVariable.class);
Assert.state(annot != null, "No DestinationVariable annotation");
return new DestinationVariableNamedValueInfo(annot);
}
@Override
@Nullable
@SuppressWarnings("unchecked")
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name) {
MessageHeaders headers = message.getHeaders();
Map<String, String> vars = (Map<String, String>) headers.get(DESTINATION_TEMPLATE_VARIABLES_HEADER);
return vars != null ? vars.get(name) : null;
}
@Override
protected void handleMissingValue(String name, MethodParameter parameter, Message<?> message) {
throw new MessageHandlingException(message, "Missing path template variable '" + name + "' " +
"for method parameter type [" + parameter.getParameterType() + "]");
}
private static final class DestinationVariableNamedValueInfo extends NamedValueInfo {
private DestinationVariableNamedValueInfo(DestinationVariable annotation) {
super(annotation.value(), true, ValueConstants.DEFAULT_NONE);
}
}
}

View File

@ -0,0 +1,121 @@
/*
* 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.annotation.support.reactive;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.support.NativeMessageHeaderAccessor;
import org.springframework.util.Assert;
/**
* Resolver for {@link Header @Header} arguments. Headers are resolved from
* either the top-level header map or the nested
* {@link NativeMessageHeaderAccessor native} header map.
*
* @author Rossen Stoyanchev
* @since 5.2
*
* @see HeadersMethodArgumentResolver
* @see NativeMessageHeaderAccessor
*/
public class HeaderMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
private static final Log logger = LogFactory.getLog(HeaderMethodArgumentResolver.class);
public HeaderMethodArgumentResolver(
ConversionService conversionService, @Nullable ConfigurableBeanFactory beanFactory) {
super(conversionService, beanFactory);
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(Header.class);
}
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
Header annot = parameter.getParameterAnnotation(Header.class);
Assert.state(annot != null, "No Header annotation");
return new HeaderNamedValueInfo(annot);
}
@Override
@Nullable
protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name) {
Object headerValue = message.getHeaders().get(name);
Object nativeHeaderValue = getNativeHeaderValue(message, name);
if (headerValue != null && nativeHeaderValue != null) {
if (logger.isDebugEnabled()) {
logger.debug("A value was found for '" + name + "', in both the top level header map " +
"and also in the nested map for native headers. Using the value from top level map. " +
"Use 'nativeHeader.myHeader' to resolve the native header.");
}
}
return (headerValue != null ? headerValue : nativeHeaderValue);
}
@Nullable
private Object getNativeHeaderValue(Message<?> message, String name) {
Map<String, List<String>> nativeHeaders = getNativeHeaders(message);
if (name.startsWith("nativeHeaders.")) {
name = name.substring("nativeHeaders.".length());
}
if (nativeHeaders == null || !nativeHeaders.containsKey(name)) {
return null;
}
List<?> nativeHeaderValues = nativeHeaders.get(name);
return (nativeHeaderValues.size() == 1 ? nativeHeaderValues.get(0) : nativeHeaderValues);
}
@SuppressWarnings("unchecked")
@Nullable
private Map<String, List<String>> getNativeHeaders(Message<?> message) {
return (Map<String, List<String>>) message.getHeaders().get(NativeMessageHeaderAccessor.NATIVE_HEADERS);
}
@Override
protected void handleMissingValue(String headerName, MethodParameter parameter, Message<?> message) {
throw new MessageHandlingException(message, "Missing header '" + headerName +
"' for method parameter type [" + parameter.getParameterType() + "]");
}
private static final class HeaderNamedValueInfo extends NamedValueInfo {
private HeaderNamedValueInfo(Header annotation) {
super(annotation.name(), annotation.required(), annotation.defaultValue());
}
}
}

View File

@ -0,0 +1,82 @@
/*
* 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.annotation.support.reactive;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.invocation.reactive.SyncHandlerMethodArgumentResolver;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.util.ReflectionUtils;
/**
* Argument resolver for headers. Resolves the following method parameters:
* <ul>
* <li>{@link Headers @Headers} {@link Map}
* <li>{@link MessageHeaders}
* <li>{@link MessageHeaderAccessor}
* </ul>
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public class HeadersMethodArgumentResolver implements SyncHandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return ((parameter.hasParameterAnnotation(Headers.class) && Map.class.isAssignableFrom(paramType)) ||
MessageHeaders.class == paramType || MessageHeaderAccessor.class.isAssignableFrom(paramType));
}
@Override
@Nullable
public Object resolveArgumentValue(MethodParameter parameter, Message<?> message) {
Class<?> paramType = parameter.getParameterType();
if (Map.class.isAssignableFrom(paramType)) {
return message.getHeaders();
}
else if (MessageHeaderAccessor.class == paramType) {
MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class);
return accessor != null ? accessor : new MessageHeaderAccessor(message);
}
else if (MessageHeaderAccessor.class.isAssignableFrom(paramType)) {
MessageHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, MessageHeaderAccessor.class);
if (accessor != null && paramType.isAssignableFrom(accessor.getClass())) {
return accessor;
}
else {
Method method = ReflectionUtils.findMethod(paramType, "wrap", Message.class);
if (method == null) {
throw new IllegalStateException(
"Cannot create accessor of type " + paramType + " for message " + message);
}
return ReflectionUtils.invokeMethod(method, null, message);
}
}
else {
throw new IllegalStateException("Unexpected parameter of type " + paramType +
" in method " + parameter.getMethod() + ". ");
}
}
}

View File

@ -0,0 +1,313 @@
/*
* 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.annotation.support.reactive;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.codec.Decoder;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.CompositeMessageCondition;
import org.springframework.messaging.handler.DestinationPatternsMessageCondition;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.support.AnnotationExceptionHandlerMethodResolver;
import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver;
import org.springframework.messaging.handler.invocation.reactive.AbstractEncoderMethodReturnValueHandler;
import org.springframework.messaging.handler.invocation.reactive.AbstractMethodMessageHandler;
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodReturnValueHandler;
import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringValueResolver;
import org.springframework.validation.Validator;
/**
* Extension of {@link AbstractMethodMessageHandler} for reactive, non-blocking
* handling of messages via {@link MessageMapping @MessageMapping} methods.
* By default such methods are detected in {@code @Controller} Spring beans but
* that can be changed via {@link #setHandlerPredicate(Predicate)}.
*
* <p>Payloads for incoming messages are decoded through the configured
* {@link #setDecoders(List)} decoders, with the help of
* {@link PayloadMethodArgumentResolver}.
*
* <p>There is no default handling for return values but
* {@link #setReturnValueHandlerConfigurer} can be used to configure custom
* return value handlers. Sub-classes may also override
* {@link #initReturnValueHandlers()} to set up default return value handlers.
*
* @author Rossen Stoyanchev
* @since 5.2
* @see AbstractEncoderMethodReturnValueHandler
*/
public class MessageMappingMessageHandler extends AbstractMethodMessageHandler<CompositeMessageCondition>
implements EmbeddedValueResolverAware {
@Nullable
private Predicate<Class<?>> handlerPredicate =
beanType -> AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
private final List<Decoder<?>> decoders = new ArrayList<>();
@Nullable
private Validator validator;
private PathMatcher pathMatcher;
private ConversionService conversionService = new DefaultFormattingConversionService();
@Nullable
private StringValueResolver valueResolver;
public MessageMappingMessageHandler() {
this.pathMatcher = new AntPathMatcher();
((AntPathMatcher) this.pathMatcher).setPathSeparator(".");
}
/**
* Manually configure handlers to check for {@code @MessageMapping} methods.
* <p><strong>Note:</strong> the given handlers are not required to be
* annotated with {@code @Controller}. Consider also using
* {@link #setAutoDetectDisabled()} if the intent is to use these handlers
* instead of, and not in addition to {@code @Controller} classes. Or
* alternatively use {@link #setHandlerPredicate(Predicate)} to select a
* different set of beans based on a different criteria.
* @param handlers the handlers to register
* @see #setAutoDetectDisabled()
* @see #setHandlerPredicate(Predicate)
*/
public void setHandlers(List<Object> handlers) {
for (Object handler : handlers) {
detectHandlerMethods(handler);
}
// Disable auto-detection..
this.handlerPredicate = null;
}
/**
* Configure the predicate to use for selecting which Spring beans to check
* for {@code @MessageMapping} methods. When set to {@code null},
* auto-detection is turned off which is what
* {@link #setAutoDetectDisabled()} does internally.
* <p>The predicate used by default selects {@code @Controller} classes.
* @see #setHandlers(List)
* @see #setAutoDetectDisabled()
*/
public void setHandlerPredicate(@Nullable Predicate<Class<?>> handlerPredicate) {
this.handlerPredicate = handlerPredicate;
}
/**
* Return the {@link #setHandlerPredicate configured} handler predicate.
*/
@Nullable
public Predicate<Class<?>> getHandlerPredicate() {
return this.handlerPredicate;
}
/**
* Disable auto-detection of {@code @MessageMapping} methods, e.g. in
* {@code @Controller}s, by setting {@link #setHandlerPredicate(Predicate)
* setHandlerPredicate(null)}.
*/
public void setAutoDetectDisabled() {
this.handlerPredicate = null;
}
/**
* Configure the decoders to use for incoming payloads.
*/
public void setDecoders(List<? extends Decoder<?>> decoders) {
this.decoders.addAll(decoders);
}
/**
* Return the configured decoders.
*/
public List<? extends Decoder<?>> getDecoders() {
return this.decoders;
}
/**
* Set the Validator instance used for validating {@code @Payload} arguments.
* @see org.springframework.validation.annotation.Validated
* @see PayloadMethodArgumentResolver
*/
public void setValidator(@Nullable Validator validator) {
this.validator = validator;
}
/**
* Return the configured Validator instance.
*/
@Nullable
public Validator getValidator() {
return this.validator;
}
/**
* Set the PathMatcher implementation to use for matching destinations
* against configured destination patterns.
* <p>By default, {@link AntPathMatcher} is used with separator set to ".".
*/
public void setPathMatcher(PathMatcher pathMatcher) {
Assert.notNull(pathMatcher, "PathMatcher must not be null");
this.pathMatcher = pathMatcher;
}
/**
* Return the PathMatcher implementation to use for matching destinations.
*/
public PathMatcher getPathMatcher() {
return this.pathMatcher;
}
/**
* Configure a {@link ConversionService} to use for type conversion of
* String based values, e.g. in destination variables or headers.
* <p>By default {@link DefaultFormattingConversionService} is used.
* @param conversionService the conversion service to use
*/
public void setConversionService(ConversionService conversionService) {
this.conversionService = conversionService;
}
/**
* Return the configured ConversionService.
*/
public ConversionService getConversionService() {
return this.conversionService;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.valueResolver = resolver;
}
@Override
protected List<? extends HandlerMethodArgumentResolver> initArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
ApplicationContext context = getApplicationContext();
ConfigurableBeanFactory beanFactory = (context instanceof ConfigurableApplicationContext ?
((ConfigurableApplicationContext) context).getBeanFactory() : null);
// Annotation-based resolvers
resolvers.add(new HeaderMethodArgumentResolver(this.conversionService, beanFactory));
resolvers.add(new HeadersMethodArgumentResolver());
resolvers.add(new DestinationVariableMethodArgumentResolver(this.conversionService));
// Custom resolvers
resolvers.addAll(getArgumentResolverConfigurer().getCustomResolvers());
// Catch-all
resolvers.add(new PayloadMethodArgumentResolver(
this.decoders, this.validator, getReactiveAdapterRegistry(), true));
return resolvers;
}
@Override
protected List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers() {
return Collections.emptyList();
}
@Override
protected Predicate<Class<?>> initHandlerPredicate() {
return this.handlerPredicate;
}
@Override
protected CompositeMessageCondition getMappingForMethod(Method method, Class<?> handlerType) {
CompositeMessageCondition methodCondition = getCondition(method);
if (methodCondition != null) {
CompositeMessageCondition typeCondition = getCondition(handlerType);
if (typeCondition != null) {
return typeCondition.combine(methodCondition);
}
}
return methodCondition;
}
@Nullable
private CompositeMessageCondition getCondition(AnnotatedElement element) {
MessageMapping annot = AnnotatedElementUtils.findMergedAnnotation(element, MessageMapping.class);
if (annot == null || annot.value().length == 0) {
return null;
}
String[] destinations = annot.value();
if (this.valueResolver != null) {
destinations = Arrays.stream(annot.value())
.map(s -> this.valueResolver.resolveStringValue(s))
.toArray(String[]::new);
}
return new CompositeMessageCondition(new DestinationPatternsMessageCondition(destinations, this.pathMatcher));
}
@Override
protected Set<String> getDirectLookupMappings(CompositeMessageCondition mapping) {
Set<String> result = new LinkedHashSet<>();
for (String pattern : mapping.getCondition(DestinationPatternsMessageCondition.class).getPatterns()) {
if (!this.pathMatcher.isPattern(pattern)) {
result.add(pattern);
}
}
return result;
}
@Override
protected String getDestination(Message<?> message) {
return (String) message.getHeaders().get(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER);
}
@Override
protected CompositeMessageCondition getMatchingMapping(CompositeMessageCondition mapping, Message<?> message) {
return mapping.getMatchingCondition(message);
}
@Override
protected Comparator<CompositeMessageCondition> getMappingComparator(Message<?> message) {
return (info1, info2) -> info1.compareTo(info2, message);
}
@Override
protected AbstractExceptionHandlerMethodResolver createExceptionMethodResolverFor(Class<?> beanType) {
return new AnnotationExceptionHandlerMethodResolver(beanType);
}
}

View File

@ -0,0 +1,302 @@
/*
* 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.annotation.support.reactive;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.Conventions;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException;
import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException;
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.SmartValidator;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
/**
* A resolver to extract and decode the payload of a message using a
* {@link Decoder}, where the payload is expected to be a {@link Publisher} of
* {@link DataBuffer DataBuffer}.
*
* <p>Validation is applied if the method argument is annotated with
* {@code @javax.validation.Valid} or
* {@link org.springframework.validation.annotation.Validated}. Validation
* failure results in an {@link MethodArgumentNotValidException}.
*
* <p>This resolver should be ordered last if {@link #useDefaultResolution} is
* set to {@code true} since in that case it supports all types and does not
* require the presence of {@link Payload}.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public class PayloadMethodArgumentResolver implements HandlerMethodArgumentResolver {
protected final Log logger = LogFactory.getLog(getClass());
private final List<Decoder<?>> decoders;
@Nullable
private final Validator validator;
private final ReactiveAdapterRegistry adapterRegistry;
private final boolean useDefaultResolution;
public PayloadMethodArgumentResolver(List<? extends Decoder<?>> decoders, @Nullable Validator validator,
@Nullable ReactiveAdapterRegistry registry, boolean useDefaultResolution) {
Assert.isTrue(!CollectionUtils.isEmpty(decoders), "At least one Decoder is required.");
this.decoders = Collections.unmodifiableList(new ArrayList<>(decoders));
this.validator = validator;
this.adapterRegistry = registry != null ? registry : ReactiveAdapterRegistry.getSharedInstance();
this.useDefaultResolution = useDefaultResolution;
}
/**
* Return a read-only list of the configured decoders.
*/
public List<Decoder<?>> getDecoders() {
return this.decoders;
}
/**
* Return the configured validator, if any.
*/
@Nullable
public Validator getValidator() {
return this.validator;
}
/**
* Return the configured {@link ReactiveAdapterRegistry}.
*/
public ReactiveAdapterRegistry getAdapterRegistry() {
return this.adapterRegistry;
}
/**
* Whether this resolver is configured to use default resolution, i.e.
* works for any argument type regardless of whether {@code @Payload} is
* present or not.
*/
public boolean isUseDefaultResolution() {
return this.useDefaultResolution;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(Payload.class) || this.useDefaultResolution;
}
/**
* Decode the content of the given message payload through a compatible
* {@link Decoder}.
*
* <p>Validation is applied if the method argument is annotated with
* {@code @javax.validation.Valid} or
* {@link org.springframework.validation.annotation.Validated}. Validation
* failure results in an {@link MethodArgumentNotValidException}.
*
* @param parameter the target method argument that we are decoding to
* @param message the message from which the content was extracted
* @return a Mono with the result of argument resolution
*
* @see #extractContent(MethodParameter, Message)
* @see #getMimeType(Message)
*/
@Override
public final Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) {
Payload ann = parameter.getParameterAnnotation(Payload.class);
if (ann != null && StringUtils.hasText(ann.expression())) {
throw new IllegalStateException("@Payload SpEL expressions not supported by this resolver");
}
MimeType mimeType = getMimeType(message);
mimeType = mimeType != null ? mimeType : MimeTypeUtils.APPLICATION_OCTET_STREAM;
Flux<DataBuffer> content = extractContent(parameter, message);
return decodeContent(parameter, message, ann == null || ann.required(), content, mimeType);
}
@SuppressWarnings("unchecked")
private Flux<DataBuffer> extractContent(MethodParameter parameter, Message<?> message) {
Object payload = message.getPayload();
if (payload instanceof DataBuffer) {
return Flux.just((DataBuffer) payload);
}
if (payload instanceof Publisher) {
return Flux.from((Publisher<?>) payload).map(value -> {
if (value instanceof DataBuffer) {
return (DataBuffer) value;
}
String className = value.getClass().getName();
throw getUnexpectedPayloadError(message, parameter, "Publisher<" + className + ">");
});
}
return Flux.error(getUnexpectedPayloadError(message, parameter, payload.getClass().getName()));
}
private MethodArgumentResolutionException getUnexpectedPayloadError(
Message<?> message, MethodParameter parameter, String actualType) {
return new MethodArgumentResolutionException(message, parameter,
"Expected DataBuffer or Publisher<DataBuffer> for the Message payload, actual: " + actualType);
}
/**
* Return the mime type for the content. By default this method checks the
* {@link MessageHeaders#CONTENT_TYPE} header expecting to find a
* {@link MimeType} value or a String to parse to a {@link MimeType}.
* @param message the input message
*/
@Nullable
protected MimeType getMimeType(Message<?> message) {
Object headerValue = message.getHeaders().get(MessageHeaders.CONTENT_TYPE);
if (headerValue == null) {
return null;
}
else if (headerValue instanceof String) {
return MimeTypeUtils.parseMimeType((String) headerValue);
}
else if (headerValue instanceof MimeType) {
return (MimeType) headerValue;
}
else {
throw new IllegalArgumentException("Unexpected MimeType value: " + headerValue);
}
}
private Mono<Object> decodeContent(MethodParameter parameter, Message<?> message,
boolean isContentRequired, Flux<DataBuffer> content, MimeType mimeType) {
ResolvableType targetType = ResolvableType.forMethodParameter(parameter);
Class<?> resolvedType = targetType.resolve();
ReactiveAdapter adapter = (resolvedType != null ? getAdapterRegistry().getAdapter(resolvedType) : null);
ResolvableType elementType = (adapter != null ? targetType.getGeneric() : targetType);
isContentRequired = isContentRequired || (adapter != null && !adapter.supportsEmpty());
Consumer<Object> validator = getValidator(message, parameter);
Map<String, Object> hints = Collections.emptyMap();
for (Decoder<?> decoder : this.decoders) {
if (decoder.canDecode(elementType, mimeType)) {
if (adapter != null && adapter.isMultiValue()) {
Flux<?> flux = content
.concatMap(buffer -> decoder.decode(Mono.just(buffer), elementType, mimeType, hints))
.onErrorResume(ex -> Flux.error(handleReadError(parameter, message, ex)));
if (isContentRequired) {
flux = flux.switchIfEmpty(Flux.error(() -> handleMissingBody(parameter, message)));
}
if (validator != null) {
flux = flux.doOnNext(validator::accept);
}
return Mono.just(adapter.fromPublisher(flux));
}
else {
// Single-value (with or without reactive type wrapper)
Mono<?> mono = decoder
.decodeToMono(content.next(), targetType, mimeType, hints)
.onErrorResume(ex -> Mono.error(handleReadError(parameter, message, ex)));
if (isContentRequired) {
mono = mono.switchIfEmpty(Mono.error(() -> handleMissingBody(parameter, message)));
}
if (validator != null) {
mono = mono.doOnNext(validator::accept);
}
return (adapter != null ? Mono.just(adapter.fromPublisher(mono)) : Mono.from(mono));
}
}
}
return Mono.error(new MethodArgumentResolutionException(
message, parameter, "Cannot decode to [" + targetType + "]" + message));
}
private Throwable handleReadError(MethodParameter parameter, Message<?> message, Throwable ex) {
return ex instanceof DecodingException ?
new MethodArgumentResolutionException(message, parameter, "Failed to read HTTP message", ex) : ex;
}
private MethodArgumentResolutionException handleMissingBody(MethodParameter param, Message<?> message) {
return new MethodArgumentResolutionException(message, param,
"Payload content is missing: " + param.getExecutable().toGenericString());
}
@Nullable
private Consumer<Object> getValidator(Message<?> message, MethodParameter parameter) {
if (this.validator == null) {
return null;
}
for (Annotation ann : parameter.getParameterAnnotations()) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
String name = Conventions.getVariableNameForParameter(parameter);
return target -> {
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(target, name);
if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) {
((SmartValidator) this.validator).validate(target, bindingResult, validationHints);
}
else {
this.validator.validate(target, bindingResult);
}
if (bindingResult.hasErrors()) {
throw new MethodArgumentNotValidException(message, parameter, bindingResult);
}
};
}
}
return null;
}
}

View File

@ -0,0 +1,10 @@
/**
* Support classes for working with annotated message-handling methods with
* non-blocking, reactive contracts.
*/
@NonNullApi
@NonNullFields
package org.springframework.messaging.handler.annotation.support.reactive;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

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.
@ -85,7 +85,7 @@ public abstract class AbstractExceptionHandlerMethodResolver {
* @return a Method to handle the exception, or {@code null} if none found
*/
@Nullable
public Method resolveMethod(Exception exception) {
public Method resolveMethod(Throwable exception) {
Method method = resolveMethodByExceptionType(exception.getClass());
if (method == null) {
Throwable cause = exception.getCause();

View File

@ -17,6 +17,7 @@
package org.springframework.messaging.handler.invocation;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
@ -51,6 +52,17 @@ public class MethodArgumentResolutionException extends MessagingException {
this.parameter = parameter;
}
/**
* Create a new instance providing the invalid {@code MethodParameter},
* prepared description, and a cause.
*/
public MethodArgumentResolutionException(
Message<?> message, MethodParameter parameter, String description, @Nullable Throwable cause) {
super(message, getMethodParameterMessage(parameter) + ": " + description, cause);
this.parameter = parameter;
}
/**
* Return the MethodParameter that was rejected.

View File

@ -0,0 +1,218 @@
/*
* 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.Collections;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* Base class for a return value handler that encodes return values to
* {@code Flux<DataBuffer>} through the configured {@link Encoder}s.
*
* <p>Sub-classes must implement the abstract method
* {@link #handleEncodedContent} to handle the resulting encoded content.
*
* <p>This handler should be ordered last since its {@link #supportsReturnType}
* returns {@code true} for any method parameter type.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public abstract class AbstractEncoderMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
private static final ResolvableType VOID_RESOLVABLE_TYPE = ResolvableType.forClass(Void.class);
private static final ResolvableType OBJECT_RESOLVABLE_TYPE = ResolvableType.forClass(Object.class);
protected final Log logger = LogFactory.getLog(getClass());
private final List<Encoder<?>> encoders;
private final ReactiveAdapterRegistry adapterRegistry;
private DataBufferFactory defaultBufferFactory = new DefaultDataBufferFactory();
protected AbstractEncoderMethodReturnValueHandler(List<Encoder<?>> encoders, ReactiveAdapterRegistry registry) {
Assert.notEmpty(encoders, "At least one Encoder is required");
Assert.notNull(registry, "ReactiveAdapterRegistry is required");
this.encoders = Collections.unmodifiableList(encoders);
this.adapterRegistry = registry;
}
/**
* The configured encoders.
*/
public List<Encoder<?>> getEncoders() {
return this.encoders;
}
/**
* The configured adapter registry.
*/
public ReactiveAdapterRegistry getAdapterRegistry() {
return this.adapterRegistry;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
// We could check canEncode but we're probably last in order anyway
return true;
}
@Override
public Mono<Void> handleReturnValue(
@Nullable Object returnValue, MethodParameter returnType, Message<?> message) {
if (returnValue == null) {
return handleNoContent(returnType, message);
}
DataBufferFactory bufferFactory = (DataBufferFactory) message.getHeaders()
.getOrDefault(HandlerMethodReturnValueHandler.DATA_BUFFER_FACTORY_HEADER, this.defaultBufferFactory);
MimeType mimeType = (MimeType) message.getHeaders().get(MessageHeaders.CONTENT_TYPE);
Flux<DataBuffer> encodedContent = encodeContent(
returnValue, returnType, bufferFactory, mimeType, Collections.emptyMap());
return new ChannelSendOperator<>(encodedContent, publisher ->
handleEncodedContent(Flux.from(publisher), returnType, message));
}
@SuppressWarnings("unchecked")
private Flux<DataBuffer> encodeContent(
@Nullable Object content, MethodParameter returnType, DataBufferFactory bufferFactory,
@Nullable MimeType mimeType, Map<String, Object> hints) {
ResolvableType returnValueType = ResolvableType.forMethodParameter(returnType);
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(returnValueType.resolve(), content);
Publisher<?> publisher;
ResolvableType elementType;
if (adapter != null) {
publisher = adapter.toPublisher(content);
ResolvableType genericType = returnValueType.getGeneric();
elementType = getElementType(adapter, genericType);
}
else {
publisher = Mono.justOrEmpty(content);
elementType = returnValueType.toClass() == Object.class && content != null ?
ResolvableType.forInstance(content) : returnValueType;
}
if (elementType.resolve() == void.class || elementType.resolve() == Void.class) {
return Flux.from(publisher).cast(DataBuffer.class);
}
Encoder<?> encoder = getEncoder(elementType, mimeType);
return Flux.from((Publisher) publisher).concatMap(value ->
encodeValue(value, elementType, encoder, bufferFactory, mimeType, hints));
}
private ResolvableType getElementType(ReactiveAdapter adapter, ResolvableType type) {
if (adapter.isNoValue()) {
return VOID_RESOLVABLE_TYPE;
}
else if (type != ResolvableType.NONE) {
return type;
}
else {
return OBJECT_RESOLVABLE_TYPE;
}
}
@Nullable
@SuppressWarnings("unchecked")
private <T> Encoder<T> getEncoder(ResolvableType elementType, @Nullable MimeType mimeType) {
for (Encoder<?> encoder : getEncoders()) {
if (encoder.canEncode(elementType, mimeType)) {
return (Encoder<T>) encoder;
}
}
return null;
}
@SuppressWarnings("unchecked")
private <T> Mono<DataBuffer> encodeValue(
Object element, ResolvableType elementType, @Nullable Encoder<T> encoder,
DataBufferFactory bufferFactory, @Nullable MimeType mimeType,
@Nullable Map<String, Object> hints) {
if (encoder == null) {
encoder = getEncoder(ResolvableType.forInstance(element), mimeType);
if (encoder == null) {
return Mono.error(new MessagingException(
"No encoder for " + elementType + ", current value type is " + element.getClass()));
}
}
Mono<T> mono = Mono.just((T) element);
Flux<DataBuffer> dataBuffers = encoder.encode(mono, bufferFactory, elementType, mimeType, hints);
return DataBufferUtils.join(dataBuffers);
}
/**
* Sub-classes implement this method to handle encoded values in some way
* such as creating and sending messages.
*
* @param encodedContent the encoded content; each {@code DataBuffer}
* represents the fully-aggregated, encoded content for one value
* (i.e. payload) returned from the HandlerMethod.
* @param returnType return type of the handler method that produced the data
* @param message the input message handled by the handler method
* @return completion {@code Mono<Void>} for the handling
*/
protected abstract Mono<Void> handleEncodedContent(
Flux<DataBuffer> encodedContent, MethodParameter returnType, Message<?> message);
/**
* Invoked for a {@code null} return value, which could mean a void method
* or method returning an async type parameterized by void.
* @param returnType return type of the handler method that produced the data
* @param message the input message handled by the handler method
* @return completion {@code Mono<Void>} for the handling
*/
protected abstract Mono<Void> handleNoContent(MethodParameter returnType, Message<?> message);
}

View File

@ -0,0 +1,517 @@
/*
* 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.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.ReactiveMessageHandler;
import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.messaging.handler.MessagingAdviceBean;
import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
/**
* Abstract base class for reactive HandlerMethod-based message handling.
* Provides most of the logic required to discover handler methods at startup,
* find a matching handler method at runtime for a given message and invoke it.
*
* <p>Also supports discovering and invoking exception handling methods to process
* exceptions raised during message handling.
*
* @author Rossen Stoyanchev
* @since 5.2
* @param <T> the type of the Object that contains information mapping information
*/
public abstract class AbstractMethodMessageHandler<T>
implements ReactiveMessageHandler, ApplicationContextAware, InitializingBean, BeanNameAware {
/**
* Bean name prefix for target beans behind scoped proxies. Used to exclude those
* targets from handler method detection, in favor of the corresponding proxies.
* <p>We're not checking the autowire-candidate status here, which is how the
* proxy target filtering problem is being handled at the autowiring level,
* since autowire-candidate may have been turned to {@code false} for other
* reasons, while still expecting the bean to be eligible for handler methods.
* <p>Originally defined in {@link org.springframework.aop.scope.ScopedProxyUtils}
* but duplicated here to avoid a hard dependency on the spring-aop module.
*/
private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";
protected final Log logger = LogFactory.getLog(getClass());
private ArgumentResolverConfigurer argumentResolverConfigurer = new ArgumentResolverConfigurer();
private ReturnValueHandlerConfigurer returnValueHandlerConfigurer = new ReturnValueHandlerConfigurer();
private final InvocableHelper invocableHelper = new InvocableHelper(this::createExceptionMethodResolverFor);
@Nullable
private ApplicationContext applicationContext;
@Nullable
private String beanName;
private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<>(64);
private final MultiValueMap<String, T> destinationLookup = new LinkedMultiValueMap<>(64);
/**
* Configure custom resolvers for handler method arguments.
*/
public void setArgumentResolverConfigurer(ArgumentResolverConfigurer configurer) {
Assert.notNull(configurer, "HandlerMethodArgumentResolver is required.");
this.argumentResolverConfigurer = configurer;
}
/**
* Return the configured custom resolvers for handler method arguments.
*/
public ArgumentResolverConfigurer getArgumentResolverConfigurer() {
return this.argumentResolverConfigurer;
}
/**
* Configure custom return value handlers for handler metohds.
*/
public void setReturnValueHandlerConfigurer(ReturnValueHandlerConfigurer configurer) {
Assert.notNull(configurer, "ReturnValueHandlerConfigurer is required.");
this.returnValueHandlerConfigurer = configurer;
}
/**
* Return the configured return value handlers.
*/
public ReturnValueHandlerConfigurer getReturnValueHandlerConfigurer() {
return this.returnValueHandlerConfigurer;
}
/**
* Configure the registry for adapting various reactive types.
* <p>By default this is an instance of {@link ReactiveAdapterRegistry} with
* default settings.
*/
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
this.invocableHelper.setReactiveAdapterRegistry(registry);
}
/**
* Return the configured registry for adapting reactive types.
*/
public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
return this.invocableHelper.getReactiveAdapterRegistry();
}
@Override
public void setApplicationContext(@Nullable ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Nullable
public ApplicationContext getApplicationContext() {
return this.applicationContext;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
public String getBeanName() {
return this.beanName != null ? this.beanName :
getClass().getSimpleName() + "@" + ObjectUtils.getIdentityHexString(this);
}
/**
* Subclasses can invoke this method to populate the MessagingAdviceBean cache
* (e.g. to support "global" {@code @MessageExceptionHandler}).
*/
protected void registerExceptionHandlerAdvice(
MessagingAdviceBean bean, AbstractExceptionHandlerMethodResolver resolver) {
this.invocableHelper.registerExceptionHandlerAdvice(bean, resolver);
}
/**
* Return a read-only map with all handler methods and their mappings.
*/
public Map<T, HandlerMethod> getHandlerMethods() {
return Collections.unmodifiableMap(this.handlerMethods);
}
/**
* Return a read-only multi-value map with a direct lookup of mappings,
* (e.g. for non-pattern destinations).
*/
public MultiValueMap<String, T> getDestinationLookup() {
return CollectionUtils.unmodifiableMultiValueMap(this.destinationLookup);
}
@Override
public void afterPropertiesSet() {
List<? extends HandlerMethodArgumentResolver> resolvers = initArgumentResolvers();
if (resolvers.isEmpty()) {
resolvers = new ArrayList<>(this.argumentResolverConfigurer.getCustomResolvers());
}
this.invocableHelper.addArgumentResolvers(resolvers);
List<? extends HandlerMethodReturnValueHandler> handlers = initReturnValueHandlers();
if (handlers.isEmpty()) {
handlers = new ArrayList<>(this.returnValueHandlerConfigurer.getCustomHandlers());
}
this.invocableHelper.addReturnValueHandlers(handlers);
initHandlerMethods();
}
/**
* Return the list of argument resolvers to use.
* <p>Subclasses should also take into account custom argument types configured via
* {@link #setArgumentResolverConfigurer}.
*/
protected abstract List<? extends HandlerMethodArgumentResolver> initArgumentResolvers();
/**
* Return the list of return value handlers to use.
* <p>Subclasses should also take into account custom return value types configured
* via {@link #setReturnValueHandlerConfigurer}.
*/
protected abstract List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers();
private void initHandlerMethods() {
if (this.applicationContext == null) {
logger.warn("No ApplicationContext available for detecting beans with message handling methods.");
return;
}
Predicate<Class<?>> handlerPredicate = initHandlerPredicate();
if (handlerPredicate == null) {
logger.warn("[" + getBeanName() + "] No auto-detection of handler methods (e.g. in @Controller).");
return;
}
for (String beanName : this.applicationContext.getBeanNamesForType(Object.class)) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
Class<?> beanType = null;
try {
beanType = this.applicationContext.getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isDebugEnabled()) {
logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
}
}
if (beanType != null && handlerPredicate.test(beanType)) {
detectHandlerMethods(beanName);
}
}
}
}
/**
* Return the predicate to use to check whether a given Spring bean should
* be introspected for message handling methods. If {@code null} is
* returned, auto-detection is effectively disabled.
*/
@Nullable
protected abstract Predicate<Class<?>> initHandlerPredicate();
/**
* Detect if the given handler has any methods that can handle messages and if
* so register it with the extracted mapping information.
* <p><strong>Note:</strong> This method is protected and can be invoked by
* sub-classes, but this should be done on startup only as documented in
* {@link #registerHandlerMethod}.
* @param handler the handler to check, either an instance of a Spring bean name
*/
protected final void detectHandlerMethods(Object handler) {
Class<?> handlerType;
if (handler instanceof String) {
ApplicationContext context = getApplicationContext();
Assert.state(context != null, "ApplicationContext is required for resolving handler bean names");
handlerType = context.getType((String) handler);
}
else {
handlerType = handler.getClass();
}
if (handlerType != null) {
final Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));
if (logger.isDebugEnabled()) {
logger.debug(methods.size() + " message handler methods found on " + userType + ": " + methods);
}
methods.forEach((key, value) -> registerHandlerMethod(handler, key, value));
}
}
/**
* Obtain the mapping for the given method, if any.
* @param method the method to check
* @param handlerType the handler type, possibly a sub-type of the method's declaring class
* @return the mapping, or {@code null} if the method is not mapped
*/
@Nullable
protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
/**
* Register a handler method and its unique mapping.
* <p><strong>Note:</strong> This method is protected and can be invoked by
* sub-classes. Keep in mind however that the registration is not protected
* for concurrent use, and is expected to be done on startup.
* @param handler the bean name of the handler or the handler instance
* @param method the method to register
* @param mapping the mapping conditions associated with the handler method
* @throws IllegalStateException if another method was already registered
* under the same mapping
*/
protected final void registerHandlerMethod(Object handler, Method method, T mapping) {
Assert.notNull(mapping, "Mapping must not be null");
HandlerMethod newHandlerMethod = createHandlerMethod(handler, method);
HandlerMethod oldHandlerMethod = this.handlerMethods.get(mapping);
if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) {
throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean() +
"' bean method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '" +
oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped.");
}
this.handlerMethods.put(mapping, newHandlerMethod);
if (logger.isTraceEnabled()) {
logger.trace("Mapped \"" + mapping + "\" onto " + newHandlerMethod);
}
for (String pattern : getDirectLookupMappings(mapping)) {
this.destinationLookup.add(pattern, mapping);
}
}
/**
* Create a HandlerMethod instance from an Object handler that is either a handler
* instance or a String-based bean name.
*/
private HandlerMethod createHandlerMethod(Object handler, Method method) {
HandlerMethod handlerMethod;
if (handler instanceof String) {
ApplicationContext context = getApplicationContext();
Assert.state(context != null, "ApplicationContext is required for resolving handler bean names");
String beanName = (String) handler;
handlerMethod = new HandlerMethod(beanName, context.getAutowireCapableBeanFactory(), method);
}
else {
handlerMethod = new HandlerMethod(handler, method);
}
return handlerMethod;
}
/**
* Return String-based destinations for the given mapping, if any, that can
* be used to find matches with a direct lookup (i.e. non-patterns).
* <p><strong>Note:</strong> This is completely optional. The mapping
* metadata for a sub-class may support neither direct lookups, nor String
* based destinations.
*/
protected abstract Set<String> getDirectLookupMappings(T mapping);
@Override
public Mono<Void> handleMessage(Message<?> message) throws MessagingException {
Match<T> match = getHandlerMethod(message);
if (match == null) {
// handleNoMatch would have been invoked already
return Mono.empty();
}
HandlerMethod handlerMethod = match.getHandlerMethod().createWithResolvedBean();
return this.invocableHelper.handleMessage(handlerMethod, message);
}
@Nullable
private Match<T> getHandlerMethod(Message<?> message) {
List<Match<T>> matches = new ArrayList<>();
String destination = getDestination(message);
List<T> mappingsByUrl = destination != null ? this.destinationLookup.get(destination) : null;
if (mappingsByUrl != null) {
addMatchesToCollection(mappingsByUrl, message, matches);
}
if (matches.isEmpty()) {
// No direct hits, go through all mappings
Set<T> allMappings = this.handlerMethods.keySet();
addMatchesToCollection(allMappings, message, matches);
}
if (matches.isEmpty()) {
handleNoMatch(destination, message);
return null;
}
Comparator<Match<T>> comparator = new MatchComparator(getMappingComparator(message));
matches.sort(comparator);
if (logger.isTraceEnabled()) {
logger.trace("Found " + matches.size() + " handler methods: " + matches);
}
Match<T> bestMatch = matches.get(0);
if (matches.size() > 1) {
Match<T> secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
HandlerMethod m1 = bestMatch.handlerMethod;
HandlerMethod m2 = secondBestMatch.handlerMethod;
throw new IllegalStateException("Ambiguous handler methods mapped for destination '" +
destination + "': {" + m1.getShortLogMessage() + ", " + m2.getShortLogMessage() + "}");
}
}
return bestMatch;
}
/**
* Extract a String-based destination, if any, that can be used to perform
* a direct look up into the registered mappings.
* <p><strong>Note:</strong> This is completely optional. The mapping
* metadata for a sub-class may support neither direct lookups, nor String
* based destinations.
* @see #getDirectLookupMappings(Object)
*/
@Nullable
protected abstract String getDestination(Message<?> message);
private void addMatchesToCollection(
Collection<T> mappingsToCheck, Message<?> message, List<Match<T>> matches) {
for (T mapping : mappingsToCheck) {
T match = getMatchingMapping(mapping, message);
if (match != null) {
matches.add(new Match<T>(match, this.handlerMethods.get(mapping)));
}
}
}
/**
* Check if a mapping matches the current message and return a possibly
* new mapping with conditions relevant to the current request.
* @param mapping the mapping to get a match for
* @param message the message being handled
* @return the match or {@code null} if there is no match
*/
@Nullable
protected abstract T getMatchingMapping(T mapping, Message<?> message);
/**
* Return a comparator for sorting matching mappings.
* The returned comparator should sort 'better' matches higher.
* @param message the current Message
* @return the comparator, never {@code null}
*/
protected abstract Comparator<T> getMappingComparator(Message<?> message);
/**
* Invoked when no matching handler is found.
* @param destination the destination
* @param message the message
*/
@Nullable
protected void handleNoMatch(@Nullable String destination, Message<?> message) {
logger.debug("No handlers for destination '" + destination + "'");
}
/**
* Create a concrete instance of {@link AbstractExceptionHandlerMethodResolver}
* that finds exception handling methods based on some criteria, e.g. based
* on the presence of {@code @MessageExceptionHandler}.
* @param beanType the class in which an exception occurred during handling
* @return the resolver to use
*/
protected abstract AbstractExceptionHandlerMethodResolver createExceptionMethodResolverFor(Class<?> beanType);
/**
* Container for matched mapping and HandlerMethod. Used for best match
* comparison and for access to mapping information.
*/
private static class Match<T> {
private final T mapping;
private final HandlerMethod handlerMethod;
Match(T mapping, HandlerMethod handlerMethod) {
this.mapping = mapping;
this.handlerMethod = handlerMethod;
}
public T getMapping() {
return this.mapping;
}
public HandlerMethod getHandlerMethod() {
return this.handlerMethod;
}
@Override
public String toString() {
return this.mapping.toString();
}
}
private class MatchComparator implements Comparator<Match<T>> {
private final Comparator<T> comparator;
MatchComparator(Comparator<T> comparator) {
this.comparator = comparator;
}
@Override
public int compare(Match<T> match1, Match<T> match2) {
return this.comparator.compare(match1.mapping, match2.mapping);
}
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.util.Assert;
/**
* Assist with configuration for handler method argument resolvers.
* At present, it supports only providing a list of custom resolvers.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public class ArgumentResolverConfigurer {
private final List<HandlerMethodArgumentResolver> customResolvers = new ArrayList<>(8);
/**
* Configure resolvers for custom handler method arguments.
* @param resolver the resolvers to add
*/
public void addCustomResolver(HandlerMethodArgumentResolver... resolver) {
Assert.notNull(resolver, "'resolvers' must not be null");
this.customResolvers.addAll(Arrays.asList(resolver));
}
public List<HandlerMethodArgumentResolver> getCustomResolvers() {
return this.customResolvers;
}
}

View File

@ -0,0 +1,410 @@
/*
* 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.util.function.Function;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import reactor.core.CoreSubscriber;
import reactor.core.Scannable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Operators;
import reactor.util.context.Context;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* ----------------------
* <p><strong>NOTE:</strong> This class was copied from
* {@code org.springframework.http.server.reactive.ChannelSendOperator} and is
* identical to it. It's used for the same purpose, i.e. the ability to switch to
* alternate handling via annotated exception handler methods if the output
* publisher starts with an error.
* <p>----------------------<br>
*
* <p>Given a write function that accepts a source {@code Publisher<T>} to write
* with and returns {@code Publisher<Void>} for the result, this operator helps
* to defer the invocation of the write function, until we know if the source
* publisher will begin publishing without an error. If the first emission is
* an error, the write function is bypassed, and the error is sent directly
* through the result publisher. Otherwise the write function is invoked.
*
* @author Rossen Stoyanchev
* @author Stephane Maldini
* @since 5.2
* @param <T> the type of element signaled
*/
class ChannelSendOperator<T> extends Mono<Void> implements Scannable {
private final Function<Publisher<T>, Publisher<Void>> writeFunction;
private final Flux<T> source;
public ChannelSendOperator(Publisher<? extends T> source, Function<Publisher<T>, Publisher<Void>> writeFunction) {
this.source = Flux.from(source);
this.writeFunction = writeFunction;
}
@Override
@Nullable
@SuppressWarnings("rawtypes")
public Object scanUnsafe(Attr key) {
if (key == Attr.PREFETCH) {
return Integer.MAX_VALUE;
}
if (key == Attr.PARENT) {
return this.source;
}
return null;
}
@Override
public void subscribe(CoreSubscriber<? super Void> actual) {
this.source.subscribe(new WriteBarrier(actual));
}
private enum State {
/** No emissions from the upstream source yet. */
NEW,
/**
* At least one signal of any kind has been received; we're ready to
* call the write function and proceed with actual writing.
*/
FIRST_SIGNAL_RECEIVED,
/**
* The write subscriber has subscribed and requested; we're going to
* emit the cached signals.
*/
EMITTING_CACHED_SIGNALS,
/**
* The write subscriber has subscribed, and cached signals have been
* emitted to it; we're ready to switch to a simple pass-through mode
* for all remaining signals.
**/
READY_TO_WRITE
}
/**
* A barrier inserted between the write source and the write subscriber
* (i.e. the HTTP server adapter) that pre-fetches and waits for the first
* signal before deciding whether to hook in to the write subscriber.
*
* <p>Acts as:
* <ul>
* <li>Subscriber to the write source.
* <li>Subscription to the write subscriber.
* <li>Publisher to the write subscriber.
* </ul>
*
* <p>Also uses {@link WriteCompletionBarrier} to communicate completion
* and detect cancel signals from the completion subscriber.
*/
private class WriteBarrier implements CoreSubscriber<T>, Subscription, Publisher<T> {
/* Bridges signals to and from the completionSubscriber */
private final WriteCompletionBarrier writeCompletionBarrier;
/* Upstream write source subscription */
@Nullable
private Subscription subscription;
/** Cached data item before readyToWrite. */
@Nullable
private T item;
/** Cached error signal before readyToWrite. */
@Nullable
private Throwable error;
/** Cached onComplete signal before readyToWrite. */
private boolean completed = false;
/** Recursive demand while emitting cached signals. */
private long demandBeforeReadyToWrite;
/** Current state. */
private State state = State.NEW;
/** The actual writeSubscriber from the HTTP server adapter. */
@Nullable
private Subscriber<? super T> writeSubscriber;
WriteBarrier(CoreSubscriber<? super Void> completionSubscriber) {
this.writeCompletionBarrier = new WriteCompletionBarrier(completionSubscriber, this);
}
// Subscriber<T> methods (we're the subscriber to the write source)..
@Override
public final void onSubscribe(Subscription s) {
if (Operators.validate(this.subscription, s)) {
this.subscription = s;
this.writeCompletionBarrier.connect();
s.request(1);
}
}
@Override
public final void onNext(T item) {
if (this.state == State.READY_TO_WRITE) {
requiredWriteSubscriber().onNext(item);
return;
}
//FIXME revisit in case of reentrant sync deadlock
synchronized (this) {
if (this.state == State.READY_TO_WRITE) {
requiredWriteSubscriber().onNext(item);
}
else if (this.state == State.NEW) {
this.item = item;
this.state = State.FIRST_SIGNAL_RECEIVED;
writeFunction.apply(this).subscribe(this.writeCompletionBarrier);
}
else {
if (this.subscription != null) {
this.subscription.cancel();
}
this.writeCompletionBarrier.onError(new IllegalStateException("Unexpected item."));
}
}
}
private Subscriber<? super T> requiredWriteSubscriber() {
Assert.state(this.writeSubscriber != null, "No write subscriber");
return this.writeSubscriber;
}
@Override
public final void onError(Throwable ex) {
if (this.state == State.READY_TO_WRITE) {
requiredWriteSubscriber().onError(ex);
return;
}
synchronized (this) {
if (this.state == State.READY_TO_WRITE) {
requiredWriteSubscriber().onError(ex);
}
else if (this.state == State.NEW) {
this.state = State.FIRST_SIGNAL_RECEIVED;
this.writeCompletionBarrier.onError(ex);
}
else {
this.error = ex;
}
}
}
@Override
public final void onComplete() {
if (this.state == State.READY_TO_WRITE) {
requiredWriteSubscriber().onComplete();
return;
}
synchronized (this) {
if (this.state == State.READY_TO_WRITE) {
requiredWriteSubscriber().onComplete();
}
else if (this.state == State.NEW) {
this.completed = true;
this.state = State.FIRST_SIGNAL_RECEIVED;
writeFunction.apply(this).subscribe(this.writeCompletionBarrier);
}
else {
this.completed = true;
}
}
}
@Override
public Context currentContext() {
return this.writeCompletionBarrier.currentContext();
}
// Subscription methods (we're the Subscription to the writeSubscriber)..
@Override
public void request(long n) {
Subscription s = this.subscription;
if (s == null) {
return;
}
if (this.state == State.READY_TO_WRITE) {
s.request(n);
return;
}
synchronized (this) {
if (this.writeSubscriber != null) {
if (this.state == State.EMITTING_CACHED_SIGNALS) {
this.demandBeforeReadyToWrite = n;
return;
}
try {
this.state = State.EMITTING_CACHED_SIGNALS;
if (emitCachedSignals()) {
return;
}
n = n + this.demandBeforeReadyToWrite - 1;
if (n == 0) {
return;
}
}
finally {
this.state = State.READY_TO_WRITE;
}
}
}
s.request(n);
}
private boolean emitCachedSignals() {
if (this.item != null) {
requiredWriteSubscriber().onNext(this.item);
}
if (this.error != null) {
requiredWriteSubscriber().onError(this.error);
return true;
}
if (this.completed) {
requiredWriteSubscriber().onComplete();
return true;
}
return false;
}
@Override
public void cancel() {
Subscription s = this.subscription;
if (s != null) {
this.subscription = null;
s.cancel();
}
}
// Publisher<T> methods (we're the Publisher to the writeSubscriber)..
@Override
public void subscribe(Subscriber<? super T> writeSubscriber) {
synchronized (this) {
Assert.state(this.writeSubscriber == null, "Only one write subscriber supported");
this.writeSubscriber = writeSubscriber;
if (this.error != null || this.completed) {
this.writeSubscriber.onSubscribe(Operators.emptySubscription());
emitCachedSignals();
}
else {
this.writeSubscriber.onSubscribe(this);
}
}
}
}
/**
* We need an extra barrier between the WriteBarrier itself and the actual
* completion subscriber.
*
* <p>The completionSubscriber is subscribed initially to the WriteBarrier.
* Later after the first signal is received, we need one more subscriber
* instance (per spec can only subscribe once) to subscribe to the write
* function and switch to delegating completion signals from it.
*/
private class WriteCompletionBarrier implements CoreSubscriber<Void>, Subscription {
/* Downstream write completion subscriber */
private final CoreSubscriber<? super Void> completionSubscriber;
private final WriteBarrier writeBarrier;
@Nullable
private Subscription subscription;
public WriteCompletionBarrier(CoreSubscriber<? super Void> subscriber, WriteBarrier writeBarrier) {
this.completionSubscriber = subscriber;
this.writeBarrier = writeBarrier;
}
/**
* Connect the underlying completion subscriber to this barrier in order
* to track cancel signals and pass them on to the write barrier.
*/
public void connect() {
this.completionSubscriber.onSubscribe(this);
}
// Subscriber methods (we're the subscriber to the write function)..
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Void aVoid) {
}
@Override
public void onError(Throwable ex) {
this.completionSubscriber.onError(ex);
}
@Override
public void onComplete() {
this.completionSubscriber.onComplete();
}
@Override
public Context currentContext() {
return this.completionSubscriber.currentContext();
}
@Override
public void request(long n) {
// Ignore: we don't produce data
}
@Override
public void cancel() {
this.writeBarrier.cancel();
Subscription subscription = this.subscription;
if (subscription != null) {
subscription.cancel();
}
}
}
}

View File

@ -0,0 +1,52 @@
/*
* 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 reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
/**
* Strategy interface for resolving method parameters into argument values
* in the context of a given {@link Message}.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public interface HandlerMethodArgumentResolver {
/**
* Whether the given {@linkplain MethodParameter method parameter} is
* supported by this resolver.
* @param parameter the method parameter to check
* @return {@code true} if this resolver supports the supplied parameter;
* {@code false} otherwise
*/
boolean supportsParameter(MethodParameter parameter);
/**
* Resolves a method parameter into an argument value from a given message.
* @param parameter the method parameter to resolve.
* This parameter must have previously been passed to
* {@link #supportsParameter(org.springframework.core.MethodParameter)}
* which must have returned {@code true}.
* @param message the currently processed message
* @return {@code Mono} for the argument value, possibly empty
*/
Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message);
}

View File

@ -0,0 +1,142 @@
/*
* 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.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
/**
* Resolves method parameters by delegating to a list of registered
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
* Previously resolved method parameters are cached for faster lookups.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
protected final Log logger = LogFactory.getLog(getClass());
private final List<HandlerMethodArgumentResolver> argumentResolvers = new LinkedList<>();
private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache =
new ConcurrentHashMap<>(256);
/**
* Add the given {@link HandlerMethodArgumentResolver}.
*/
public HandlerMethodArgumentResolverComposite addResolver(HandlerMethodArgumentResolver resolver) {
this.argumentResolvers.add(resolver);
return this;
}
/**
* Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
*/
public HandlerMethodArgumentResolverComposite addResolvers(@Nullable HandlerMethodArgumentResolver... resolvers) {
if (resolvers != null) {
Collections.addAll(this.argumentResolvers, resolvers);
}
return this;
}
/**
* Add the given {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}.
*/
public HandlerMethodArgumentResolverComposite addResolvers(
@Nullable List<? extends HandlerMethodArgumentResolver> resolvers) {
if (resolvers != null) {
this.argumentResolvers.addAll(resolvers);
}
return this;
}
/**
* Return a read-only list with the contained resolvers, or an empty list.
*/
public List<HandlerMethodArgumentResolver> getResolvers() {
return Collections.unmodifiableList(this.argumentResolvers);
}
/**
* Clear the list of configured resolvers.
*/
public void clear() {
this.argumentResolvers.clear();
}
/**
* Whether the given {@linkplain MethodParameter method parameter} is
* supported by any registered {@link HandlerMethodArgumentResolver}.
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
return getArgumentResolver(parameter) != null;
}
/**
* Iterate over registered
* {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} and
* invoke the one that supports it.
* @throws IllegalStateException if no suitable
* {@link HandlerMethodArgumentResolver} is found.
*/
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) {
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException(
"Unsupported parameter type [" + parameter.getParameterType().getName() + "]." +
" supportsParameter should be called first.");
}
return resolver.resolveArgument(parameter, message);
}
/**
* Find a registered {@link HandlerMethodArgumentResolver} that supports
* the given method parameter.
*/
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
if (methodArgumentResolver.supportsParameter(parameter)) {
result = methodArgumentResolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.
* 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;
/**
* Handle the return value from the invocation of an annotated {@link Message}
* handling method.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public interface HandlerMethodReturnValueHandler {
/** Header containing a DataBufferFactory for use in return value handling. */
String DATA_BUFFER_FACTORY_HEADER = "dataBufferFactory";
/**
* Whether the given {@linkplain MethodParameter method return type} is
* supported by this handler.
* @param returnType the method return type to check
* @return {@code true} if this handler supports the supplied return type;
* {@code false} otherwise
*/
boolean supportsReturnType(MethodParameter returnType);
/**
* Handle the given return value.
* @param returnValue the value returned from the handler method
* @param returnType the type of the return value. This type must have previously
* been passed to {@link #supportsReturnType(MethodParameter)}
* and it must have returned {@code true}.
* @return {@code Mono<Void>} to indicate when handling is complete.
*/
Mono<Void> handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, Message<?> message);
}

View File

@ -0,0 +1,108 @@
/*
* 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.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
/**
* A HandlerMethodReturnValueHandler that wraps and delegates to others.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {
protected final Log logger = LogFactory.getLog(getClass());
private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<>();
/**
* Return a read-only list with the configured handlers.
*/
public List<HandlerMethodReturnValueHandler> getReturnValueHandlers() {
return Collections.unmodifiableList(this.returnValueHandlers);
}
/**
* Clear the list of configured handlers.
*/
public void clear() {
this.returnValueHandlers.clear();
}
/**
* Add the given {@link HandlerMethodReturnValueHandler}.
*/
public HandlerMethodReturnValueHandlerComposite addHandler(HandlerMethodReturnValueHandler returnValueHandler) {
this.returnValueHandlers.add(returnValueHandler);
return this;
}
/**
* Add the given {@link HandlerMethodReturnValueHandler HandlerMethodReturnValueHandlers}.
*/
public HandlerMethodReturnValueHandlerComposite addHandlers(
@Nullable List<? extends HandlerMethodReturnValueHandler> handlers) {
if (handlers != null) {
this.returnValueHandlers.addAll(handlers);
}
return this;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return getReturnValueHandler(returnType) != null;
}
@Override
public Mono<Void> handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, Message<?> message) {
HandlerMethodReturnValueHandler handler = getReturnValueHandler(returnType);
if (handler == null) {
throw new IllegalStateException("No handler for return value type: " + returnType.getParameterType());
}
if (logger.isTraceEnabled()) {
logger.trace("Processing return value with " + handler);
}
return handler.handleReturnValue(returnValue, returnType, message);
}
@SuppressWarnings("ForLoopReplaceableByForEach")
@Nullable
private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) {
for (int i = 0; i < this.returnValueHandlers.size(); i++) {
HandlerMethodReturnValueHandler handler = this.returnValueHandlers.get(i);
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
}

View File

@ -0,0 +1,213 @@
/*
* 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.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import reactor.core.publisher.Mono;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
/**
* Extension of {@link HandlerMethod} that invokes the underlying method with
* argument values resolved from the current HTTP request through a list of
* {@link HandlerMethodArgumentResolver}.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public class InvocableHandlerMethod extends HandlerMethod {
private static final Mono<Object[]> EMPTY_ARGS = Mono.just(new Object[0]);
private static final Object NO_ARG_VALUE = new Object();
private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
/**
* Create an instance from a {@code HandlerMethod}.
*/
public InvocableHandlerMethod(HandlerMethod handlerMethod) {
super(handlerMethod);
}
/**
* Create an instance from a bean instance and a method.
*/
public InvocableHandlerMethod(Object bean, Method method) {
super(bean, method);
}
/**
* Configure the argument resolvers to use to use for resolving method
* argument values against a {@code ServerWebExchange}.
*/
public void setArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
this.resolvers.addResolvers(resolvers);
}
/**
* Return the configured argument resolvers.
*/
public List<HandlerMethodArgumentResolver> getResolvers() {
return this.resolvers.getResolvers();
}
/**
* Set the ParameterNameDiscoverer for resolving parameter names when needed
* (e.g. default request attribute name).
* <p>Default is a {@link DefaultParameterNameDiscoverer}.
*/
public void setParameterNameDiscoverer(ParameterNameDiscoverer nameDiscoverer) {
this.parameterNameDiscoverer = nameDiscoverer;
}
/**
* Return the configured parameter name discoverer.
*/
public ParameterNameDiscoverer getParameterNameDiscoverer() {
return this.parameterNameDiscoverer;
}
/**
* Configure a reactive registry. This is needed for cases where the response
* is fully handled within the controller in combination with an async void
* return value.
* <p>By default this is an instance of {@link ReactiveAdapterRegistry} with
* default settings.
* @param registry the registry to use
*/
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
this.reactiveAdapterRegistry = registry;
}
/**
* Invoke the method for the given exchange.
* @param message the current message
* @param providedArgs optional list of argument values to match by type
* @return a Mono with the result from the invocation.
*/
public Mono<Object> invoke(Message<?> message, Object... providedArgs) {
return getMethodArgumentValues(message, providedArgs).flatMap(args -> {
Object value;
try {
ReflectionUtils.makeAccessible(getBridgedMethod());
value = getBridgedMethod().invoke(getBean(), args);
}
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
return Mono.error(new IllegalStateException(formatInvokeError(text, args), ex));
}
catch (InvocationTargetException ex) {
return Mono.error(ex.getTargetException());
}
catch (Throwable ex) {
// Unlikely to ever get here, but it must be handled...
return Mono.error(new IllegalStateException(formatInvokeError("Invocation failure", args), ex));
}
MethodParameter returnType = getReturnType();
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(returnType.getParameterType());
return isAsyncVoidReturnType(returnType, adapter) ?
Mono.from(adapter.toPublisher(value)) : Mono.justOrEmpty(value);
});
}
private Mono<Object[]> getMethodArgumentValues(Message<?> message, Object... providedArgs) {
if (ObjectUtils.isEmpty(getMethodParameters())) {
return EMPTY_ARGS;
}
MethodParameter[] parameters = getMethodParameters();
List<Mono<Object>> argMonos = new ArrayList<>(parameters.length);
for (MethodParameter parameter : parameters) {
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
Object providedArg = findProvidedArgument(parameter, providedArgs);
if (providedArg != null) {
argMonos.add(Mono.just(providedArg));
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
return Mono.error(new MethodArgumentResolutionException(
message, parameter, formatArgumentError(parameter, "No suitable resolver")));
}
try {
argMonos.add(this.resolvers.resolveArgument(parameter, message)
.defaultIfEmpty(NO_ARG_VALUE)
.doOnError(cause -> logArgumentErrorIfNecessary(parameter, cause)));
}
catch (Exception ex) {
logArgumentErrorIfNecessary(parameter, ex);
argMonos.add(Mono.error(ex));
}
}
return Mono.zip(argMonos, values ->
Stream.of(values).map(o -> o != NO_ARG_VALUE ? o : null).toArray());
}
private void logArgumentErrorIfNecessary(MethodParameter parameter, Throwable cause) {
// Leave stack trace for later, if error is not handled..
String causeMessage = cause.getMessage();
if (!causeMessage.contains(parameter.getExecutable().toGenericString())) {
if (logger.isDebugEnabled()) {
logger.debug(formatArgumentError(parameter, causeMessage));
}
}
}
private boolean isAsyncVoidReturnType(MethodParameter returnType, @Nullable ReactiveAdapter reactiveAdapter) {
if (reactiveAdapter != null && reactiveAdapter.supportsEmpty()) {
if (reactiveAdapter.isNoValue()) {
return true;
}
Type parameterType = returnType.getGenericParameterType();
if (parameterType instanceof ParameterizedType) {
ParameterizedType type = (ParameterizedType) parameterType;
if (type.getActualTypeArguments().length == 1) {
return Void.class.equals(type.getActualTypeArguments()[0]);
}
}
}
return false;
}
}

View File

@ -0,0 +1,208 @@
/*
* 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.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.messaging.handler.MessagingAdviceBean;
import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver;
import org.springframework.util.Assert;
/**
* Help to initialize and invoke an {@link InvocableHandlerMethod}, and to then
* apply return value handling and exception handling. Holds all necessary
* configuration necessary to do so.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
class InvocableHelper {
private static Log logger = LogFactory.getLog(InvocableHelper.class);
private final HandlerMethodArgumentResolverComposite argumentResolvers =
new HandlerMethodArgumentResolverComposite();
private final HandlerMethodReturnValueHandlerComposite returnValueHandlers =
new HandlerMethodReturnValueHandlerComposite();
private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
private final Function<Class<?>, AbstractExceptionHandlerMethodResolver> exceptionMethodResolverFactory;
private final Map<Class<?>, AbstractExceptionHandlerMethodResolver> exceptionHandlerCache =
new ConcurrentHashMap<>(64);
private final Map<MessagingAdviceBean, AbstractExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
new LinkedHashMap<>(64);
public InvocableHelper(
Function<Class<?>, AbstractExceptionHandlerMethodResolver> exceptionMethodResolverFactory) {
this.exceptionMethodResolverFactory = exceptionMethodResolverFactory;
}
/**
* Add the arguments resolvers to use for message handling and exception
* handling methods.
*/
public void addArgumentResolvers(List<? extends HandlerMethodArgumentResolver> resolvers) {
this.argumentResolvers.addResolvers(resolvers);
}
/**
* Add the return value handlers to use for message handling and exception
* handling methods.
*/
public void addReturnValueHandlers(List<? extends HandlerMethodReturnValueHandler> handlers) {
this.returnValueHandlers.addHandlers(handlers);
}
/**
* Configure the registry for adapting various reactive types.
* <p>By default this is an instance of {@link ReactiveAdapterRegistry} with
* default settings.
*/
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
Assert.notNull(registry, "ReactiveAdapterRegistry is required");
this.reactiveAdapterRegistry = registry;
}
/**
* Return the configured registry for adapting reactive types.
*/
public ReactiveAdapterRegistry getReactiveAdapterRegistry() {
return this.reactiveAdapterRegistry;
}
/**
* Method to populate the MessagingAdviceBean cache (e.g. to support "global"
* {@code @MessageExceptionHandler}).
*/
public void registerExceptionHandlerAdvice(
MessagingAdviceBean bean, AbstractExceptionHandlerMethodResolver resolver) {
this.exceptionHandlerAdviceCache.put(bean, resolver);
}
/**
* Create {@link InvocableHandlerMethod} with the configured arg resolvers.
* @param handlerMethod the target handler method to invoke
* @return the created instance
*/
public InvocableHandlerMethod initMessageMappingMethod(HandlerMethod handlerMethod) {
InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);
invocable.setArgumentResolvers(this.argumentResolvers.getResolvers());
return invocable;
}
/**
* Find an exception handling method for the given exception.
* <p>The default implementation searches methods in the class hierarchy of
* the HandlerMethod first and if not found, it continues searching for
* additional handling methods registered via
* {@link #registerExceptionHandlerAdvice}.
* @param handlerMethod the method where the exception was raised
* @param ex the exception raised or signaled
* @return a method to handle the exception, or {@code null}
*/
@Nullable
public InvocableHandlerMethod initExceptionHandlerMethod(HandlerMethod handlerMethod, Throwable ex) {
if (logger.isDebugEnabled()) {
logger.debug("Searching for methods to handle " + ex.getClass().getSimpleName());
}
Class<?> beanType = handlerMethod.getBeanType();
AbstractExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(beanType);
if (resolver == null) {
resolver = this.exceptionMethodResolverFactory.apply(beanType);
this.exceptionHandlerCache.put(beanType, resolver);
}
InvocableHandlerMethod exceptionHandlerMethod = null;
Method method = resolver.resolveMethod(ex);
if (method != null) {
exceptionHandlerMethod = new InvocableHandlerMethod(handlerMethod.getBean(), method);
}
else {
for (MessagingAdviceBean advice : this.exceptionHandlerAdviceCache.keySet()) {
if (advice.isApplicableToBeanType(beanType)) {
resolver = this.exceptionHandlerAdviceCache.get(advice);
method = resolver.resolveMethod(ex);
if (method != null) {
exceptionHandlerMethod = new InvocableHandlerMethod(advice.resolveBean(), method);
break;
}
}
}
}
if (exceptionHandlerMethod != null) {
logger.debug("Found exception handler " + exceptionHandlerMethod.getShortLogMessage());
exceptionHandlerMethod.setArgumentResolvers(this.argumentResolvers.getResolvers());
}
else {
logger.error("No exception handling method", ex);
}
return exceptionHandlerMethod;
}
public Mono<Void> handleMessage(HandlerMethod handlerMethod, Message<?> message) {
InvocableHandlerMethod invocable = initMessageMappingMethod(handlerMethod);
if (logger.isDebugEnabled()) {
logger.debug("Invoking " + invocable.getShortLogMessage());
}
return invocable.invoke(message)
.switchIfEmpty(Mono.defer(() -> handleReturnValue(null, invocable, message)))
.flatMap(returnValue -> handleReturnValue(returnValue, invocable, message))
.onErrorResume(ex -> {
InvocableHandlerMethod exHandler = initExceptionHandlerMethod(handlerMethod, ex);
if (exHandler == null) {
return Mono.error(ex);
}
if (logger.isDebugEnabled()) {
logger.debug("Invoking " + exHandler.getShortLogMessage());
}
return exHandler.invoke(message, ex)
.switchIfEmpty(Mono.defer(() -> handleReturnValue(null, exHandler, message)))
.flatMap(returnValue -> handleReturnValue(returnValue, exHandler, message));
});
}
private Mono<Void> handleReturnValue(
@Nullable Object returnValue, HandlerMethod handlerMethod, Message<?> message) {
MethodParameter returnType = handlerMethod.getReturnType();
return this.returnValueHandlers.handleReturnValue(returnValue, returnType, message);
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.springframework.util.Assert;
/**
* Assist with configuration for handler method return value handlers.
* At present, it supports only providing a list of custom handlers.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public class ReturnValueHandlerConfigurer {
private final List<HandlerMethodReturnValueHandler> customHandlers = new ArrayList<>(8);
/**
* Configure custom return value handlers for handler methods.
* @param handlers the handlers to add
*/
public void addCustomHandler(HandlerMethodReturnValueHandler... handlers) {
Assert.notNull(handlers, "'handlers' must not be null");
this.customHandlers.addAll(Arrays.asList(handlers));
}
public List<HandlerMethodReturnValueHandler> getCustomHandlers() {
return this.customHandlers;
}
}

View File

@ -0,0 +1,52 @@
/*
* 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;
/**
* An extension of {@link HandlerMethodArgumentResolver} for implementations
* that are synchronous in nature and do not block to resolve values.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public interface SyncHandlerMethodArgumentResolver extends HandlerMethodArgumentResolver {
/**
* {@inheritDoc}
* <p>By default this simply delegates to {@link #resolveArgumentValue} for
* synchronous resolution.
*/
@Override
default Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) {
return Mono.justOrEmpty(resolveArgumentValue(parameter, message));
}
/**
* Resolve the value for the method parameter synchronously.
* @param parameter the method parameter
* @param message the currently processed message
* @return the resolved value, if any
*/
@Nullable
Object resolveArgumentValue(MethodParameter parameter, Message<?> message);
}

View File

@ -0,0 +1,10 @@
/**
* Common infrastructure for invoking message handler methods with non-blocking,
* and reactive contracts.
*/
@NonNullApi
@NonNullFields
package org.springframework.messaging.handler.invocation.reactive;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@ -0,0 +1,272 @@
/*
* 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.rsocket;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
/**
* Default, package-private {@link RSocketRequester} implementation.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
final class DefaultRSocketRequester implements RSocketRequester {
private static final Map<String, Object> EMPTY_HINTS = Collections.emptyMap();
private final RSocket rsocket;
@Nullable
private final MimeType dataMimeType;
private final RSocketStrategies strategies;
private DataBuffer emptyDataBuffer;
DefaultRSocketRequester(RSocket rsocket, @Nullable MimeType dataMimeType, RSocketStrategies strategies) {
Assert.notNull(rsocket, "RSocket is required");
Assert.notNull(strategies, "RSocketStrategies is required");
this.rsocket = rsocket;
this.dataMimeType = dataMimeType;
this.strategies = strategies;
this.emptyDataBuffer = this.strategies.dataBufferFactory().wrap(new byte[0]);
}
@Override
public RSocket rsocket() {
return this.rsocket;
}
@Override
public RequestSpec route(String route) {
return new DefaultRequestSpec(route);
}
private static boolean isVoid(ResolvableType elementType) {
return Void.class.equals(elementType.resolve()) || void.class.equals(elementType.resolve());
}
private class DefaultRequestSpec implements RequestSpec {
private final String route;
DefaultRequestSpec(String route) {
this.route = route;
}
@Override
public ResponseSpec data(Object data) {
Assert.notNull(data, "'data' must not be null");
return toResponseSpec(data, ResolvableType.NONE);
}
@Override
public <T, P extends Publisher<T>> ResponseSpec data(P publisher, Class<T> dataType) {
Assert.notNull(publisher, "'publisher' must not be null");
Assert.notNull(dataType, "'dataType' must not be null");
return toResponseSpec(publisher, ResolvableType.forClass(dataType));
}
@Override
public <T, P extends Publisher<T>> ResponseSpec data(P publisher, ParameterizedTypeReference<T> dataTypeRef) {
Assert.notNull(publisher, "'publisher' must not be null");
Assert.notNull(dataTypeRef, "'dataTypeRef' must not be null");
return toResponseSpec(publisher, ResolvableType.forType(dataTypeRef));
}
private ResponseSpec toResponseSpec(Object input, ResolvableType dataType) {
ReactiveAdapter adapter = strategies.reactiveAdapterRegistry().getAdapter(input.getClass());
Publisher<?> publisher;
if (input instanceof Publisher) {
publisher = (Publisher<?>) input;
}
else if (adapter != null) {
publisher = adapter.toPublisher(input);
}
else {
Mono<Payload> payloadMono = encodeValue(input, ResolvableType.forInstance(input), null)
.map(this::firstPayload)
.switchIfEmpty(emptyPayload());
return new DefaultResponseSpec(payloadMono);
}
if (isVoid(dataType) || (adapter != null && adapter.isNoValue())) {
Mono<Payload> payloadMono = Mono.when(publisher).then(emptyPayload());
return new DefaultResponseSpec(payloadMono);
}
Encoder<?> encoder = dataType != ResolvableType.NONE && !Object.class.equals(dataType.resolve()) ?
strategies.encoder(dataType, dataMimeType) : null;
if (adapter != null && !adapter.isMultiValue()) {
Mono<Payload> payloadMono = Mono.from(publisher)
.flatMap(value -> encodeValue(value, dataType, encoder))
.map(this::firstPayload)
.switchIfEmpty(emptyPayload());
return new DefaultResponseSpec(payloadMono);
}
Flux<Payload> payloadFlux = Flux.from(publisher)
.concatMap(value -> encodeValue(value, dataType, encoder))
.switchOnFirst((signal, inner) -> {
DataBuffer data = signal.get();
if (data != null) {
return Flux.concat(
Mono.just(firstPayload(data)),
inner.skip(1).map(PayloadUtils::createPayload));
}
else {
return inner.map(PayloadUtils::createPayload);
}
})
.switchIfEmpty(emptyPayload());
return new DefaultResponseSpec(payloadFlux);
}
@SuppressWarnings("unchecked")
private <T> Mono<DataBuffer> encodeValue(T value, ResolvableType valueType, @Nullable Encoder<?> encoder) {
if (encoder == null) {
encoder = strategies.encoder(ResolvableType.forInstance(value), dataMimeType);
}
return DataBufferUtils.join(((Encoder<T>) encoder).encode(
Mono.just(value), strategies.dataBufferFactory(), valueType, dataMimeType, EMPTY_HINTS));
}
private Payload firstPayload(DataBuffer data) {
return PayloadUtils.createPayload(getMetadata(), data);
}
private Mono<Payload> emptyPayload() {
return Mono.fromCallable(() -> firstPayload(emptyDataBuffer));
}
private DataBuffer getMetadata() {
return strategies.dataBufferFactory().wrap(this.route.getBytes(StandardCharsets.UTF_8));
}
}
private class DefaultResponseSpec implements ResponseSpec {
@Nullable
private final Mono<Payload> payloadMono;
@Nullable
private final Flux<Payload> payloadFlux;
DefaultResponseSpec(Mono<Payload> payloadMono) {
this.payloadMono = payloadMono;
this.payloadFlux = null;
}
DefaultResponseSpec(Flux<Payload> payloadFlux) {
this.payloadMono = null;
this.payloadFlux = payloadFlux;
}
@Override
public Mono<Void> send() {
Assert.notNull(this.payloadMono, "No RSocket interaction model for one-way send with Flux.");
return this.payloadMono.flatMap(rsocket::fireAndForget);
}
@Override
public <T> Mono<T> retrieveMono(Class<T> dataType) {
return retrieveMono(ResolvableType.forClass(dataType));
}
@Override
public <T> Mono<T> retrieveMono(ParameterizedTypeReference<T> dataTypeRef) {
return retrieveMono(ResolvableType.forType(dataTypeRef));
}
@Override
public <T> Flux<T> retrieveFlux(Class<T> dataType) {
return retrieveFlux(ResolvableType.forClass(dataType));
}
@Override
public <T> Flux<T> retrieveFlux(ParameterizedTypeReference<T> dataTypeRef) {
return retrieveFlux(ResolvableType.forType(dataTypeRef));
}
@SuppressWarnings("unchecked")
private <T> Mono<T> retrieveMono(ResolvableType elementType) {
Assert.notNull(this.payloadMono,
"No RSocket interaction model for Flux request to Mono response.");
Mono<Payload> payloadMono = this.payloadMono.flatMap(rsocket::requestResponse);
if (isVoid(elementType)) {
return (Mono<T>) payloadMono.then();
}
Decoder<?> decoder = strategies.decoder(elementType, dataMimeType);
return (Mono<T>) decoder.decodeToMono(
payloadMono.map(this::retainDataAndReleasePayload), elementType, dataMimeType, EMPTY_HINTS);
}
@SuppressWarnings("unchecked")
private <T> Flux<T> retrieveFlux(ResolvableType elementType) {
Flux<Payload> payloadFlux = this.payloadMono != null ?
this.payloadMono.flatMapMany(rsocket::requestStream) :
rsocket.requestChannel(this.payloadFlux);
if (isVoid(elementType)) {
return payloadFlux.thenMany(Flux.empty());
}
Decoder<?> decoder = strategies.decoder(elementType, dataMimeType);
return payloadFlux.map(this::retainDataAndReleasePayload).concatMap(dataBuffer ->
(Mono<T>) decoder.decodeToMono(Mono.just(dataBuffer), elementType, dataMimeType, EMPTY_HINTS));
}
private DataBuffer retainDataAndReleasePayload(Payload payload) {
return PayloadUtils.retainDataAndReleasePayload(payload, strategies.dataBufferFactory());
}
}
}

View File

@ -0,0 +1,140 @@
/*
* 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.rsocket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Default, package-private {@link RSocketStrategies} implementation.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
final class DefaultRSocketStrategies implements RSocketStrategies {
private final List<Encoder<?>> encoders;
private final List<Decoder<?>> decoders;
private final ReactiveAdapterRegistry adapterRegistry;
private final DataBufferFactory bufferFactory;
private DefaultRSocketStrategies(
List<Encoder<?>> encoders, List<Decoder<?>> decoders,
ReactiveAdapterRegistry adapterRegistry, DataBufferFactory bufferFactory) {
this.encoders = Collections.unmodifiableList(encoders);
this.decoders = Collections.unmodifiableList(decoders);
this.adapterRegistry = adapterRegistry;
this.bufferFactory = bufferFactory;
}
@Override
public List<Encoder<?>> encoders() {
return this.encoders;
}
@Override
public List<Decoder<?>> decoders() {
return this.decoders;
}
@Override
public ReactiveAdapterRegistry reactiveAdapterRegistry() {
return this.adapterRegistry;
}
@Override
public DataBufferFactory dataBufferFactory() {
return this.bufferFactory;
}
/**
* Default RSocketStrategies.Builder implementation.
*/
static class DefaultRSocketStrategiesBuilder implements RSocketStrategies.Builder {
private final List<Encoder<?>> encoders = new ArrayList<>();
private final List<Decoder<?>> decoders = new ArrayList<>();
private ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
@Nullable
private DataBufferFactory dataBufferFactory;
@Override
public Builder encoder(Encoder<?>... encoders) {
this.encoders.addAll(Arrays.asList(encoders));
return this;
}
@Override
public Builder decoder(Decoder<?>... decoder) {
this.decoders.addAll(Arrays.asList(decoder));
return this;
}
@Override
public Builder encoders(Consumer<List<Encoder<?>>> consumer) {
consumer.accept(this.encoders);
return this;
}
@Override
public Builder decoders(Consumer<List<Decoder<?>>> consumer) {
consumer.accept(this.decoders);
return this;
}
@Override
public Builder reactiveAdapterStrategy(ReactiveAdapterRegistry registry) {
Assert.notNull(registry, "ReactiveAdapterRegistry is required");
this.adapterRegistry = registry;
return this;
}
@Override
public Builder dataBufferFactory(DataBufferFactory bufferFactory) {
this.dataBufferFactory = bufferFactory;
return this;
}
@Override
public RSocketStrategies build() {
return new DefaultRSocketStrategies(this.encoders, this.decoders, this.adapterRegistry,
this.dataBufferFactory != null ? this.dataBufferFactory : new DefaultDataBufferFactory());
}
}
}

View File

@ -0,0 +1,77 @@
/*
* 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.rsocket;
import java.util.function.Function;
import io.rsocket.ConnectionSetupPayload;
import io.rsocket.RSocket;
import io.rsocket.SocketAcceptor;
import reactor.core.publisher.Mono;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.util.MimeType;
/**
* Extension of {@link RSocketMessageHandler} that can be plugged directly into
* RSocket to receive connections either on the
* {@link io.rsocket.RSocketFactory.ClientRSocketFactory#acceptor(Function) client} or on the
* {@link io.rsocket.RSocketFactory.ServerRSocketFactory#acceptor(SocketAcceptor) server}
* side. Requests are handled by delegating to the "super" {@link #handleMessage(Message)}.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public final class MessageHandlerAcceptor extends RSocketMessageHandler
implements SocketAcceptor, Function<RSocket, RSocket> {
@Nullable
private MimeType defaultDataMimeType;
/**
* Configure the default content type to use for data payloads.
* <p>By default this is not set. However a server acceptor will use the
* content type from the {@link ConnectionSetupPayload}, so this is typically
* required for clients but can also be used on servers as a fallback.
* @param defaultDataMimeType the MimeType to use
*/
public void setDefaultDataMimeType(@Nullable MimeType defaultDataMimeType) {
this.defaultDataMimeType = defaultDataMimeType;
}
@Override
public Mono<RSocket> accept(ConnectionSetupPayload setupPayload, RSocket sendingRSocket) {
MessagingRSocket rsocket = createRSocket(sendingRSocket);
// Allow handling of the ConnectionSetupPayload via @MessageMapping methods.
// However, if the handling is to make requests to the client, it's expected
// it will do so decoupled from the handling, e.g. via .subscribe().
return rsocket.handleConnectionSetupPayload(setupPayload).then(Mono.just(rsocket));
}
@Override
public RSocket apply(RSocket sendingRSocket) {
return createRSocket(sendingRSocket);
}
private MessagingRSocket createRSocket(RSocket rsocket) {
return new MessagingRSocket(
this::handleMessage, rsocket, this.defaultDataMimeType, getRSocketStrategies());
}
}

View File

@ -0,0 +1,195 @@
/*
* 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.rsocket;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import io.rsocket.AbstractRSocket;
import io.rsocket.ConnectionSetupPayload;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBuffer;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.DestinationPatternsMessageCondition;
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodReturnValueHandler;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.StringUtils;
/**
* Implementation of {@link RSocket} that wraps incoming requests with a
* {@link Message}, delegates to a {@link Function} for handling, and then
* obtains the response from a "reply" header.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
class MessagingRSocket extends AbstractRSocket {
private final Function<Message<?>, Mono<Void>> handler;
private final RSocketRequester requester;
@Nullable
private MimeType dataMimeType;
private final RSocketStrategies strategies;
MessagingRSocket(Function<Message<?>, Mono<Void>> handler, RSocket sendingRSocket,
@Nullable MimeType defaultDataMimeType, RSocketStrategies strategies) {
Assert.notNull(handler, "'handler' is required");
Assert.notNull(sendingRSocket, "'sendingRSocket' is required");
this.handler = handler;
this.requester = RSocketRequester.create(sendingRSocket, defaultDataMimeType, strategies);
this.dataMimeType = defaultDataMimeType;
this.strategies = strategies;
}
/**
* Wrap the {@link ConnectionSetupPayload} with a {@link Message} and
* delegate to {@link #handle(Payload)} for handling.
* @param payload the connection payload
* @return completion handle for success or error
*/
public Mono<Void> handleConnectionSetupPayload(ConnectionSetupPayload payload) {
if (StringUtils.hasText(payload.dataMimeType())) {
this.dataMimeType = MimeTypeUtils.parseMimeType(payload.dataMimeType());
}
// frameDecoder does not apply to connectionSetupPayload
// so retain here since handle expects it..
payload.retain();
return handle(payload);
}
@Override
public Mono<Void> fireAndForget(Payload payload) {
return handle(payload);
}
@Override
public Mono<Payload> requestResponse(Payload payload) {
return handleAndReply(payload, Flux.just(payload)).next();
}
@Override
public Flux<Payload> requestStream(Payload payload) {
return handleAndReply(payload, Flux.just(payload));
}
@Override
public Flux<Payload> requestChannel(Publisher<Payload> payloads) {
return Flux.from(payloads)
.switchOnFirst((signal, innerFlux) -> {
Payload firstPayload = signal.get();
return firstPayload == null ? innerFlux : handleAndReply(firstPayload, innerFlux);
});
}
@Override
public Mono<Void> metadataPush(Payload payload) {
// Not very useful until createHeaders does more with metadata
return handle(payload);
}
private Mono<Void> handle(Payload payload) {
String destination = getDestination(payload);
MessageHeaders headers = createHeaders(destination, null);
DataBuffer dataBuffer = retainDataAndReleasePayload(payload);
int refCount = refCount(dataBuffer);
Message<?> message = MessageBuilder.createMessage(dataBuffer, headers);
return Mono.defer(() -> this.handler.apply(message))
.doFinally(s -> {
if (refCount(dataBuffer) == refCount) {
DataBufferUtils.release(dataBuffer);
}
});
}
private int refCount(DataBuffer dataBuffer) {
return dataBuffer instanceof NettyDataBuffer ?
((NettyDataBuffer) dataBuffer).getNativeBuffer().refCnt() : 1;
}
private Flux<Payload> handleAndReply(Payload firstPayload, Flux<Payload> payloads) {
MonoProcessor<Flux<Payload>> replyMono = MonoProcessor.create();
String destination = getDestination(firstPayload);
MessageHeaders headers = createHeaders(destination, replyMono);
AtomicBoolean read = new AtomicBoolean();
Flux<DataBuffer> buffers = payloads.map(this::retainDataAndReleasePayload).doOnSubscribe(s -> read.set(true));
Message<Flux<DataBuffer>> message = MessageBuilder.createMessage(buffers, headers);
return Mono.defer(() -> this.handler.apply(message))
.doFinally(s -> {
// Subscription should have happened by now due to ChannelSendOperator
if (!read.get()) {
buffers.subscribe(DataBufferUtils::release);
}
})
.thenMany(Flux.defer(() -> replyMono.isTerminated() ?
replyMono.flatMapMany(Function.identity()) :
Mono.error(new IllegalStateException("Something went wrong: reply Mono not set"))));
}
private String getDestination(Payload payload) {
// TODO:
// For now treat the metadata as a simple string with routing information.
// We'll have to get more sophisticated once the routing extension is completed.
// https://github.com/rsocket/rsocket-java/issues/568
return payload.getMetadataUtf8();
}
private DataBuffer retainDataAndReleasePayload(Payload payload) {
return PayloadUtils.retainDataAndReleasePayload(payload, this.strategies.dataBufferFactory());
}
private MessageHeaders createHeaders(String destination, @Nullable MonoProcessor<?> replyMono) {
MessageHeaderAccessor headers = new MessageHeaderAccessor();
headers.setHeader(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, destination);
if (this.dataMimeType != null) {
headers.setContentType(this.dataMimeType);
}
headers.setHeader(RSocketRequesterMethodArgumentResolver.RSOCKET_REQUESTER_HEADER, this.requester);
if (replyMono != null) {
headers.setHeader(RSocketPayloadReturnValueHandler.RESPONSE_HEADER, replyMono);
}
DataBufferFactory bufferFactory = this.strategies.dataBufferFactory();
headers.setHeader(HandlerMethodReturnValueHandler.DATA_BUFFER_FACTORY_HEADER, bufferFactory);
return headers.getMessageHeaders();
}
}

View File

@ -0,0 +1,107 @@
/*
* 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.rsocket;
import io.netty.buffer.ByteBuf;
import io.rsocket.Frame;
import io.rsocket.Payload;
import io.rsocket.util.ByteBufPayload;
import io.rsocket.util.DefaultPayload;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBuffer;
import org.springframework.core.io.buffer.NettyDataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.util.Assert;
/**
* Static utility methods to create {@link Payload} from {@link DataBuffer}s
* and vice versa.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
abstract class PayloadUtils {
/**
* Use this method to slice, retain and wrap the data portion of the
* {@code Payload}, and also to release the {@code Payload}. This assumes
* the Payload metadata has been read by now and ensures downstream code
* need only be aware of {@code DataBuffer}s.
* @param payload the payload to process
* @param bufferFactory the DataBufferFactory to wrap with
* @return the created {@code DataBuffer} instance
*/
public static DataBuffer retainDataAndReleasePayload(Payload payload, DataBufferFactory bufferFactory) {
try {
if (bufferFactory instanceof NettyDataBufferFactory) {
ByteBuf byteBuf = payload.sliceData().retain();
return ((NettyDataBufferFactory) bufferFactory).wrap(byteBuf);
}
Assert.isTrue(!(payload instanceof ByteBufPayload) && !(payload instanceof Frame),
"NettyDataBufferFactory expected, actual: " + bufferFactory.getClass().getSimpleName());
return bufferFactory.wrap(payload.getData());
}
finally {
if (payload.refCnt() > 0) {
payload.release();
}
}
}
/**
* Create a Payload from the given metadata and data.
* @param metadata the metadata part for the payload
* @param data the data part for the payload
* @return the created Payload
*/
public static Payload createPayload(DataBuffer metadata, DataBuffer data) {
if (metadata instanceof NettyDataBuffer && data instanceof NettyDataBuffer) {
return ByteBufPayload.create(
((NettyDataBuffer) data).getNativeBuffer(),
((NettyDataBuffer) metadata).getNativeBuffer());
}
else if (metadata instanceof DefaultDataBuffer && data instanceof DefaultDataBuffer) {
return DefaultPayload.create(
((DefaultDataBuffer) data).getNativeBuffer(),
((DefaultDataBuffer) metadata).getNativeBuffer());
}
else {
return DefaultPayload.create(data.asByteBuffer(), metadata.asByteBuffer());
}
}
/**
* Create a Payload from the given data.
* @param data the data part for the payload
* @return the created Payload
*/
public static Payload createPayload(DataBuffer data) {
if (data instanceof NettyDataBuffer) {
return ByteBufPayload.create(((NettyDataBuffer) data).getNativeBuffer());
}
else if (data instanceof DefaultDataBuffer) {
return DefaultPayload.create(((DefaultDataBuffer) data).getNativeBuffer());
}
else {
return DefaultPayload.create(data.asByteBuffer());
}
}
}

View File

@ -0,0 +1,125 @@
/*
* 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.rsocket;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.handler.annotation.support.reactive.MessageMappingMessageHandler;
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodReturnValueHandler;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* RSocket-specific extension of {@link MessageMappingMessageHandler}.
*
* <p>The configured {@link #setEncoders(List) encoders} are used to encode the
* return values from handler methods, with the help of
* {@link RSocketPayloadReturnValueHandler}.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public class RSocketMessageHandler extends MessageMappingMessageHandler {
private final List<Encoder<?>> encoders = new ArrayList<>();
@Nullable
private RSocketStrategies rsocketStrategies;
/**
* Configure the encoders to use for encoding handler method return values.
*/
public void setEncoders(List<? extends Encoder<?>> encoders) {
this.encoders.addAll(encoders);
}
/**
* Return the configured {@link #setEncoders(List) encoders}.
*/
public List<? extends Encoder<?>> getEncoders() {
return this.encoders;
}
/**
* Provide configuration in the form of {@link RSocketStrategies}. This is
* an alternative to using {@link #setEncoders(List)},
* {@link #setDecoders(List)}, and others directly. It is convenient when
* you also configuring an {@link RSocketRequester} in which case the
* {@link RSocketStrategies} encapsulates required configuration for re-use.
* @param rsocketStrategies the strategies to use
*/
public void setRSocketStrategies(RSocketStrategies rsocketStrategies) {
Assert.notNull(rsocketStrategies, "RSocketStrategies must not be null");
this.rsocketStrategies = rsocketStrategies;
setDecoders(rsocketStrategies.decoders());
setEncoders(rsocketStrategies.encoders());
setReactiveAdapterRegistry(rsocketStrategies.reactiveAdapterRegistry());
}
/**
* Return the {@code RSocketStrategies} instance provided via
* {@link #setRSocketStrategies rsocketStrategies}, or
* otherwise initialize it with the configured {@link #setEncoders(List)
* encoders}, {@link #setDecoders(List) decoders}, and others.
*/
public RSocketStrategies getRSocketStrategies() {
if (this.rsocketStrategies == null) {
this.rsocketStrategies = RSocketStrategies.builder()
.decoder(getDecoders().toArray(new Decoder<?>[0]))
.encoder(getEncoders().toArray(new Encoder<?>[0]))
.reactiveAdapterStrategy(getReactiveAdapterRegistry())
.build();
}
return this.rsocketStrategies;
}
@Override
public void afterPropertiesSet() {
getArgumentResolverConfigurer().addCustomResolver(new RSocketRequesterMethodArgumentResolver());
super.afterPropertiesSet();
}
@Override
protected List<? extends HandlerMethodReturnValueHandler> initReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>();
handlers.add(new RSocketPayloadReturnValueHandler(this.encoders, getReactiveAdapterRegistry()));
handlers.addAll(getReturnValueHandlerConfigurer().getCustomHandlers());
return handlers;
}
@Override
protected void handleNoMatch(@Nullable String destination, Message<?> message) {
// MessagingRSocket will raise an error anyway if reply Mono is expected
// Here we raise a more helpful message a destination is present
// It is OK if some messages (ConnectionSetupPayload, metadataPush) are not handled
// We need a better way to avoid raising errors for those
if (StringUtils.hasText(destination)) {
throw new MessageDeliveryException("No handler for destination '" + destination + "'");
}
}
}

View File

@ -0,0 +1,86 @@
/*
* 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.rsocket;
import java.util.List;
import io.rsocket.Payload;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
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.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.invocation.reactive.AbstractEncoderMethodReturnValueHandler;
import org.springframework.util.Assert;
/**
* Extension of {@link AbstractEncoderMethodReturnValueHandler} that
* {@link #handleEncodedContent handles} encoded content by wrapping data buffers
* as RSocket payloads and by passing those to the {@link MonoProcessor}
* from the {@link #RESPONSE_HEADER} header.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public class RSocketPayloadReturnValueHandler extends AbstractEncoderMethodReturnValueHandler {
/**
* Message header name that is expected to have a {@link MonoProcessor}
* which will receive the {@code Flux<Payload>} that represents the response.
*/
public static final String RESPONSE_HEADER = "rsocketResponse";
public RSocketPayloadReturnValueHandler(List<Encoder<?>> encoders, ReactiveAdapterRegistry registry) {
super(encoders, registry);
}
@Override
@SuppressWarnings("unchecked")
protected Mono<Void> handleEncodedContent(
Flux<DataBuffer> encodedContent, MethodParameter returnType, Message<?> message) {
MonoProcessor<Flux<Payload>> replyMono = getReplyMono(message);
Assert.notNull(replyMono, "Missing '" + RESPONSE_HEADER + "'");
replyMono.onNext(encodedContent.map(PayloadUtils::createPayload));
replyMono.onComplete();
return Mono.empty();
}
@Override
protected Mono<Void> handleNoContent(MethodParameter returnType, Message<?> message) {
MonoProcessor<Flux<Payload>> replyMono = getReplyMono(message);
if (replyMono != null) {
replyMono.onComplete();
}
return Mono.empty();
}
@Nullable
@SuppressWarnings("unchecked")
private MonoProcessor<Flux<Payload>> getReplyMono(Message<?> message) {
Object headerValue = message.getHeaders().get(RESPONSE_HEADER);
Assert.state(headerValue == null || headerValue instanceof MonoProcessor, "Expected MonoProcessor");
return (MonoProcessor<Flux<Payload>>) headerValue;
}
}

View File

@ -0,0 +1,166 @@
/*
* 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.rsocket;
import io.rsocket.RSocket;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.lang.Nullable;
import org.springframework.util.MimeType;
/**
* A thin wrapper around a sending {@link RSocket} with a fluent API accepting
* and returning higher level Objects for input and for output, along with
* methods specify routing and other metadata.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public interface RSocketRequester {
/**
* Return the underlying RSocket used to make requests.
*/
RSocket rsocket();
/**
* Create a new {@code RSocketRequester} from the given {@link RSocket} and
* strategies for encoding and decoding request and response payloads.
* @param rsocket the sending RSocket to use
* @param dataMimeType the MimeType for data (from the SETUP frame)
* @param strategies encoders, decoders, and others
* @return the created RSocketRequester wrapper
*/
static RSocketRequester create(RSocket rsocket, @Nullable MimeType dataMimeType, RSocketStrategies strategies) {
return new DefaultRSocketRequester(rsocket, dataMimeType, strategies);
}
// For now we treat metadata as a simple string that is the route.
// This will change after the resolution of:
// https://github.com/rsocket/rsocket-java/issues/568
/**
* Entry point to prepare a new request to the given route.
*
* <p>For requestChannel interactions, i.e. Flux-to-Flux the metadata is
* attached to the first request payload.
*
* @param route the routing destination
* @return a spec for further defining and executing the reuqest
*/
RequestSpec route(String route);
/**
* Contract to provide input data for an RSocket request.
*/
interface RequestSpec {
/**
* Provide request payload data. The given Object may be a synchronous
* value, or a {@link Publisher} of values, or another async type that's
* registered in the configured {@link ReactiveAdapterRegistry}.
* <p>For multivalued Publishers, prefer using
* {@link #data(Publisher, Class)} or
* {@link #data(Publisher, ParameterizedTypeReference)} since that makes
* it possible to find a compatible {@code Encoder} up front vs looking
* it up on every value.
* @param data the Object to use for payload data
* @return spec for declaring the expected response
*/
ResponseSpec data(Object data);
/**
* Provide a {@link Publisher} of value(s) for request payload data.
* <p>Publisher semantics determined through the configured
* {@link ReactiveAdapterRegistry} influence which of the 4 RSocket
* interactions to use. Publishers with unknown semantics are treated
* as multivalued. Consider registering a reactive type adapter, or
* passing {@code Mono.from(publisher)}.
* <p>If the publisher completes empty, possibly {@code Publisher<Void>},
* the request will have an empty data Payload.
* @param publisher source of payload data value(s)
* @param dataType the type of values to be published
* @param <T> the type of element values
* @param <P> the type of publisher
* @return spec for declaring the expected response
*/
<T, P extends Publisher<T>> ResponseSpec data(P publisher, Class<T> dataType);
/**
* Variant of {@link #data(Publisher, Class)} for when the dataType has
* to have a generic type. See {@link ParameterizedTypeReference}.
*/
<T, P extends Publisher<T>> ResponseSpec data(P publisher, ParameterizedTypeReference<T> dataTypeRef);
}
/**
* Contract to declare the expected RSocket response.
*/
interface ResponseSpec {
/**
* Perform {@link RSocket#fireAndForget fireAndForget}.
*/
Mono<Void> send();
/**
* Perform {@link RSocket#requestResponse requestResponse}. If the
* expected data type is {@code Void.class}, the returned {@code Mono}
* will complete after all data is consumed.
* <p><strong>Note:</strong> Use of this method will raise an error if
* the request payload is a multivalued {@link Publisher} as
* determined through the configured {@link ReactiveAdapterRegistry}.
* @param dataType the expected data type for the response
* @param <T> parameter for the expected data type
* @return the decoded response
*/
<T> Mono<T> retrieveMono(Class<T> dataType);
/**
* Variant of {@link #retrieveMono(Class)} for when the dataType has
* to have a generic type. See {@link ParameterizedTypeReference}.
*/
<T> Mono<T> retrieveMono(ParameterizedTypeReference<T> dataTypeRef);
/**
* Perform {@link RSocket#requestStream requestStream} or
* {@link RSocket#requestChannel requestChannel} depending on whether
* the request input consists of a single or multiple payloads.
* If the expected data type is {@code Void.class}, the returned
* {@code Flux} will complete after all data is consumed.
* @param dataType the expected type for values in the response
* @param <T> parameterize the expected type of values
* @return the decoded response
*/
<T> Flux<T> retrieveFlux(Class<T> dataType);
/**
* Variant of {@link #retrieveFlux(Class)} for when the dataType has
* to have a generic type. See {@link ParameterizedTypeReference}.
*/
<T> Flux<T> retrieveFlux(ParameterizedTypeReference<T> dataTypeRef);
}
}

View File

@ -0,0 +1,70 @@
/*
* 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.rsocket;
import io.rsocket.RSocket;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver;
import org.springframework.util.Assert;
/**
* Resolves arguments of type {@link RSocket} that can be used for making
* requests to the remote peer.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public class RSocketRequesterMethodArgumentResolver implements HandlerMethodArgumentResolver {
/**
* Message header name that is expected to have the {@link RSocket} to
* initiate new interactions to the remote peer with.
*/
public static final String RSOCKET_REQUESTER_HEADER = "rsocketRequester";
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> type = parameter.getParameterType();
return RSocketRequester.class.equals(type) || RSocket.class.isAssignableFrom(type);
}
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) {
Object headerValue = message.getHeaders().get(RSOCKET_REQUESTER_HEADER);
Assert.notNull(headerValue, "Missing '" + RSOCKET_REQUESTER_HEADER + "'");
Assert.isInstanceOf(RSocketRequester.class, headerValue, "Expected header value of type RSocketRequester");
RSocketRequester requester = (RSocketRequester) headerValue;
Class<?> type = parameter.getParameterType();
if (RSocketRequester.class.equals(type)) {
return Mono.just(requester);
}
else if (RSocket.class.isAssignableFrom(type)) {
return Mono.just(requester.rsocket());
}
else {
return Mono.error(new IllegalArgumentException("Unexpected parameter type: " + parameter));
}
}
}

View File

@ -0,0 +1,171 @@
/*
* 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.rsocket;
import java.util.List;
import java.util.function.Consumer;
import io.netty.buffer.PooledByteBufAllocator;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.MimeType;
/**
* Access to strategies for use by RSocket requester and responder components.
*
* @author Rossen Stoyanchev
* @since 5.2
*/
public interface RSocketStrategies {
/**
* Return the configured {@link Builder#encoder(Encoder[]) encoders}.
* @see #encoder(ResolvableType, MimeType)
*/
List<Encoder<?>> encoders();
/**
* Find a compatible Encoder for the given element type.
* @param elementType the element type to match
* @param mimeType the MimeType to match
* @param <T> for casting the Encoder to the expected element type
* @return the matching Encoder
* @throws IllegalArgumentException if no matching Encoder is found
*/
@SuppressWarnings("unchecked")
default <T> Encoder<T> encoder(ResolvableType elementType, @Nullable MimeType mimeType) {
for (Encoder<?> encoder : encoders()) {
if (encoder.canEncode(elementType, mimeType)) {
return (Encoder<T>) encoder;
}
}
throw new IllegalArgumentException("No encoder for " + elementType);
}
/**
* Return the configured {@link Builder#decoder(Decoder[]) decoders}.
* @see #decoder(ResolvableType, MimeType)
*/
List<Decoder<?>> decoders();
/**
* Find a compatible Decoder for the given element type.
* @param elementType the element type to match
* @param mimeType the MimeType to match
* @param <T> for casting the Decoder to the expected element type
* @return the matching Decoder
* @throws IllegalArgumentException if no matching Decoder is found
*/
@SuppressWarnings("unchecked")
default <T> Decoder<T> decoder(ResolvableType elementType, @Nullable MimeType mimeType) {
for (Decoder<?> decoder : decoders()) {
if (decoder.canDecode(elementType, mimeType)) {
return (Decoder<T>) decoder;
}
}
throw new IllegalArgumentException("No decoder for " + elementType);
}
/**
* Return the configured
* {@link Builder#reactiveAdapterStrategy(ReactiveAdapterRegistry) reactiveAdapterRegistry}.
*/
ReactiveAdapterRegistry reactiveAdapterRegistry();
/**
* Return the configured
* {@link Builder#dataBufferFactory(DataBufferFactory) dataBufferFactory}.
*/
DataBufferFactory dataBufferFactory();
/**
* Return a builder to build a new {@code RSocketStrategies} instance.
*/
static Builder builder() {
return new DefaultRSocketStrategies.DefaultRSocketStrategiesBuilder();
}
/**
* The builder options for creating {@code RSocketStrategies}.
*/
interface Builder {
/**
* Add encoders to use for serializing Objects.
* <p>By default this is empty.
*/
Builder encoder(Encoder<?>... encoder);
/**
* Add decoders for de-serializing Objects.
* <p>By default this is empty.
*/
Builder decoder(Decoder<?>... decoder);
/**
* Access and manipulate the list of configured {@link #encoder encoders}.
*/
Builder encoders(Consumer<List<Encoder<?>>> consumer);
/**
* Access and manipulate the list of configured {@link #encoder decoders}.
*/
Builder decoders(Consumer<List<Decoder<?>>> consumer);
/**
* Configure the registry for reactive type support. This can be used to
* to adapt to, and/or determine the semantics of a given
* {@link org.reactivestreams.Publisher Publisher}.
* <p>By default this {@link ReactiveAdapterRegistry#sharedInstance}.
* @param registry the registry to use
*/
Builder reactiveAdapterStrategy(ReactiveAdapterRegistry registry);
/**
* Configure the DataBufferFactory to use for allocating buffers, for
* example when preparing requests or when responding. The choice here
* must be aligned with the frame decoder configured in
* {@link io.rsocket.RSocketFactory}.
* <p>By default this property is an instance of
* {@link org.springframework.core.io.buffer.DefaultDataBufferFactory
* DefaultDataBufferFactory} matching to the default frame decoder in
* {@link io.rsocket.RSocketFactory} which copies the payload. This
* comes at cost to performance but does not require reference counting
* and eliminates possibility for memory leaks.
* <p>To switch to a zero-copy strategy,
* <a href="https://github.com/rsocket/rsocket-java#zero-copy">configure RSocket</a>
* accordingly, and then configure this property with an instance of
* {@link org.springframework.core.io.buffer.NettyDataBufferFactory
* NettyDataBufferFactory} with a pooled allocator such as
* {@link PooledByteBufAllocator#DEFAULT}.
* @param bufferFactory the DataBufferFactory to use
*/
Builder dataBufferFactory(DataBufferFactory bufferFactory);
/**
* Builder the {@code RSocketStrategies} instance.
*/
RSocketStrategies build();
}
}

View File

@ -0,0 +1,9 @@
/**
* Support for the RSocket protocol.
*/
@NonNullApi
@NonNullFields
package org.springframework.messaging.rsocket;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

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.
@ -18,6 +18,7 @@ package org.springframework.messaging.simp;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.CompositeMessageCondition;
import org.springframework.messaging.handler.DestinationPatternsMessageCondition;
import org.springframework.messaging.handler.MessageCondition;
@ -34,62 +35,44 @@ import org.springframework.messaging.handler.MessageCondition;
*/
public class SimpMessageMappingInfo implements MessageCondition<SimpMessageMappingInfo> {
private final SimpMessageTypeMessageCondition messageTypeMessageCondition;
private final DestinationPatternsMessageCondition destinationConditions;
private final CompositeMessageCondition delegate;
public SimpMessageMappingInfo(SimpMessageTypeMessageCondition messageTypeMessageCondition,
DestinationPatternsMessageCondition destinationConditions) {
this.messageTypeMessageCondition = messageTypeMessageCondition;
this.destinationConditions = destinationConditions;
this.delegate = new CompositeMessageCondition(messageTypeMessageCondition, destinationConditions);
}
private SimpMessageMappingInfo(CompositeMessageCondition delegate) {
this.delegate = delegate;
}
public SimpMessageTypeMessageCondition getMessageTypeMessageCondition() {
return this.messageTypeMessageCondition;
return this.delegate.getCondition(SimpMessageTypeMessageCondition.class);
}
public DestinationPatternsMessageCondition getDestinationConditions() {
return this.destinationConditions;
return this.delegate.getCondition(DestinationPatternsMessageCondition.class);
}
@Override
public SimpMessageMappingInfo combine(SimpMessageMappingInfo other) {
SimpMessageTypeMessageCondition typeCond =
this.getMessageTypeMessageCondition().combine(other.getMessageTypeMessageCondition());
DestinationPatternsMessageCondition destCond =
this.destinationConditions.combine(other.getDestinationConditions());
return new SimpMessageMappingInfo(typeCond, destCond);
return new SimpMessageMappingInfo(this.delegate.combine(other.delegate));
}
@Override
@Nullable
public SimpMessageMappingInfo getMatchingCondition(Message<?> message) {
SimpMessageTypeMessageCondition typeCond = this.messageTypeMessageCondition.getMatchingCondition(message);
if (typeCond == null) {
return null;
}
DestinationPatternsMessageCondition destCond = this.destinationConditions.getMatchingCondition(message);
if (destCond == null) {
return null;
}
return new SimpMessageMappingInfo(typeCond, destCond);
CompositeMessageCondition condition = this.delegate.getMatchingCondition(message);
return condition != null ? new SimpMessageMappingInfo(condition) : null;
}
@Override
public int compareTo(SimpMessageMappingInfo other, Message<?> message) {
int result = this.messageTypeMessageCondition.compareTo(other.messageTypeMessageCondition, message);
if (result != 0) {
return result;
}
result = this.destinationConditions.compareTo(other.destinationConditions, message);
if (result != 0) {
return result;
}
return 0;
return this.delegate.compareTo(other.delegate, message);
}
@ -101,19 +84,17 @@ public class SimpMessageMappingInfo implements MessageCondition<SimpMessageMappi
if (!(other instanceof SimpMessageMappingInfo)) {
return false;
}
SimpMessageMappingInfo otherInfo = (SimpMessageMappingInfo) other;
return (this.destinationConditions.equals(otherInfo.destinationConditions) &&
this.messageTypeMessageCondition.equals(otherInfo.messageTypeMessageCondition));
return this.delegate.equals(((SimpMessageMappingInfo) other).delegate);
}
@Override
public int hashCode() {
return (this.destinationConditions.hashCode() * 31 + this.messageTypeMessageCondition.hashCode());
return this.delegate.hashCode();
}
@Override
public String toString() {
return "{" + this.destinationConditions + ",messageType=" + this.messageTypeMessageCondition + '}';
return this.delegate.toString();
}
}

View File

@ -0,0 +1,128 @@
/*
* 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.annotation;
import java.util.function.Predicate;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
/**
* Predicates for messaging annotations.
*
* @author Rossen Stoyanchev
*/
public class MessagingPredicates {
public static DestinationVariablePredicate destinationVar() {
return new DestinationVariablePredicate();
}
public static DestinationVariablePredicate destinationVar(String value) {
return new DestinationVariablePredicate().value(value);
}
public static HeaderPredicate header() {
return new HeaderPredicate();
}
public static HeaderPredicate header(String name) {
return new HeaderPredicate().name(name);
}
public static HeaderPredicate header(String name, String defaultValue) {
return new HeaderPredicate().name(name).defaultValue(defaultValue);
}
public static HeaderPredicate headerPlain() {
return new HeaderPredicate().noAttributes();
}
public static class DestinationVariablePredicate implements Predicate<MethodParameter> {
@Nullable
private String value;
public DestinationVariablePredicate value(@Nullable String name) {
this.value = name;
return this;
}
public DestinationVariablePredicate noValue() {
this.value = "";
return this;
}
@Override
public boolean test(MethodParameter parameter) {
DestinationVariable annotation = parameter.getParameterAnnotation(DestinationVariable.class);
return annotation != null && (this.value == null || annotation.value().equals(this.value));
}
}
public static class HeaderPredicate implements Predicate<MethodParameter> {
@Nullable
private String name;
@Nullable
private Boolean required;
@Nullable
private String defaultValue;
public HeaderPredicate name(@Nullable String name) {
this.name = name;
return this;
}
public HeaderPredicate noName() {
this.name = "";
return this;
}
public HeaderPredicate required(boolean required) {
this.required = required;
return this;
}
public HeaderPredicate defaultValue(@Nullable String value) {
this.defaultValue = value;
return this;
}
public HeaderPredicate noAttributes() {
this.name = "";
this.required = true;
this.defaultValue = ValueConstants.DEFAULT_NONE;
return this;
}
@Override
public boolean test(MethodParameter parameter) {
Header annotation = parameter.getParameterAnnotation(Header.class);
return annotation != null &&
(this.name == null || annotation.name().equals(this.name)) &&
(this.required == null || annotation.required() == this.required) &&
(this.defaultValue == null || annotation.defaultValue().equals(this.defaultValue));
}
}
}

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.
@ -16,23 +16,21 @@
package org.springframework.messaging.handler.annotation.support;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.invocation.ResolvableMethod;
import org.springframework.messaging.support.MessageBuilder;
import static org.junit.Assert.*;
import static org.springframework.messaging.handler.annotation.MessagingPredicates.*;
/**
* Test fixture for {@link DestinationVariableMethodArgumentResolver} tests.
@ -41,33 +39,17 @@ import static org.junit.Assert.*;
*/
public class DestinationVariableMethodArgumentResolverTests {
private DestinationVariableMethodArgumentResolver resolver;
private final DestinationVariableMethodArgumentResolver resolver =
new DestinationVariableMethodArgumentResolver(new DefaultConversionService());
private MethodParameter paramAnnotated;
private MethodParameter paramAnnotatedValue;
private MethodParameter paramNotAnnotated;
private final ResolvableMethod resolvable =
ResolvableMethod.on(getClass()).named("handleMessage").build();
@Before
public void setup() throws Exception {
this.resolver = new DestinationVariableMethodArgumentResolver(new DefaultConversionService());
Method method = getClass().getDeclaredMethod("handleMessage", String.class, String.class, String.class);
this.paramAnnotated = new MethodParameter(method, 0);
this.paramAnnotatedValue = new MethodParameter(method, 1);
this.paramNotAnnotated = new MethodParameter(method, 2);
this.paramAnnotated.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
GenericTypeResolver.resolveParameterType(this.paramAnnotated, DestinationVariableMethodArgumentResolver.class);
this.paramAnnotatedValue.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
GenericTypeResolver.resolveParameterType(this.paramAnnotatedValue, DestinationVariableMethodArgumentResolver.class);
}
@Test
public void supportsParameter() {
assertTrue(resolver.supportsParameter(paramAnnotated));
assertTrue(resolver.supportsParameter(paramAnnotatedValue));
assertFalse(resolver.supportsParameter(paramNotAnnotated));
assertTrue(resolver.supportsParameter(this.resolvable.annot(destinationVar().noValue()).arg()));
assertFalse(resolver.supportsParameter(this.resolvable.annotNotPresent(DestinationVariable.class).arg()));
}
@Test
@ -80,17 +62,19 @@ public class DestinationVariableMethodArgumentResolverTests {
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeader(
DestinationVariableMethodArgumentResolver.DESTINATION_TEMPLATE_VARIABLES_HEADER, vars).build();
Object result = this.resolver.resolveArgument(this.paramAnnotated, message);
MethodParameter param = this.resolvable.annot(destinationVar().noValue()).arg();
Object result = this.resolver.resolveArgument(param, message);
assertEquals("bar", result);
result = this.resolver.resolveArgument(this.paramAnnotatedValue, message);
param = this.resolvable.annot(destinationVar("name")).arg();
result = this.resolver.resolveArgument(param, message);
assertEquals("value", result);
}
@Test(expected = MessageHandlingException.class)
public void resolveArgumentNotFound() throws Exception {
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build();
this.resolver.resolveArgument(this.paramAnnotated, message);
this.resolver.resolveArgument(this.resolvable.annot(destinationVar().noValue()).arg(), message);
}
@SuppressWarnings("unused")

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.
@ -16,7 +16,6 @@
package org.springframework.messaging.handler.annotation.support;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -25,19 +24,17 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.invocation.ResolvableMethod;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.messaging.support.NativeMessageHeaderAccessor;
import org.springframework.util.ReflectionUtils;
import static org.junit.Assert.*;
import static org.springframework.messaging.handler.annotation.MessagingPredicates.*;
/**
* Test fixture for {@link HeaderMethodArgumentResolver} tests.
@ -50,46 +47,27 @@ public class HeaderMethodArgumentResolverTests {
private HeaderMethodArgumentResolver resolver;
private MethodParameter paramRequired;
private MethodParameter paramNamedDefaultValueStringHeader;
private MethodParameter paramSystemPropertyDefaultValue;
private MethodParameter paramSystemPropertyName;
private MethodParameter paramNotAnnotated;
private MethodParameter paramOptional;
private MethodParameter paramNativeHeader;
private final ResolvableMethod resolvable = ResolvableMethod.on(getClass()).named("handleMessage").build();
@Before
public void setup() {
@SuppressWarnings("resource")
GenericApplicationContext cxt = new GenericApplicationContext();
cxt.refresh();
this.resolver = new HeaderMethodArgumentResolver(new DefaultConversionService(), cxt.getBeanFactory());
Method method = ReflectionUtils.findMethod(getClass(), "handleMessage", (Class<?>[]) null);
this.paramRequired = new SynthesizingMethodParameter(method, 0);
this.paramNamedDefaultValueStringHeader = new SynthesizingMethodParameter(method, 1);
this.paramSystemPropertyDefaultValue = new SynthesizingMethodParameter(method, 2);
this.paramSystemPropertyName = new SynthesizingMethodParameter(method, 3);
this.paramNotAnnotated = new SynthesizingMethodParameter(method, 4);
this.paramOptional = new SynthesizingMethodParameter(method, 5);
this.paramNativeHeader = new SynthesizingMethodParameter(method, 6);
this.paramRequired.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
GenericTypeResolver.resolveParameterType(this.paramRequired, HeaderMethodArgumentResolver.class);
GenericApplicationContext context = new GenericApplicationContext();
context.refresh();
this.resolver = new HeaderMethodArgumentResolver(new DefaultConversionService(), context.getBeanFactory());
}
@Test
public void supportsParameter() {
assertTrue(resolver.supportsParameter(paramNamedDefaultValueStringHeader));
assertFalse(resolver.supportsParameter(paramNotAnnotated));
assertTrue(this.resolver.supportsParameter(this.resolvable.annot(headerPlain()).arg()));
assertFalse(this.resolver.supportsParameter(this.resolvable.annotNotPresent(Header.class).arg()));
}
@Test
public void resolveArgument() throws Exception {
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeader("param1", "foo").build();
Object result = this.resolver.resolveArgument(this.paramRequired, message);
Object result = this.resolver.resolveArgument(this.resolvable.annot(headerPlain()).arg(), message);
assertEquals("foo", result);
}
@ -98,7 +76,7 @@ public class HeaderMethodArgumentResolverTests {
TestMessageHeaderAccessor headers = new TestMessageHeaderAccessor();
headers.setNativeHeader("param1", "foo");
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
assertEquals("foo", this.resolver.resolveArgument(this.paramRequired, message));
assertEquals("foo", this.resolver.resolveArgument(this.resolvable.annot(headerPlain()).arg(), message));
}
@Test
@ -108,20 +86,23 @@ public class HeaderMethodArgumentResolverTests {
headers.setNativeHeader("param1", "native-foo");
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
assertEquals("foo", this.resolver.resolveArgument(this.paramRequired, message));
assertEquals("native-foo", this.resolver.resolveArgument(this.paramNativeHeader, message));
assertEquals("foo", this.resolver.resolveArgument(
this.resolvable.annot(headerPlain()).arg(), message));
assertEquals("native-foo", this.resolver.resolveArgument(
this.resolvable.annot(header("nativeHeaders.param1")).arg(), message));
}
@Test(expected = MessageHandlingException.class)
public void resolveArgumentNotFound() throws Exception {
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build();
this.resolver.resolveArgument(this.paramRequired, message);
this.resolver.resolveArgument(this.resolvable.annot(headerPlain()).arg(), message);
}
@Test
public void resolveArgumentDefaultValue() throws Exception {
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build();
Object result = this.resolver.resolveArgument(this.paramNamedDefaultValueStringHeader, message);
Object result = this.resolver.resolveArgument(this.resolvable.annot(header("name", "bar")).arg(), message);
assertEquals("bar", result);
}
@ -130,7 +111,8 @@ public class HeaderMethodArgumentResolverTests {
System.setProperty("systemProperty", "sysbar");
try {
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build();
Object result = resolver.resolveArgument(paramSystemPropertyDefaultValue, message);
MethodParameter param = this.resolvable.annot(header("name", "#{systemProperties.systemProperty}")).arg();
Object result = resolver.resolveArgument(param, message);
assertEquals("sysbar", result);
}
finally {
@ -143,7 +125,8 @@ public class HeaderMethodArgumentResolverTests {
System.setProperty("systemProperty", "sysbar");
try {
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeader("sysbar", "foo").build();
Object result = resolver.resolveArgument(paramSystemPropertyName, message);
MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg();
Object result = resolver.resolveArgument(param, message);
assertEquals("foo", result);
}
finally {
@ -153,31 +136,22 @@ public class HeaderMethodArgumentResolverTests {
@Test
public void resolveOptionalHeaderWithValue() throws Exception {
GenericApplicationContext cxt = new GenericApplicationContext();
cxt.refresh();
HeaderMethodArgumentResolver resolver =
new HeaderMethodArgumentResolver(new DefaultConversionService(), cxt.getBeanFactory());
Message<String> message = MessageBuilder.withPayload("foo").setHeader("foo", "bar").build();
Object result = resolver.resolveArgument(paramOptional, message);
MethodParameter param = this.resolvable.annot(header("foo")).arg(Optional.class, String.class);
Object result = resolver.resolveArgument(param, message);
assertEquals(Optional.of("bar"), result);
}
@Test
public void resolveOptionalHeaderAsEmpty() throws Exception {
GenericApplicationContext cxt = new GenericApplicationContext();
cxt.refresh();
HeaderMethodArgumentResolver resolver =
new HeaderMethodArgumentResolver(new DefaultConversionService(), cxt.getBeanFactory());
Message<String> message = MessageBuilder.withPayload("foo").build();
Object result = resolver.resolveArgument(paramOptional, message);
MethodParameter param = this.resolvable.annot(header("foo")).arg(Optional.class, String.class);
Object result = resolver.resolveArgument(param, message);
assertEquals(Optional.empty(), result);
}
@SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"})
public void handleMessage(
@Header String param1,
@Header(name = "name", defaultValue = "bar") String param2,
@ -191,7 +165,7 @@ public class HeaderMethodArgumentResolverTests {
public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor {
protected TestMessageHeaderAccessor() {
TestMessageHeaderAccessor() {
super((Map<String, List<String>>) null);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 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.
@ -16,17 +16,16 @@
package org.springframework.messaging.handler.annotation.support;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Collections;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.invocation.ResolvableMethod;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.messaging.support.NativeMessageHeaderAccessor;
@ -41,47 +40,31 @@ import static org.junit.Assert.*;
*/
public class HeadersMethodArgumentResolverTests {
private HeadersMethodArgumentResolver resolver;
private final HeadersMethodArgumentResolver resolver = new HeadersMethodArgumentResolver();
private MethodParameter paramAnnotated;
private MethodParameter paramAnnotatedNotMap;
private MethodParameter paramMessageHeaders;
private MethodParameter paramMessageHeaderAccessor;
private MethodParameter paramMessageHeaderAccessorSubclass;
private Message<byte[]> message =
MessageBuilder.withPayload(new byte[0]).copyHeaders(Collections.singletonMap("foo", "bar")).build();
private Message<byte[]> message;
private final ResolvableMethod resolvable = ResolvableMethod.on(getClass()).named("handleMessage").build();
@Before
public void setup() throws Exception {
this.resolver = new HeadersMethodArgumentResolver();
Method method = getClass().getDeclaredMethod("handleMessage", Map.class, String.class,
MessageHeaders.class, MessageHeaderAccessor.class, TestMessageHeaderAccessor.class);
this.paramAnnotated = new MethodParameter(method, 0);
this.paramAnnotatedNotMap = new MethodParameter(method, 1);
this.paramMessageHeaders = new MethodParameter(method, 2);
this.paramMessageHeaderAccessor = new MethodParameter(method, 3);
this.paramMessageHeaderAccessorSubclass = new MethodParameter(method, 4);
Map<String, Object> headers = new HashMap<>();
headers.put("foo", "bar");
this.message = MessageBuilder.withPayload(new byte[0]).copyHeaders(headers).build();
}
@Test
public void supportsParameter() {
assertTrue(this.resolver.supportsParameter(this.paramAnnotated));
assertFalse(this.resolver.supportsParameter(this.paramAnnotatedNotMap));
assertTrue(this.resolver.supportsParameter(this.paramMessageHeaders));
assertTrue(this.resolver.supportsParameter(this.paramMessageHeaderAccessor));
assertTrue(this.resolver.supportsParameter(this.paramMessageHeaderAccessorSubclass));
assertTrue(this.resolver.supportsParameter(
this.resolvable.annotPresent(Headers.class).arg(Map.class, String.class, Object.class)));
assertTrue(this.resolver.supportsParameter(this.resolvable.arg(MessageHeaders.class)));
assertTrue(this.resolver.supportsParameter(this.resolvable.arg(MessageHeaderAccessor.class)));
assertTrue(this.resolver.supportsParameter(this.resolvable.arg(TestMessageHeaderAccessor.class)));
assertFalse(this.resolver.supportsParameter(this.resolvable.annotPresent(Headers.class).arg(String.class)));
}
@Test
public void resolveArgumentAnnotated() throws Exception {
Object resolved = this.resolver.resolveArgument(this.paramAnnotated, this.message);
MethodParameter param = this.resolvable.annotPresent(Headers.class).arg(Map.class, String.class, Object.class);
Object resolved = this.resolver.resolveArgument(param, this.message);
assertTrue(resolved instanceof Map);
@SuppressWarnings("unchecked")
@ -91,12 +74,12 @@ public class HeadersMethodArgumentResolverTests {
@Test(expected = IllegalStateException.class)
public void resolveArgumentAnnotatedNotMap() throws Exception {
this.resolver.resolveArgument(this.paramAnnotatedNotMap, this.message);
this.resolver.resolveArgument(this.resolvable.annotPresent(Headers.class).arg(String.class), this.message);
}
@Test
public void resolveArgumentMessageHeaders() throws Exception {
Object resolved = this.resolver.resolveArgument(this.paramMessageHeaders, this.message);
Object resolved = this.resolver.resolveArgument(this.resolvable.arg(MessageHeaders.class), this.message);
assertTrue(resolved instanceof MessageHeaders);
MessageHeaders headers = (MessageHeaders) resolved;
@ -105,7 +88,8 @@ public class HeadersMethodArgumentResolverTests {
@Test
public void resolveArgumentMessageHeaderAccessor() throws Exception {
Object resolved = this.resolver.resolveArgument(this.paramMessageHeaderAccessor, this.message);
MethodParameter param = this.resolvable.arg(MessageHeaderAccessor.class);
Object resolved = this.resolver.resolveArgument(param, this.message);
assertTrue(resolved instanceof MessageHeaderAccessor);
MessageHeaderAccessor headers = (MessageHeaderAccessor) resolved;
@ -114,7 +98,8 @@ public class HeadersMethodArgumentResolverTests {
@Test
public void resolveArgumentMessageHeaderAccessorSubclass() throws Exception {
Object resolved = this.resolver.resolveArgument(this.paramMessageHeaderAccessorSubclass, this.message);
MethodParameter param = this.resolvable.arg(TestMessageHeaderAccessor.class);
Object resolved = this.resolver.resolveArgument(param, this.message);
assertTrue(resolved instanceof TestMessageHeaderAccessor);
TestMessageHeaderAccessor headers = (TestMessageHeaderAccessor) resolved;
@ -124,7 +109,7 @@ public class HeadersMethodArgumentResolverTests {
@SuppressWarnings("unused")
private void handleMessage(
@Headers Map<String, ?> param1,
@Headers Map<String, Object> param1,
@Headers String param2,
MessageHeaders param3,
MessageHeaderAccessor param4,
@ -134,7 +119,7 @@ public class HeadersMethodArgumentResolverTests {
public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor {
protected TestMessageHeaderAccessor(Message<?> message) {
TestMessageHeaderAccessor(Message<?> message) {
super(message);
}

View File

@ -0,0 +1,91 @@
/*
* 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.annotation.support.reactive;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.invocation.ResolvableMethod;
import org.springframework.messaging.support.MessageBuilder;
import static org.junit.Assert.*;
import static org.springframework.messaging.handler.annotation.MessagingPredicates.*;
/**
* Test fixture for {@link DestinationVariableMethodArgumentResolver} tests.
* @author Rossen Stoyanchev
*/
public class DestinationVariableMethodArgumentResolverTests {
private final DestinationVariableMethodArgumentResolver resolver =
new DestinationVariableMethodArgumentResolver(new DefaultConversionService());
private final ResolvableMethod resolvable =
ResolvableMethod.on(getClass()).named("handleMessage").build();
@Test
public void supportsParameter() {
assertTrue(resolver.supportsParameter(this.resolvable.annot(destinationVar().noValue()).arg()));
assertFalse(resolver.supportsParameter(this.resolvable.annotNotPresent(DestinationVariable.class).arg()));
}
@Test
public void resolveArgument() {
Map<String, Object> vars = new HashMap<>();
vars.put("foo", "bar");
vars.put("name", "value");
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeader(
DestinationVariableMethodArgumentResolver.DESTINATION_TEMPLATE_VARIABLES_HEADER, vars).build();
Object result = resolveArgument(this.resolvable.annot(destinationVar().noValue()).arg(), message);
assertEquals("bar", result);
result = resolveArgument(this.resolvable.annot(destinationVar("name")).arg(), message);
assertEquals("value", result);
}
@Test(expected = MessageHandlingException.class)
public void resolveArgumentNotFound() {
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build();
resolveArgument(this.resolvable.annot(destinationVar().noValue()).arg(), message);
}
@SuppressWarnings({"unchecked", "ConstantConditions"})
private <T> T resolveArgument(MethodParameter param, Message<?> message) {
return (T) this.resolver.resolveArgument(param, message).block(Duration.ofSeconds(5));
}
@SuppressWarnings("unused")
private void handleMessage(
@DestinationVariable String foo,
@DestinationVariable(value = "name") String param1,
String param3) {
}
}

View File

@ -0,0 +1,176 @@
/*
* 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.annotation.support.reactive;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.invocation.ResolvableMethod;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.messaging.support.NativeMessageHeaderAccessor;
import static org.junit.Assert.*;
import static org.springframework.messaging.handler.annotation.MessagingPredicates.*;
/**
* Test fixture for {@link HeaderMethodArgumentResolver} tests.
* @author Rossen Stoyanchev
*/
public class HeaderMethodArgumentResolverTests {
private HeaderMethodArgumentResolver resolver;
private final ResolvableMethod resolvable = ResolvableMethod.on(getClass()).named("handleMessage").build();
@Before
public void setup() {
GenericApplicationContext context = new GenericApplicationContext();
context.refresh();
this.resolver = new HeaderMethodArgumentResolver(new DefaultConversionService(), context.getBeanFactory());
}
@Test
public void supportsParameter() {
assertTrue(this.resolver.supportsParameter(this.resolvable.annot(headerPlain()).arg()));
assertFalse(this.resolver.supportsParameter(this.resolvable.annotNotPresent(Header.class).arg()));
}
@Test
public void resolveArgument() {
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeader("param1", "foo").build();
Object result = resolveArgument(this.resolvable.annot(headerPlain()).arg(), message);
assertEquals("foo", result);
}
@Test // SPR-11326
public void resolveArgumentNativeHeader() {
TestMessageHeaderAccessor headers = new TestMessageHeaderAccessor();
headers.setNativeHeader("param1", "foo");
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
assertEquals("foo", resolveArgument(this.resolvable.annot(headerPlain()).arg(), message));
}
@Test
public void resolveArgumentNativeHeaderAmbiguity() {
TestMessageHeaderAccessor headers = new TestMessageHeaderAccessor();
headers.setHeader("param1", "foo");
headers.setNativeHeader("param1", "native-foo");
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
assertEquals("foo", resolveArgument(
this.resolvable.annot(headerPlain()).arg(), message));
assertEquals("native-foo", resolveArgument(
this.resolvable.annot(header("nativeHeaders.param1")).arg(), message));
}
@Test(expected = MessageHandlingException.class)
public void resolveArgumentNotFound() {
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build();
resolveArgument(this.resolvable.annot(headerPlain()).arg(), message);
}
@Test
public void resolveArgumentDefaultValue() {
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build();
Object result = resolveArgument(this.resolvable.annot(header("name", "bar")).arg(), message);
assertEquals("bar", result);
}
@Test
public void resolveDefaultValueSystemProperty() {
System.setProperty("systemProperty", "sysbar");
try {
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).build();
MethodParameter param = this.resolvable.annot(header("name", "#{systemProperties.systemProperty}")).arg();
Object result = resolveArgument(param, message);
assertEquals("sysbar", result);
}
finally {
System.clearProperty("systemProperty");
}
}
@Test
public void resolveNameFromSystemProperty() {
System.setProperty("systemProperty", "sysbar");
try {
Message<byte[]> message = MessageBuilder.withPayload(new byte[0]).setHeader("sysbar", "foo").build();
MethodParameter param = this.resolvable.annot(header("#{systemProperties.systemProperty}")).arg();
Object result = resolveArgument(param, message);
assertEquals("foo", result);
}
finally {
System.clearProperty("systemProperty");
}
}
@Test
public void resolveOptionalHeaderWithValue() {
Message<String> message = MessageBuilder.withPayload("foo").setHeader("foo", "bar").build();
MethodParameter param = this.resolvable.annot(header("foo")).arg(Optional.class, String.class);
Object result = resolveArgument(param, message);
assertEquals(Optional.of("bar"), result);
}
@Test
public void resolveOptionalHeaderAsEmpty() {
Message<String> message = MessageBuilder.withPayload("foo").build();
MethodParameter param = this.resolvable.annot(header("foo")).arg(Optional.class, String.class);
Object result = resolveArgument(param, message);
assertEquals(Optional.empty(), result);
}
@SuppressWarnings({"unchecked", "ConstantConditions"})
private <T> T resolveArgument(MethodParameter param, Message<?> message) {
return (T) this.resolver.resolveArgument(param, message).block(Duration.ofSeconds(5));
}
@SuppressWarnings({"unused", "OptionalUsedAsFieldOrParameterType"})
public void handleMessage(
@Header String param1,
@Header(name = "name", defaultValue = "bar") String param2,
@Header(name = "name", defaultValue = "#{systemProperties.systemProperty}") String param3,
@Header(name = "#{systemProperties.systemProperty}") String param4,
String param5,
@Header("foo") Optional<String> param6,
@Header("nativeHeaders.param1") String nativeHeaderParam1) {
}
public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor {
TestMessageHeaderAccessor() {
super((Map<String, List<String>>) null);
}
}
}

View File

@ -0,0 +1,121 @@
/*
* 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.annotation.support.reactive;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.invocation.ResolvableMethod;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.messaging.support.NativeMessageHeaderAccessor;
import static org.junit.Assert.*;
/**
* Test fixture for {@link HeadersMethodArgumentResolver} tests.
* @author Rossen Stoyanchev
*/
public class HeadersMethodArgumentResolverTests {
private final HeadersMethodArgumentResolver resolver = new HeadersMethodArgumentResolver();
private Message<byte[]> message =
MessageBuilder.withPayload(new byte[0]).copyHeaders(Collections.singletonMap("foo", "bar")).build();
private final ResolvableMethod resolvable = ResolvableMethod.on(getClass()).named("handleMessage").build();
@Test
public void supportsParameter() {
assertTrue(this.resolver.supportsParameter(
this.resolvable.annotPresent(Headers.class).arg(Map.class, String.class, Object.class)));
assertTrue(this.resolver.supportsParameter(this.resolvable.arg(MessageHeaders.class)));
assertTrue(this.resolver.supportsParameter(this.resolvable.arg(MessageHeaderAccessor.class)));
assertTrue(this.resolver.supportsParameter(this.resolvable.arg(TestMessageHeaderAccessor.class)));
assertFalse(this.resolver.supportsParameter(this.resolvable.annotPresent(Headers.class).arg(String.class)));
}
@Test
@SuppressWarnings("unchecked")
public void resolveArgumentAnnotated() {
MethodParameter param = this.resolvable.annotPresent(Headers.class).arg(Map.class, String.class, Object.class);
Map<String, Object> headers = resolveArgument(param);
assertEquals("bar", headers.get("foo"));
}
@Test(expected = IllegalStateException.class)
public void resolveArgumentAnnotatedNotMap() {
resolveArgument(this.resolvable.annotPresent(Headers.class).arg(String.class));
}
@Test
public void resolveArgumentMessageHeaders() {
MessageHeaders headers = resolveArgument(this.resolvable.arg(MessageHeaders.class));
assertEquals("bar", headers.get("foo"));
}
@Test
public void resolveArgumentMessageHeaderAccessor() {
MessageHeaderAccessor headers = resolveArgument(this.resolvable.arg(MessageHeaderAccessor.class));
assertEquals("bar", headers.getHeader("foo"));
}
@Test
public void resolveArgumentMessageHeaderAccessorSubclass() {
TestMessageHeaderAccessor headers = resolveArgument(this.resolvable.arg(TestMessageHeaderAccessor.class));
assertEquals("bar", headers.getHeader("foo"));
}
@SuppressWarnings({"unchecked", "ConstantConditions"})
private <T> T resolveArgument(MethodParameter param) {
return (T) this.resolver.resolveArgument(param, this.message).block(Duration.ofSeconds(5));
}
@SuppressWarnings("unused")
private void handleMessage(
@Headers Map<String, Object> param1,
@Headers String param2,
MessageHeaders param3,
MessageHeaderAccessor param4,
TestMessageHeaderAccessor param5) {
}
public static class TestMessageHeaderAccessor extends NativeMessageHeaderAccessor {
TestMessageHeaderAccessor(Message<?> message) {
super(message);
}
public static TestMessageHeaderAccessor wrap(Message<?> message) {
return new TestMessageHeaderAccessor(message);
}
}
}

View File

@ -0,0 +1,201 @@
/*
* 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.annotation.support.reactive;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
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.ReactiveAdapterRegistry;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.env.MapPropertySource;
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.messaging.Message;
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.TestEncoderMethodReturnValueHandler;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.stereotype.Controller;
import static java.nio.charset.StandardCharsets.*;
import static org.junit.Assert.*;
/**
* Unit tests for {@link MessageMappingMessageHandler}.
* @author Rossen Stoyanchev
*/
@SuppressWarnings("ALL")
public class MessageMappingMessageHandlerTests {
private static final DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
private TestEncoderMethodReturnValueHandler returnValueHandler;
@Test
public void handleString() {
MessageMappingMessageHandler messsageHandler = initMesssageHandler();
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));
verifyOutputContent(Collections.singletonList("abcdef::response"));
}
@Test
public void handleFluxString() {
MessageMappingMessageHandler messsageHandler = initMesssageHandler();
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));
verifyOutputContent(Collections.singletonList("abcdef::response"));
}
@Test
public void handleException() {
MessageMappingMessageHandler messsageHandler = initMesssageHandler();
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));
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() {
List<Decoder<?>> decoders = Collections.singletonList(StringDecoder.allMimeTypes());
List<Encoder<?>> encoders = Collections.singletonList(CharSequenceEncoder.allMimeTypes());
ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance();
this.returnValueHandler = new TestEncoderMethodReturnValueHandler(encoders, registry);
PropertySource<?> source = new MapPropertySource("test", Collections.singletonMap("path", "path123"));
StaticApplicationContext context = new StaticApplicationContext();
context.getEnvironment().getPropertySources().addFirst(source);
context.registerSingleton("testController", TestController.class);
context.refresh();
MessageMappingMessageHandler messageHandler = new MessageMappingMessageHandler();
messageHandler.getReturnValueHandlerConfigurer().addCustomHandler(this.returnValueHandler);
messageHandler.setApplicationContext(context);
messageHandler.setEmbeddedValueResolver(new EmbeddedValueResolver(context.getBeanFactory()));
messageHandler.setDecoders(decoders);
messageHandler.afterPropertiesSet();
return messageHandler;
}
private Message<?> message(String destination, String... content) {
return new GenericMessage<>(
Flux.fromIterable(Arrays.asList(content)).map(payload -> toDataBuffer(payload)),
Collections.singletonMap(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER, destination));
}
private DataBuffer toDataBuffer(String payload) {
return bufferFactory.wrap(payload.getBytes(UTF_8));
}
private void verifyOutputContent(List<String> expected) {
Flux<String> result = this.returnValueHandler.getContentAsStrings();
StepVerifier.create(result.collectList()).expectNext(expected).verifyComplete();
}
@Controller
static class TestController {
@MessageMapping("string")
String handleString(String payload) {
return payload + "::response";
}
@MessageMapping("monoString")
Mono<String> handleMonoString(Mono<String> payload) {
return payload.map(s -> s + "::response").delayElement(Duration.ofMillis(10));
}
@MessageMapping("fluxString")
Flux<String> handleFluxString(Flux<String> payload) {
return payload.map(s -> s + "::response").delayElements(Duration.ofMillis(10));
}
@MessageMapping("${path}")
String handleWithPlaceholder(String payload) {
return payload + "::response";
}
@MessageMapping("exception")
String handleAndThrow() {
throw new IllegalArgumentException("rejected");
}
@MessageMapping("errorSignal")
Mono<String> handleAndSignalError() {
return Mono.delay(Duration.ofMillis(10))
.flatMap(aLong -> Mono.error(new IllegalArgumentException("rejected")));
}
@MessageExceptionHandler
Mono<String> handleException(IllegalArgumentException ex) {
return Mono.delay(Duration.ofMillis(10)).map(aLong -> ex.getMessage() + "::handled");
}
}
}

View File

@ -0,0 +1,208 @@
/*
* 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.annotation.support.reactive;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException;
import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException;
import org.springframework.messaging.handler.invocation.ResolvableMethod;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.util.MimeTypeUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import static org.junit.Assert.*;
/**
* Unit tests for {@link PayloadMethodArgumentResolver}.
*
* @author Rossen Stoyanchev
*/
public class PayloadMethodArgumentResolverTests {
private final List<Decoder<?>> decoders = new ArrayList<>();
private final ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build();
@Test
public void supportsParameter() {
boolean useDefaultResolution = true;
PayloadMethodArgumentResolver resolver = createResolver(null, useDefaultResolution);
assertTrue(resolver.supportsParameter(this.testMethod.annotPresent(Payload.class).arg()));
assertTrue(resolver.supportsParameter(this.testMethod.annotNotPresent(Payload.class).arg(String.class)));
useDefaultResolution = false;
resolver = createResolver(null, useDefaultResolution);
assertTrue(resolver.supportsParameter(this.testMethod.annotPresent(Payload.class).arg()));
assertFalse(resolver.supportsParameter(this.testMethod.annotNotPresent(Payload.class).arg(String.class)));
}
@Test
public void emptyBodyWhenRequired() {
MethodParameter param = this.testMethod.arg(ResolvableType.forClassWithGenerics(Mono.class, String.class));
Mono<Object> mono = resolveValue(param, Mono.empty(), null);
StepVerifier.create(mono)
.consumeErrorWith(ex -> {
assertEquals(MethodArgumentResolutionException.class, ex.getClass());
assertTrue(ex.getMessage(), ex.getMessage().contains("Payload content is missing"));
})
.verify();
}
@Test
public void emptyBodyWhenNotRequired() {
MethodParameter param = this.testMethod.annotPresent(Payload.class).arg();
assertNull(resolveValue(param, Mono.empty(), null));
}
@Test
public void stringMono() {
String body = "foo";
MethodParameter param = this.testMethod.arg(ResolvableType.forClassWithGenerics(Mono.class, String.class));
Mono<Object> mono = resolveValue(param,
Mono.delay(Duration.ofMillis(10)).map(aLong -> toDataBuffer(body)), null);
assertEquals(body, mono.block());
}
@Test
public void stringFlux() {
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(this::toDataBuffer), null);
assertEquals(body, flux.collectList().block());
}
@Test
public void string() {
String body = "foo";
MethodParameter param = this.testMethod.annotNotPresent(Payload.class).arg(String.class);
Object value = resolveValue(param, Mono.just(toDataBuffer(body)), null);
assertEquals(body, value);
}
@Test
public void validateStringMono() {
ResolvableType type = ResolvableType.forClassWithGenerics(Mono.class, String.class);
MethodParameter param = this.testMethod.arg(type);
Mono<Object> mono = resolveValue(param, Mono.just(toDataBuffer("12345")), new TestValidator());
StepVerifier.create(mono).expectNextCount(0)
.expectError(MethodArgumentNotValidException.class).verify();
}
@Test
public void validateStringFlux() {
ResolvableType type = ResolvableType.forClassWithGenerics(Flux.class, String.class);
MethodParameter param = this.testMethod.arg(type);
Flux<Object> flux = resolveValue(param, Mono.just(toDataBuffer("12345678\n12345")), new TestValidator());
StepVerifier.create(flux)
.expectNext("12345678")
.expectError(MethodArgumentNotValidException.class)
.verify();
}
private DataBuffer toDataBuffer(String value) {
return new DefaultDataBufferFactory().wrap(value.getBytes(StandardCharsets.UTF_8));
}
@SuppressWarnings("unchecked")
@Nullable
private <T> T resolveValue(MethodParameter param, Publisher<DataBuffer> content, Validator validator) {
Message<?> message = new GenericMessage<>(content,
Collections.singletonMap(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN));
Mono<Object> result = createResolver(validator, true).resolveArgument(param, message);
Object value = result.block(Duration.ofSeconds(5));
if (value != null) {
Class<?> expectedType = param.getParameterType();
assertTrue("Unexpected return value type: " + value, expectedType.isAssignableFrom(value.getClass()));
}
return (T) value;
}
private PayloadMethodArgumentResolver createResolver(@Nullable Validator validator, boolean useDefaultResolution) {
if (this.decoders.isEmpty()) {
this.decoders.add(StringDecoder.allMimeTypes());
}
List<StringDecoder> decoders = Collections.singletonList(StringDecoder.allMimeTypes());
return new PayloadMethodArgumentResolver(decoders, validator, null, useDefaultResolution) {};
}
@SuppressWarnings("unused")
private void handle(
@Validated Mono<String> valueMono,
@Validated Flux<String> valueFlux,
@Payload(required = false) String optionalValue,
String value) {
}
private static class TestValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return clazz.equals(String.class);
}
@Override
public void validate(@Nullable Object target, Errors errors) {
if (target instanceof String && ((String) target).length() < 8) {
errors.reject("Invalid length");
}
}
}
}

View File

@ -23,7 +23,6 @@ import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.ResolvableMethod;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

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"));
@ -166,7 +164,7 @@ public class MethodMessageHandlerTests {
this.method = "secondBestMatch";
}
public void illegalStateException(IllegalStateException exception) {
public void handleIllegalStateException(IllegalStateException exception) {
this.method = "illegalStateException";
this.arguments.put("exception", exception);
}
@ -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

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.messaging.handler;
package org.springframework.messaging.handler.invocation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
@ -57,7 +57,10 @@ import org.springframework.util.ReflectionUtils;
import static java.util.stream.Collectors.*;
/**
* Convenience class to resolve method parameters from hints.
* NOTE: This class is a replica of the same class in spring-web so it can
* be used for tests in spring-messaging.
*
* <p>Convenience class to resolve method parameters from hints.
*
* <h1>Background</h1>
*
@ -120,7 +123,7 @@ import static java.util.stream.Collectors.*;
* </pre>
*
* @author Rossen Stoyanchev
* @since 5.0
* @since 5.2
*/
public class ResolvableMethod {
@ -186,6 +189,7 @@ public class ResolvableMethod {
/**
* Filter on method arguments with annotation.
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
*/
@SafeVarargs
public final ArgResolver annot(Predicate<MethodParameter>... filter) {
@ -298,6 +302,7 @@ public class ResolvableMethod {
/**
* Filter on annotated methods.
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
*/
@SafeVarargs
public final Builder<T> annot(Predicate<Method>... filters) {
@ -308,6 +313,7 @@ public class ResolvableMethod {
/**
* Filter on methods annotated with the given annotation type.
* @see #annot(Predicate[])
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
*/
@SafeVarargs
public final Builder<T> annotPresent(Class<? extends Annotation>... annotationTypes) {
@ -524,6 +530,7 @@ public class ResolvableMethod {
/**
* Filter on method arguments with annotations.
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
*/
@SafeVarargs
public final ArgResolver annot(Predicate<MethodParameter>... filters) {
@ -535,6 +542,7 @@ public class ResolvableMethod {
* Filter on method arguments that have the given annotations.
* @param annotationTypes the annotation types
* @see #annot(Predicate[])
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
*/
@SafeVarargs
public final ArgResolver annotPresent(Class<? extends Annotation>... annotationTypes) {

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,125 @@
/*
* 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.Collections;
import io.reactivex.Completable;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import static org.springframework.messaging.handler.invocation.ResolvableMethod.*;
/**
* Unit tests for {@link AbstractEncoderMethodReturnValueHandler}.
*
* @author Rossen Stoyanchev
*/
public class EncoderMethodReturnValueHandlerTests {
private final TestEncoderMethodReturnValueHandler handler = new TestEncoderMethodReturnValueHandler(
Collections.singletonList(CharSequenceEncoder.textPlainOnly()),
ReactiveAdapterRegistry.getSharedInstance());
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, this.message).block();
Flux<String> result = this.handler.getContentAsStrings();
StepVerifier.create(result).expectNext("foo").verifyComplete();
}
@Test
public void objectReturnValue() {
MethodParameter parameter = on(TestController.class).resolveReturnType(Object.class);
this.handler.handleReturnValue("foo", parameter, this.message).block();
Flux<String> result = this.handler.getContentAsStrings();
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, this.message).block();
Flux<String> result = this.handler.getContentAsStrings();
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
public void voidReturnValue() {
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, 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);
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();
}
@SuppressWarnings({"unused", "ConstantConditions"})
private static class TestController {
String string() { return null; }
Object object() { return null; }
Flux<String> fluxString() { return null; }
Flux<Object> fluxObject() { return null; }
void voidReturn() { }
Mono<Void> monoVoid() { return null; }
Completable completable() { return null; }
}
}

View File

@ -0,0 +1,237 @@
/*
* 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.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException;
import org.springframework.messaging.handler.invocation.ResolvableMethod;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* Unit tests for {@link InvocableHandlerMethod}.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
*/
public class InvocableHandlerMethodTests {
private final Message<?> message = mock(Message.class);
private final List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
@Test
public void resolveArg() {
this.resolvers.add(new StubArgumentResolver(99));
this.resolvers.add(new StubArgumentResolver("value"));
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method();
Object value = invokeAndBlock(new Handler(), method);
assertEquals(1, getStubResolver(0).getResolvedParameters().size());
assertEquals(1, getStubResolver(1).getResolvedParameters().size());
assertEquals("99-value", value);
assertEquals("intArg", getStubResolver(0).getResolvedParameters().get(0).getParameterName());
assertEquals("stringArg", getStubResolver(1).getResolvedParameters().get(0).getParameterName());
}
@Test
public void resolveNoArgValue() {
this.resolvers.add(new StubArgumentResolver(Integer.class));
this.resolvers.add(new StubArgumentResolver(String.class));
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method();
Object value = invokeAndBlock(new Handler(), method);
assertEquals(1, getStubResolver(0).getResolvedParameters().size());
assertEquals(1, getStubResolver(1).getResolvedParameters().size());
assertEquals("null-null", value);
}
@Test
public void cannotResolveArg() {
try {
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method();
invokeAndBlock(new Handler(), method);
fail("Expected exception");
}
catch (MethodArgumentResolutionException ex) {
assertNotNull(ex.getMessage());
assertTrue(ex.getMessage().contains("Could not resolve parameter [0]"));
}
}
@Test
public void resolveProvidedArg() {
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method();
Object value = invokeAndBlock(new Handler(), method, 99, "value");
assertNotNull(value);
assertEquals(String.class, value.getClass());
assertEquals("99-value", value);
}
@Test
public void resolveProvidedArgFirst() {
this.resolvers.add(new StubArgumentResolver(1));
this.resolvers.add(new StubArgumentResolver("value1"));
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method();
Object value = invokeAndBlock(new Handler(), method, 2, "value2");
assertEquals("2-value2", value);
}
@Test
public void exceptionInResolvingArg() {
this.resolvers.add(new InvocableHandlerMethodTests.ExceptionRaisingArgumentResolver());
try {
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method();
invokeAndBlock(new Handler(), method);
fail("Expected exception");
}
catch (IllegalArgumentException ex) {
// expected - allow HandlerMethodArgumentResolver exceptions to propagate
}
}
@Test
public void illegalArgumentException() {
this.resolvers.add(new StubArgumentResolver(Integer.class, "__not_an_int__"));
this.resolvers.add(new StubArgumentResolver("value"));
try {
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method();
invokeAndBlock(new Handler(), method);
fail("Expected exception");
}
catch (IllegalStateException ex) {
assertNotNull("Exception not wrapped", ex.getCause());
assertTrue(ex.getCause() instanceof IllegalArgumentException);
assertTrue(ex.getMessage().contains("Endpoint ["));
assertTrue(ex.getMessage().contains("Method ["));
assertTrue(ex.getMessage().contains("with argument values:"));
assertTrue(ex.getMessage().contains("[0] [type=java.lang.String] [value=__not_an_int__]"));
assertTrue(ex.getMessage().contains("[1] [type=java.lang.String] [value=value"));
}
}
@Test
public void invocationTargetException() {
Method method = ResolvableMethod.on(Handler.class).argTypes(Throwable.class).resolveMethod();
Throwable expected = new Throwable("error");
Mono<Object> result = invoke(new Handler(), method, expected);
StepVerifier.create(result).expectErrorSatisfies(actual -> assertSame(expected, actual)).verify();
}
@Test
public void voidMethod() {
this.resolvers.add(new StubArgumentResolver(double.class, 5.25));
Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0.0d)).method();
Handler handler = new Handler();
Object value = invokeAndBlock(handler, method);
assertNull(value);
assertEquals(1, getStubResolver(0).getResolvedParameters().size());
assertEquals("5.25", handler.getResult());
assertEquals("amount", getStubResolver(0).getResolvedParameters().get(0).getParameterName());
}
@Test
public void voidMonoMethod() {
Method method = ResolvableMethod.on(Handler.class).mockCall(Handler::handleAsync).method();
Handler handler = new Handler();
Object value = invokeAndBlock(handler, method);
assertNull(value);
assertEquals("success", handler.getResult());
}
@Nullable
private Object invokeAndBlock(Object handler, Method method, Object... providedArgs) {
return invoke(handler, method, providedArgs).block(Duration.ofSeconds(5));
}
private Mono<Object> invoke(Object handler, Method method, Object... providedArgs) {
InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(handler, method);
handlerMethod.setArgumentResolvers(this.resolvers);
return handlerMethod.invoke(this.message, providedArgs);
}
private StubArgumentResolver getStubResolver(int index) {
return (StubArgumentResolver) this.resolvers.get(index);
}
@SuppressWarnings({"unused", "UnusedReturnValue", "SameParameterValue"})
private static class Handler {
private AtomicReference<String> result = new AtomicReference<>();
public String getResult() {
return this.result.get();
}
String handle(Integer intArg, String stringArg) {
return intArg + "-" + stringArg;
}
void handle(double amount) {
this.result.set(String.valueOf(amount));
}
void handleWithException(Throwable ex) throws Throwable {
throw ex;
}
Mono<Void> handleAsync() {
return Mono.delay(Duration.ofMillis(100)).thenEmpty(Mono.defer(() -> {
this.result.set("success");
return Mono.empty();
}));
}
}
private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return true;
}
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) {
return Mono.error(new IllegalArgumentException("oops, can't read"));
}
}
}

View File

@ -0,0 +1,263 @@
/*
* 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.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 java.util.function.Predicate;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.reactivestreams.Publisher;
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", "/handleMessageWithError",
"/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((Publisher<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((Publisher<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, "/handleMessageWithError"));
handler.handleMessage(message).block(Duration.ofSeconds(5));
StepVerifier.create((Publisher<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<String> handleMessageWithError() {
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);
}
@Override
protected Predicate<Class<?>> initHandlerPredicate() {
return handlerType -> handlerType.getName().endsWith("Controller");
}
@Nullable
public Object getLastReturnValue() {
return this.returnValueHandler.getLastReturnValue();
}
public void register(Object handler, Method method, String mapping) {
super.registerHandlerMethod(handler, method, mapping);
}
@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,74 @@
/*
* 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.util.ArrayList;
import java.util.List;
import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
/**
* Stub resolver for a fixed value type and/or value.
*
* @author Rossen Stoyanchev
*/
public class StubArgumentResolver implements HandlerMethodArgumentResolver {
private final Class<?> valueType;
@Nullable
private final Object value;
private List<MethodParameter> resolvedParameters = new ArrayList<>();
public StubArgumentResolver(Object value) {
this(value.getClass(), value);
}
public StubArgumentResolver(Class<?> valueType) {
this(valueType, null);
}
public StubArgumentResolver(Class<?> valueType, Object value) {
this.valueType = valueType;
this.value = value;
}
public List<MethodParameter> getResolvedParameters() {
return resolvedParameters;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(this.valueType);
}
@SuppressWarnings("unchecked")
@Override
public Mono<Object> resolveArgument(MethodParameter parameter, Message<?> message) {
this.resolvedParameters.add(parameter);
return Mono.justOrEmpty(this.value);
}
}

View File

@ -0,0 +1,69 @@
/*
* 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();
}
@Override
protected Mono<Void> handleNoContent(MethodParameter returnType, Message<?> message) {
this.encodedContent = Flux.empty();
return Mono.empty();
}
}

View File

@ -0,0 +1,59 @@
/*
* 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 org.reactivestreams.Publisher;
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
@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

@ -0,0 +1,275 @@
/*
* 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.rsocket;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.rsocket.AbstractRSocket;
import io.rsocket.Payload;
import org.junit.Before;
import org.junit.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.lang.Nullable;
import org.springframework.messaging.rsocket.RSocketRequester.RequestSpec;
import org.springframework.messaging.rsocket.RSocketRequester.ResponseSpec;
import org.springframework.util.MimeTypeUtils;
import static java.util.concurrent.TimeUnit.*;
import static org.junit.Assert.*;
/**
* Unit tests for {@link DefaultRSocketRequester}.
*
* @author Rossen Stoyanchev
*/
public class DefaultRSocketRequesterTests {
private static final Duration MILLIS_10 = Duration.ofMillis(10);
private TestRSocket rsocket;
private RSocketRequester requester;
private final DefaultDataBufferFactory bufferFactory = new DefaultDataBufferFactory();
@Before
public void setUp() {
RSocketStrategies strategies = RSocketStrategies.builder()
.decoder(StringDecoder.allMimeTypes())
.encoder(CharSequenceEncoder.allMimeTypes())
.build();
this.rsocket = new TestRSocket();
this.requester = RSocketRequester.create(rsocket, MimeTypeUtils.TEXT_PLAIN, strategies);
}
@Test
public void singlePayload() {
// data(Object)
testSinglePayload(spec -> spec.data("bodyA"), "bodyA");
testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).map(l -> "bodyA")), "bodyA");
testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).then()), "");
testSinglePayload(spec -> spec.data(Single.timer(10, MILLISECONDS).map(l -> "bodyA")), "bodyA");
testSinglePayload(spec -> spec.data(Completable.complete()), "");
// data(Publisher<T>, Class<T>)
testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).map(l -> "bodyA"), String.class), "bodyA");
testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).map(l -> "bodyA"), Object.class), "bodyA");
testSinglePayload(spec -> spec.data(Mono.delay(MILLIS_10).then(), Void.class), "");
}
private void testSinglePayload(Function<RequestSpec, ResponseSpec> mapper, String expectedValue) {
mapper.apply(this.requester.route("toA")).send().block(Duration.ofSeconds(5));
assertEquals("fireAndForget", this.rsocket.getSavedMethodName());
assertEquals("toA", this.rsocket.getSavedPayload().getMetadataUtf8());
assertEquals(expectedValue, this.rsocket.getSavedPayload().getDataUtf8());
}
@Test
public void multiPayload() {
String[] values = new String[] {"bodyA", "bodyB", "bodyC"};
Flux<String> stringFlux = Flux.fromArray(values).delayElements(MILLIS_10);
// data(Object)
testMultiPayload(spec -> spec.data(stringFlux), values);
testMultiPayload(spec -> spec.data(Flux.empty()), "");
testMultiPayload(spec -> spec.data(Observable.fromArray(values).delay(10, MILLISECONDS)), values);
testMultiPayload(spec -> spec.data(Observable.empty()), "");
// data(Publisher<T>, Class<T>)
testMultiPayload(spec -> spec.data(stringFlux, String.class), values);
testMultiPayload(spec -> spec.data(stringFlux.cast(Object.class), Object.class), values);
}
private void testMultiPayload(Function<RequestSpec, ResponseSpec> mapper, String... expectedValues) {
this.rsocket.reset();
mapper.apply(this.requester.route("toA")).retrieveFlux(String.class).blockLast(Duration.ofSeconds(5));
assertEquals("requestChannel", this.rsocket.getSavedMethodName());
List<Payload> payloads = this.rsocket.getSavedPayloadFlux().collectList().block(Duration.ofSeconds(5));
assertNotNull(payloads);
if (Arrays.equals(new String[] {""}, expectedValues)) {
assertEquals(1, payloads.size());
assertEquals("toA", payloads.get(0).getMetadataUtf8());
assertEquals("", payloads.get(0).getDataUtf8());
}
else {
assertArrayEquals(new String[] {"toA", "", ""},
payloads.stream().map(Payload::getMetadataUtf8).toArray(String[]::new));
assertArrayEquals(expectedValues,
payloads.stream().map(Payload::getDataUtf8).toArray(String[]::new));
}
}
@Test
public void send() {
String value = "bodyA";
this.requester.route("toA").data(value).send().block(Duration.ofSeconds(5));
assertEquals("fireAndForget", this.rsocket.getSavedMethodName());
assertEquals("toA", this.rsocket.getSavedPayload().getMetadataUtf8());
assertEquals("bodyA", this.rsocket.getSavedPayload().getDataUtf8());
}
@Test
public void retrieveMono() {
String value = "bodyA";
this.rsocket.setPayloadMonoToReturn(Mono.delay(MILLIS_10).thenReturn(toPayload(value)));
Mono<String> response = this.requester.route("").data("").retrieveMono(String.class);
StepVerifier.create(response).expectNext(value).expectComplete().verify(Duration.ofSeconds(5));
assertEquals("requestResponse", this.rsocket.getSavedMethodName());
}
@Test
public void retrieveMonoVoid() {
AtomicBoolean consumed = new AtomicBoolean(false);
Mono<Payload> mono = Mono.delay(MILLIS_10).thenReturn(toPayload("bodyA")).doOnSuccess(p -> consumed.set(true));
this.rsocket.setPayloadMonoToReturn(mono);
this.requester.route("").data("").retrieveMono(Void.class).block(Duration.ofSeconds(5));
assertTrue(consumed.get());
assertEquals("requestResponse", this.rsocket.getSavedMethodName());
}
@Test
public void retrieveFlux() {
String[] values = new String[] {"bodyA", "bodyB", "bodyC"};
this.rsocket.setPayloadFluxToReturn(Flux.fromArray(values).delayElements(MILLIS_10).map(this::toPayload));
Flux<String> response = this.requester.route("").data("").retrieveFlux(String.class);
StepVerifier.create(response).expectNext(values).expectComplete().verify(Duration.ofSeconds(5));
assertEquals("requestStream", this.rsocket.getSavedMethodName());
}
@Test
public void retrieveFluxVoid() {
AtomicBoolean consumed = new AtomicBoolean(false);
Flux<Payload> flux = Flux.just("bodyA", "bodyB")
.delayElements(MILLIS_10).map(this::toPayload).doOnComplete(() -> consumed.set(true));
this.rsocket.setPayloadFluxToReturn(flux);
this.requester.route("").data("").retrieveFlux(Void.class).blockLast(Duration.ofSeconds(5));
assertTrue(consumed.get());
assertEquals("requestStream", this.rsocket.getSavedMethodName());
}
@Test
public void rejectFluxToMono() {
try {
this.requester.route("").data(Flux.just("a", "b")).retrieveMono(String.class);
fail();
}
catch (IllegalArgumentException ex) {
assertEquals("No RSocket interaction model for Flux request to Mono response.", ex.getMessage());
}
}
private Payload toPayload(String value) {
return PayloadUtils.createPayload(bufferFactory.wrap(value.getBytes(StandardCharsets.UTF_8)));
}
private static class TestRSocket extends AbstractRSocket {
private Mono<Payload> payloadMonoToReturn = Mono.empty();
private Flux<Payload> payloadFluxToReturn = Flux.empty();
@Nullable private volatile String savedMethodName;
@Nullable private volatile Payload savedPayload;
@Nullable private volatile Flux<Payload> savedPayloadFlux;
void setPayloadMonoToReturn(Mono<Payload> payloadMonoToReturn) {
this.payloadMonoToReturn = payloadMonoToReturn;
}
void setPayloadFluxToReturn(Flux<Payload> payloadFluxToReturn) {
this.payloadFluxToReturn = payloadFluxToReturn;
}
@Nullable
String getSavedMethodName() {
return this.savedMethodName;
}
@Nullable
Payload getSavedPayload() {
return this.savedPayload;
}
@Nullable
Flux<Payload> getSavedPayloadFlux() {
return this.savedPayloadFlux;
}
public void reset() {
this.savedMethodName = null;
this.savedPayload = null;
this.savedPayloadFlux = null;
}
@Override
public Mono<Void> fireAndForget(Payload payload) {
this.savedMethodName = "fireAndForget";
this.savedPayload = payload;
return Mono.empty();
}
@Override
public Mono<Payload> requestResponse(Payload payload) {
this.savedMethodName = "requestResponse";
this.savedPayload = payload;
return this.payloadMonoToReturn;
}
@Override
public Flux<Payload> requestStream(Payload payload) {
this.savedMethodName = "requestStream";
this.savedPayload = payload;
return this.payloadFluxToReturn;
}
@Override
public Flux<Payload> requestChannel(Publisher<Payload> publisher) {
this.savedMethodName = "requestChannel";
this.savedPayloadFlux = Flux.from(publisher);
return this.payloadFluxToReturn;
}
}
}

View File

@ -0,0 +1,78 @@
/*
* 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.rsocket;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import io.rsocket.AbstractRSocket;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.plugins.RSocketInterceptor;
import io.rsocket.util.RSocketProxy;
import reactor.core.publisher.Mono;
/**
* Intercept received RSockets and count successfully completed requests seen
* on the server side. This is useful for verifying fire-and-forget
* interactions.
*
* @author Rossen Stoyanchev
*/
class FireAndForgetCountingInterceptor extends AbstractRSocket implements RSocketInterceptor {
private final List<CountingDecorator> rsockets = new CopyOnWriteArrayList<>();
public int getRSocketCount() {
return this.rsockets.size();
}
public int getFireAndForgetCount(int index) {
return this.rsockets.get(index).getFireAndForgetCount();
}
@Override
public RSocket apply(RSocket rsocket) {
CountingDecorator decorator = new CountingDecorator(rsocket);
this.rsockets.add(decorator);
return decorator;
}
private static class CountingDecorator extends RSocketProxy {
private final AtomicInteger fireAndForget = new AtomicInteger(0);
CountingDecorator(RSocket delegate) {
super(delegate);
}
public int getFireAndForgetCount() {
return this.fireAndForget.get();
}
@Override
public Mono<Void> fireAndForget(Payload payload) {
return super.fireAndForget(payload).doOnSuccess(aVoid -> this.fireAndForget.incrementAndGet());
}
}
}

View File

@ -0,0 +1,466 @@
/*
* 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.rsocket;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.util.ReferenceCounted;
import io.rsocket.AbstractRSocket;
import io.rsocket.Frame;
import io.rsocket.RSocket;
import io.rsocket.RSocketFactory;
import io.rsocket.plugins.RSocketInterceptor;
import io.rsocket.transport.netty.client.TcpClientTransport;
import io.rsocket.transport.netty.server.CloseableChannel;
import io.rsocket.transport.netty.server.TcpServerTransport;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.ReplayProcessor;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
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.core.io.Resource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.NettyDataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.core.io.buffer.PooledDataBuffer;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Controller;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.ObjectUtils;
import static org.junit.Assert.*;
/**
* Tests for scenarios that could lead to Payload and/or DataBuffer leaks.
*
* @author Rossen Stoyanchev
*/
public class RSocketBufferLeakTests {
private static AnnotationConfigApplicationContext context;
private static final PayloadInterceptor payloadInterceptor = new PayloadInterceptor();
private static CloseableChannel server;
private static RSocket client;
private static RSocketRequester requester;
@BeforeClass
@SuppressWarnings("ConstantConditions")
public static void setupOnce() {
context = new AnnotationConfigApplicationContext(ServerConfig.class);
server = RSocketFactory.receive()
.frameDecoder(Frame::retain) // zero copy
.addServerPlugin(payloadInterceptor) // intercept responding
.acceptor(context.getBean(MessageHandlerAcceptor.class))
.transport(TcpServerTransport.create("localhost", 7000))
.start()
.block();
client = RSocketFactory.connect()
.frameDecoder(Frame::retain) // zero copy
.addClientPlugin(payloadInterceptor) // intercept outgoing requests
.dataMimeType(MimeTypeUtils.TEXT_PLAIN_VALUE)
.transport(TcpClientTransport.create("localhost", 7000))
.start()
.block();
requester = RSocketRequester.create(
client, MimeTypeUtils.TEXT_PLAIN, context.getBean(RSocketStrategies.class));
}
@AfterClass
public static void tearDownOnce() {
client.dispose();
server.dispose();
}
@Before
public void setUp() {
getLeakAwareNettyDataBufferFactory().reset();
payloadInterceptor.reset();
}
@After
public void tearDown() throws InterruptedException {
getLeakAwareNettyDataBufferFactory().checkForLeaks(Duration.ofSeconds(5));
payloadInterceptor.checkForLeaks();
}
private LeakAwareNettyDataBufferFactory getLeakAwareNettyDataBufferFactory() {
return (LeakAwareNettyDataBufferFactory) context.getBean(RSocketStrategies.class).dataBufferFactory();
}
@Test
public void assemblyTimeErrorForHandleAndReply() {
Mono<String> result = requester.route("A.B").data("foo").retrieveMono(String.class);
StepVerifier.create(result).expectErrorMatches(ex -> {
String prefix = "Ambiguous handler methods mapped for destination 'A.B':";
return ex.getMessage().startsWith(prefix);
}).verify();
}
@Test
public void subscriptionTimeErrorForHandleAndReply() {
Mono<String> result = requester.route("not-decodable").data("foo").retrieveMono(String.class);
StepVerifier.create(result).expectErrorMatches(ex -> {
String prefix = "Cannot decode to [org.springframework.core.io.Resource]";
return ex.getMessage().contains(prefix);
}).verify();
}
@Test
public void errorSignalWithExceptionHandler() {
Mono<String> result = requester.route("error-signal").data("foo").retrieveMono(String.class);
StepVerifier.create(result).expectNext("Handled 'bad input'").verifyComplete();
}
@Test
public void ignoreInput() {
Flux<String> result = requester.route("ignore-input").data("a").retrieveFlux(String.class);
StepVerifier.create(result).expectNext("bar").verifyComplete();
}
@Test
public void retrieveMonoFromFluxResponderMethod() {
Mono<String> result = requester.route("request-stream").data("foo").retrieveMono(String.class);
StepVerifier.create(result).expectNext("foo-1").verifyComplete();
}
@Controller
static class ServerController {
@MessageMapping("A.*")
void ambiguousMatchA(String payload) {
throw new IllegalStateException("Unexpected call");
}
@MessageMapping("*.B")
void ambiguousMatchB(String payload) {
throw new IllegalStateException("Unexpected call");
}
@MessageMapping("not-decodable")
void notDecodable(@Payload Resource resource) {
throw new IllegalStateException("Unexpected call");
}
@MessageMapping("error-signal")
public Flux<String> errorSignal(String payload) {
return Flux.error(new IllegalArgumentException("bad input"))
.delayElements(Duration.ofMillis(10))
.cast(String.class);
}
@MessageExceptionHandler
public String handleIllegalArgument(IllegalArgumentException ex) {
return "Handled '" + ex.getMessage() + "'";
}
@MessageMapping("ignore-input")
Mono<String> ignoreInput() {
return Mono.delay(Duration.ofMillis(10)).map(l -> "bar");
}
@MessageMapping("request-stream")
Flux<String> stream(String payload) {
return Flux.range(1,100).delayElements(Duration.ofMillis(10)).map(idx -> payload + "-" + idx);
}
}
@Configuration
static class ServerConfig {
@Bean
public ServerController controller() {
return new ServerController();
}
@Bean
public MessageHandlerAcceptor messageHandlerAcceptor() {
MessageHandlerAcceptor acceptor = new MessageHandlerAcceptor();
acceptor.setRSocketStrategies(rsocketStrategies());
return acceptor;
}
@Bean
public RSocketStrategies rsocketStrategies() {
return RSocketStrategies.builder()
.decoder(StringDecoder.allMimeTypes())
.encoder(CharSequenceEncoder.allMimeTypes())
.dataBufferFactory(new LeakAwareNettyDataBufferFactory(PooledByteBufAllocator.DEFAULT))
.build();
}
}
/**
* Similar {@link org.springframework.core.io.buffer.LeakAwareDataBufferFactory}
* but extends {@link NettyDataBufferFactory} rather than rely on
* decoration, since {@link PayloadUtils} does instanceof checks.
*/
private static class LeakAwareNettyDataBufferFactory extends NettyDataBufferFactory {
private final List<DataBufferLeakInfo> created = new ArrayList<>();
LeakAwareNettyDataBufferFactory(ByteBufAllocator byteBufAllocator) {
super(byteBufAllocator);
}
void checkForLeaks(Duration duration) throws InterruptedException {
Instant start = Instant.now();
while (true) {
try {
this.created.forEach(info -> {
if (((PooledDataBuffer) info.getDataBuffer()).isAllocated()) {
throw info.getError();
}
});
break;
}
catch (AssertionError ex) {
if (Instant.now().isAfter(start.plus(duration))) {
throw ex;
}
}
Thread.sleep(50);
}
}
void reset() {
this.created.clear();
}
@Override
public NettyDataBuffer allocateBuffer() {
return (NettyDataBuffer) record(super.allocateBuffer());
}
@Override
public NettyDataBuffer allocateBuffer(int initialCapacity) {
return (NettyDataBuffer) record(super.allocateBuffer(initialCapacity));
}
@Override
public NettyDataBuffer wrap(ByteBuf byteBuf) {
NettyDataBuffer dataBuffer = super.wrap(byteBuf);
if (byteBuf != Unpooled.EMPTY_BUFFER) {
record(dataBuffer);
}
return dataBuffer;
}
@Override
public DataBuffer join(List<? extends DataBuffer> dataBuffers) {
return record(super.join(dataBuffers));
}
private DataBuffer record(DataBuffer buffer) {
this.created.add(new DataBufferLeakInfo(buffer, new AssertionError(String.format(
"DataBuffer leak: {%s} {%s} not released.%nStacktrace at buffer creation: ", buffer,
ObjectUtils.getIdentityHexString(((NettyDataBuffer) buffer).getNativeBuffer())))));
return buffer;
}
}
private static class DataBufferLeakInfo {
private final DataBuffer dataBuffer;
private final AssertionError error;
DataBufferLeakInfo(DataBuffer dataBuffer, AssertionError error) {
this.dataBuffer = dataBuffer;
this.error = error;
}
DataBuffer getDataBuffer() {
return this.dataBuffer;
}
AssertionError getError() {
return this.error;
}
}
/**
* Store all intercepted incoming and outgoing payloads and then use
* {@link #checkForLeaks()} at the end to check reference counts.
*/
private static class PayloadInterceptor extends AbstractRSocket implements RSocketInterceptor {
private final List<PayloadSavingDecorator> rsockets = new CopyOnWriteArrayList<>();
void checkForLeaks() {
this.rsockets.stream().map(PayloadSavingDecorator::getPayloads)
.forEach(payloadInfoProcessor -> {
payloadInfoProcessor.onComplete();
payloadInfoProcessor
.doOnNext(this::checkForLeak)
.blockLast();
});
}
private void checkForLeak(PayloadLeakInfo info) {
Instant start = Instant.now();
while (true) {
try {
int count = info.getReferenceCount();
assertTrue("Leaked payload (refCnt=" + count + "): " + info, count == 0);
break;
}
catch (AssertionError ex) {
if (Instant.now().isAfter(start.plus(Duration.ofSeconds(5)))) {
throw ex;
}
}
try {
Thread.sleep(50);
}
catch (InterruptedException ex) {
// ignore
}
}
}
public void reset() {
this.rsockets.forEach(PayloadSavingDecorator::reset);
}
@Override
public RSocket apply(RSocket rsocket) {
PayloadSavingDecorator decorator = new PayloadSavingDecorator(rsocket);
this.rsockets.add(decorator);
return decorator;
}
private static class PayloadSavingDecorator extends AbstractRSocket {
private final RSocket delegate;
private ReplayProcessor<PayloadLeakInfo> payloads = ReplayProcessor.create();
PayloadSavingDecorator(RSocket delegate) {
this.delegate = delegate;
}
ReplayProcessor<PayloadLeakInfo> getPayloads() {
return this.payloads;
}
void reset() {
this.payloads = ReplayProcessor.create();
}
@Override
public Mono<Void> fireAndForget(io.rsocket.Payload payload) {
return this.delegate.fireAndForget(addPayload(payload));
}
@Override
public Mono<io.rsocket.Payload> requestResponse(io.rsocket.Payload payload) {
return this.delegate.requestResponse(addPayload(payload)).doOnSuccess(this::addPayload);
}
@Override
public Flux<io.rsocket.Payload> requestStream(io.rsocket.Payload payload) {
return this.delegate.requestStream(addPayload(payload)).doOnNext(this::addPayload);
}
@Override
public Flux<io.rsocket.Payload> requestChannel(Publisher<io.rsocket.Payload> payloads) {
return this.delegate
.requestChannel(Flux.from(payloads).doOnNext(this::addPayload))
.doOnNext(this::addPayload);
}
private io.rsocket.Payload addPayload(io.rsocket.Payload payload) {
this.payloads.onNext(new PayloadLeakInfo(payload));
return payload;
}
@Override
public Mono<Void> metadataPush(io.rsocket.Payload payload) {
return this.delegate.metadataPush(addPayload(payload));
}
}
}
private static class PayloadLeakInfo {
private final String description;
private final ReferenceCounted referenceCounted;
PayloadLeakInfo(io.rsocket.Payload payload) {
this.description = payload.toString();
this.referenceCounted = payload;
}
int getReferenceCount() {
return this.referenceCounted.refCnt();
}
@Override
public String toString() {
return this.description;
}
}
}

View File

@ -0,0 +1,274 @@
/*
* 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.rsocket;
import java.time.Duration;
import io.netty.buffer.PooledByteBufAllocator;
import io.rsocket.Frame;
import io.rsocket.RSocket;
import io.rsocket.RSocketFactory;
import io.rsocket.transport.netty.client.TcpClientTransport;
import io.rsocket.transport.netty.server.CloseableChannel;
import io.rsocket.transport.netty.server.TcpServerTransport;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.ReplayProcessor;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
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.core.io.buffer.NettyDataBufferFactory;
import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
import org.springframework.util.MimeTypeUtils;
import static org.junit.Assert.*;
/**
* Server-side handling of RSocket requests.
*
* @author Rossen Stoyanchev
*/
public class RSocketClientToServerIntegrationTests {
private static AnnotationConfigApplicationContext context;
private static CloseableChannel server;
private static FireAndForgetCountingInterceptor interceptor = new FireAndForgetCountingInterceptor();
private static RSocket client;
private static RSocketRequester requester;
@BeforeClass
@SuppressWarnings("ConstantConditions")
public static void setupOnce() {
context = new AnnotationConfigApplicationContext(ServerConfig.class);
server = RSocketFactory.receive()
.addServerPlugin(interceptor)
.frameDecoder(Frame::retain) // as per https://github.com/rsocket/rsocket-java#zero-copy
.acceptor(context.getBean(MessageHandlerAcceptor.class))
.transport(TcpServerTransport.create("localhost", 7000))
.start()
.block();
client = RSocketFactory.connect()
.dataMimeType(MimeTypeUtils.TEXT_PLAIN_VALUE)
.frameDecoder(Frame::retain) // as per https://github.com/rsocket/rsocket-java#zero-copy
.transport(TcpClientTransport.create("localhost", 7000))
.start()
.block();
requester = RSocketRequester.create(
client, MimeTypeUtils.TEXT_PLAIN, context.getBean(RSocketStrategies.class));
}
@AfterClass
public static void tearDownOnce() {
client.dispose();
server.dispose();
}
@Test
public void fireAndForget() {
Flux.range(1, 3)
.concatMap(i -> requester.route("receive").data("Hello " + i).send())
.blockLast();
StepVerifier.create(context.getBean(ServerController.class).fireForgetPayloads)
.expectNext("Hello 1")
.expectNext("Hello 2")
.expectNext("Hello 3")
.thenCancel()
.verify(Duration.ofSeconds(5));
assertEquals(1, interceptor.getRSocketCount());
assertEquals("Fire and forget requests did not actually complete handling on the server side",
3, interceptor.getFireAndForgetCount(0));
}
@Test
public void echo() {
Flux<String> result = Flux.range(1, 3).concatMap(i ->
requester.route("echo").data("Hello " + i).retrieveMono(String.class));
StepVerifier.create(result)
.expectNext("Hello 1").expectNext("Hello 2").expectNext("Hello 3")
.verifyComplete();
}
@Test
public void echoAsync() {
Flux<String> result = Flux.range(1, 3).concatMap(i ->
requester.route("echo-async").data("Hello " + i).retrieveMono(String.class));
StepVerifier.create(result)
.expectNext("Hello 1 async").expectNext("Hello 2 async").expectNext("Hello 3 async")
.verifyComplete();
}
@Test
public void echoStream() {
Flux<String> result = requester.route("echo-stream").data("Hello").retrieveFlux(String.class);
StepVerifier.create(result)
.expectNext("Hello 0").expectNextCount(6).expectNext("Hello 7")
.thenCancel()
.verify();
}
@Test
public void echoChannel() {
Flux<String> result = requester.route("echo-channel")
.data(Flux.range(1, 10).map(i -> "Hello " + i), String.class)
.retrieveFlux(String.class);
StepVerifier.create(result)
.expectNext("Hello 1 async").expectNextCount(8).expectNext("Hello 10 async")
.verifyComplete();
}
@Test
public void voidReturnValue() {
Flux<String> result = requester.route("void-return-value").data("Hello").retrieveFlux(String.class);
StepVerifier.create(result).verifyComplete();
}
@Test
public void voidReturnValueFromExceptionHandler() {
Flux<String> result = requester.route("void-return-value").data("bad").retrieveFlux(String.class);
StepVerifier.create(result).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);
StepVerifier.create(result).verifyErrorMessage("No handler for destination 'invalid'");
}
@Controller
static class ServerController {
final ReplayProcessor<String> fireForgetPayloads = ReplayProcessor.create();
@MessageMapping("receive")
void receive(String payload) {
this.fireForgetPayloads.onNext(payload);
}
@MessageMapping("echo")
String echo(String payload) {
return payload;
}
@MessageMapping("echo-async")
Mono<String> echoAsync(String payload) {
return Mono.delay(Duration.ofMillis(10)).map(aLong -> payload + " async");
}
@MessageMapping("echo-stream")
Flux<String> echoStream(String payload) {
return Flux.interval(Duration.ofMillis(10)).map(aLong -> payload + " " + aLong);
}
@MessageMapping("echo-channel")
Flux<String> echoChannel(Flux<String> payloads) {
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"));
}
@MessageMapping("void-return-value")
Mono<Void> voidReturnValue(String payload) {
return !payload.equals("bad") ?
Mono.delay(Duration.ofMillis(10)).then(Mono.empty()) :
Mono.error(new IllegalStateException("bad"));
}
@MessageExceptionHandler
Mono<String> handleException(IllegalArgumentException ex) {
return Mono.delay(Duration.ofMillis(10)).map(aLong -> ex.getMessage() + " handled");
}
@MessageExceptionHandler
Mono<Void> handleExceptionWithVoidReturnValue(IllegalStateException ex) {
return Mono.delay(Duration.ofMillis(10)).then(Mono.empty());
}
}
@Configuration
static class ServerConfig {
@Bean
public ServerController controller() {
return new ServerController();
}
@Bean
public MessageHandlerAcceptor messageHandlerAcceptor() {
MessageHandlerAcceptor acceptor = new MessageHandlerAcceptor();
acceptor.setRSocketStrategies(rsocketStrategies());
return acceptor;
}
@Bean
public RSocketStrategies rsocketStrategies() {
return RSocketStrategies.builder()
.decoder(StringDecoder.allMimeTypes())
.encoder(CharSequenceEncoder.allMimeTypes())
.dataBufferFactory(new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT))
.build();
}
}
}

View File

@ -0,0 +1,285 @@
/*
* 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.rsocket;
import java.time.Duration;
import java.util.Collections;
import io.netty.buffer.PooledByteBufAllocator;
import io.rsocket.Closeable;
import io.rsocket.Frame;
import io.rsocket.RSocket;
import io.rsocket.RSocketFactory;
import io.rsocket.transport.netty.client.TcpClientTransport;
import io.rsocket.transport.netty.server.TcpServerTransport;
import io.rsocket.util.DefaultPayload;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.core.publisher.ReplayProcessor;
import reactor.core.scheduler.Schedulers;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
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.core.io.buffer.NettyDataBufferFactory;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
/**
* Client-side handling of requests initiated from the server side.
*
* @author Rossen Stoyanchev
*/
public class RSocketServerToClientIntegrationTests {
private static AnnotationConfigApplicationContext context;
private static Closeable server;
@BeforeClass
@SuppressWarnings("ConstantConditions")
public static void setupOnce() {
context = new AnnotationConfigApplicationContext(RSocketConfig.class);
server = RSocketFactory.receive()
.frameDecoder(Frame::retain) // as per https://github.com/rsocket/rsocket-java#zero-copy
.acceptor(context.getBean("serverAcceptor", MessageHandlerAcceptor.class))
.transport(TcpServerTransport.create("localhost", 7000))
.start()
.block();
}
@AfterClass
public static void tearDownOnce() {
server.dispose();
}
@Test
public void echo() {
connectAndVerify("connect.echo");
}
@Test
public void echoAsync() {
connectAndVerify("connect.echo-async");
}
@Test
public void echoStream() {
connectAndVerify("connect.echo-stream");
}
@Test
public void echoChannel() {
connectAndVerify("connect.echo-channel");
}
private static void connectAndVerify(String destination) {
ServerController serverController = context.getBean(ServerController.class);
serverController.reset();
RSocket rsocket = null;
try {
rsocket = RSocketFactory.connect()
.setupPayload(DefaultPayload.create("", destination))
.dataMimeType("text/plain")
.frameDecoder(Frame::retain) // as per https://github.com/rsocket/rsocket-java#zero-copy
.acceptor(context.getBean("clientAcceptor", MessageHandlerAcceptor.class))
.transport(TcpClientTransport.create("localhost", 7000))
.start()
.block();
serverController.await(Duration.ofSeconds(5));
}
finally {
if (rsocket != null) {
rsocket.dispose();
}
}
}
@Controller
@SuppressWarnings({"unused", "NullableProblems"})
static class ServerController {
// Must be initialized by @Test method...
volatile MonoProcessor<Void> result;
public void reset() {
this.result = MonoProcessor.create();
}
public void await(Duration duration) {
this.result.block(duration);
}
@MessageMapping("connect.echo")
void echo(RSocketRequester requester) {
runTest(() -> {
Flux<String> flux = Flux.range(1, 3).concatMap(i ->
requester.route("echo").data("Hello " + i).retrieveMono(String.class));
StepVerifier.create(flux)
.expectNext("Hello 1")
.expectNext("Hello 2")
.expectNext("Hello 3")
.verifyComplete();
});
}
@MessageMapping("connect.echo-async")
void echoAsync(RSocketRequester requester) {
runTest(() -> {
Flux<String> flux = Flux.range(1, 3).concatMap(i ->
requester.route("echo-async").data("Hello " + i).retrieveMono(String.class));
StepVerifier.create(flux)
.expectNext("Hello 1 async")
.expectNext("Hello 2 async")
.expectNext("Hello 3 async")
.verifyComplete();
});
}
@MessageMapping("connect.echo-stream")
void echoStream(RSocketRequester requester) {
runTest(() -> {
Flux<String> flux = requester.route("echo-stream").data("Hello").retrieveFlux(String.class);
StepVerifier.create(flux)
.expectNext("Hello 0")
.expectNextCount(5)
.expectNext("Hello 6")
.expectNext("Hello 7")
.thenCancel()
.verify();
});
}
@MessageMapping("connect.echo-channel")
void echoChannel(RSocketRequester requester) {
runTest(() -> {
Flux<String> flux = requester.route("echo-channel")
.data(Flux.range(1, 10).map(i -> "Hello " + i), String.class)
.retrieveFlux(String.class);
StepVerifier.create(flux)
.expectNext("Hello 1 async")
.expectNextCount(7)
.expectNext("Hello 9 async")
.expectNext("Hello 10 async")
.verifyComplete();
});
}
private void runTest(Runnable testEcho) {
Mono.fromRunnable(testEcho)
.doOnError(ex -> result.onError(ex))
.doOnSuccess(o -> result.onComplete())
.subscribeOn(Schedulers.elastic()) // StepVerifier will block
.subscribe();
}
}
private static class ClientHandler {
final ReplayProcessor<String> fireForgetPayloads = ReplayProcessor.create();
@MessageMapping("receive")
void receive(String payload) {
this.fireForgetPayloads.onNext(payload);
}
@MessageMapping("echo")
String echo(String payload) {
return payload;
}
@MessageMapping("echo-async")
Mono<String> echoAsync(String payload) {
return Mono.delay(Duration.ofMillis(10)).map(aLong -> payload + " async");
}
@MessageMapping("echo-stream")
Flux<String> echoStream(String payload) {
return Flux.interval(Duration.ofMillis(10)).map(aLong -> payload + " " + aLong);
}
@MessageMapping("echo-channel")
Flux<String> echoChannel(Flux<String> payloads) {
return payloads.delayElements(Duration.ofMillis(10)).map(payload -> payload + " async");
}
}
@Configuration
static class RSocketConfig {
@Bean
public ClientHandler clientHandler() {
return new ClientHandler();
}
@Bean
public ServerController serverController() {
return new ServerController();
}
@Bean
public MessageHandlerAcceptor clientAcceptor() {
MessageHandlerAcceptor acceptor = new MessageHandlerAcceptor();
acceptor.setHandlers(Collections.singletonList(clientHandler()));
acceptor.setAutoDetectDisabled();
acceptor.setRSocketStrategies(rsocketStrategies());
return acceptor;
}
@Bean
public MessageHandlerAcceptor serverAcceptor() {
MessageHandlerAcceptor handler = new MessageHandlerAcceptor();
handler.setRSocketStrategies(rsocketStrategies());
return handler;
}
@Bean
public RSocketStrategies rsocketStrategies() {
return RSocketStrategies.builder()
.decoder(StringDecoder.allMimeTypes())
.encoder(CharSequenceEncoder.allMimeTypes())
.dataBufferFactory(new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT))
.build();
}
}
}

View File

@ -57,7 +57,9 @@ import org.springframework.util.ReflectionUtils;
import static java.util.stream.Collectors.*;
/**
* Convenience class to resolve method parameters from hints.
* Convenience class to resolve to a Method and method parameters.
*
* <p>Note that a replica of this class also exists in spring-messaging.
*
* <h1>Background</h1>
*