Initial support for RSocket in spring-messaging
This commit is contained in:
commit
1ec3261062
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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(",", "{", "}"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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]";
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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() + "]");
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() + ". ");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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() + ". ");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 + "'");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.*;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in New Issue