INT-4033: Support SpEL in Gateway Timeouts

JIRA: https://jira.spring.io/browse/INT-4033

Provide a mechanism to support dynamic timeouts when invoking gateway methods.

Polishing - PR Comments

* Polishing `what's new`
* Add `STOMP` to the `endpoint-summary.adoc`
* Polishing code style a bit for the changes in this fix
This commit is contained in:
Gary Russell
2017-05-23 21:28:52 -04:00
committed by Artem Bilan
parent 914094e51d
commit f58106ec3c
20 changed files with 421 additions and 99 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* 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.
@@ -57,7 +57,7 @@ import java.lang.annotation.Target;
*
* @see MessagingGateway
*/
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@@ -80,19 +80,41 @@ public @interface Gateway {
/**
* Specify the timeout (ms) when sending to the request channel - only applies if the
* send might block (such as a bounded {@code QueueChannel} that is currently full.
* Overrides the encompassing gatewsy's default request timeout.
* Overrides the encompassing gateway's default request timeout.
* @return the timeout.
* @see #requestTimeoutExpression()
*/
long requestTimeout() default Long.MIN_VALUE;
/**
* Specify a SpEL Expression to determine the timeout (ms) when sending to the request
* channel - only applies if the send might block (such as a bounded
* {@code QueueChannel} that is currently full. Overrides the encompassing gateway's
* default request timeout. Overrides {@link #requestTimeout()}.
* @return the timeout.
* @since 5.0
*/
String requestTimeoutExpression() default "";
/**
* Specify the time (ms) that the thread sending the request will wait for a reply.
* The timer starts when the thread returns to the gateway, not when the request
* message is sent. Overrides the encompassing gateway's default reply timeout.
* @return the timeout.
* @see #replyTimeoutExpression()
*/
long replyTimeout() default Long.MIN_VALUE;
/**
* Specify a SpEL Expression to determine the the time (ms) that the thread sending
* the request will wait for a reply. The timer starts when the thread returns to the
* gateway, not when the request message is sent. Overrides the encompassing gateway's
* default reply timeout. Overrides {@link #replyTimeout()}.
* @return the timeout.
* @since 5.0
*/
String replyTimeoutExpression() default "";
/**
* Specify a SpEL expression to determine the payload of the request message.
* @return the expression.
@@ -103,6 +125,6 @@ public @interface Gateway {
* Specify additional headers that will be added to the request message.
* @return the headers.
*/
GatewayHeader[] headers() default {};
GatewayHeader[] headers() default { };
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2016 the original author or authors.
* Copyright 2014-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.
@@ -35,6 +35,8 @@ import java.lang.annotation.Target;
* ignores interfaces.
*
* @author Artem Bilan
* @author Gary Russell
*
* @since 4.0
*
* @see IntegrationComponentScan
@@ -76,17 +78,20 @@ public @interface MessagingGateway {
String errorChannel() default "";
/**
* Provides the amount of time dispatcher would wait to send a {@code Message}.
* This timeout would only apply if there is a potential to block in the send call.
* For example if this gateway is hooked up to a {@code QueueChannel}.
* Provides the amount of time dispatcher would wait to send a {@code Message}. This
* timeout would only apply if there is a potential to block in the send call. For
* example if this gateway is hooked up to a {@code QueueChannel}. Value is specified
* in milliseconds; it can be a simple long value or a SpEL expression; array variable
* #args is available.
* @return the suggested timeout in milliseconds, if any
*/
String defaultRequestTimeout() default "-9223372036854775808";
/**
* Allows to specify how long this gateway will wait for the reply {@code Message}
* before returning. By default it will wait indefinitely. {@code null} is returned
if the gateway times out.
* before returning. By default it will wait indefinitely. {@code null} is returned if
* the gateway times out. Value is specified in milliseconds; it can be a simple long
* value or a SpEL expression; array variable #args is available.
* @return the suggested timeout in milliseconds, if any
*/
String defaultReplyTimeout() default "-9223372036854775808";
@@ -116,7 +121,7 @@ public @interface MessagingGateway {
* all methods on the service-interface (unless overridden by a specific method).
* @return the suggested payload expression, if any
*/
GatewayHeader[] defaultHeaders() default {};
GatewayHeader[] defaultHeaders() default { };
/**
* An {@link org.springframework.integration.gateway.MethodArgsMessageMapper}

View File

@@ -145,8 +145,10 @@ public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar
gatewayProxyBuilder.addPropertyReference("mapper", mapper);
}
gatewayProxyBuilder.addPropertyValue("defaultRequestTimeout", gatewayAttributes.get("defaultRequestTimeout"));
gatewayProxyBuilder.addPropertyValue("defaultReplyTimeout", gatewayAttributes.get("defaultReplyTimeout"));
gatewayProxyBuilder.addPropertyValue("defaultRequestTimeoutExpressionString",
gatewayAttributes.get("defaultRequestTimeout"));
gatewayProxyBuilder.addPropertyValue("defaultReplyTimeoutExpressionString",
gatewayAttributes.get("defaultReplyTimeout"));
gatewayProxyBuilder.addPropertyValue("methodMetadataMap", gatewayAttributes.get("methods"));

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -97,4 +97,18 @@ public class MessagingTemplate extends GenericMessagingTemplate {
return super.sendAndReceive(destination, requestMessage);
}
public Object receiveAndConvert(MessageChannel destination, long timeout) {
Message<?> message = doReceive(destination, timeout);
if (message != null) {
return doConvert(message, null);
}
else {
return null;
}
}
public Message<?> receive(MessageChannel destination, long timeout) {
return doReceive(destination, timeout);
}
}

View File

@@ -27,6 +27,8 @@ import org.springframework.context.expression.MapAccessor;
import org.springframework.core.convert.ConversionService;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeConverter;
import org.springframework.integration.context.IntegrationContextUtils;
@@ -44,6 +46,8 @@ import org.springframework.util.Assert;
*/
public final class ExpressionUtils {
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private static final Log logger = LogFactory.getLog(ExpressionUtils.class);
private ExpressionUtils() {
@@ -141,4 +145,38 @@ public final class ExpressionUtils {
return file;
}
/**
* Return a {@link ValueExpression} for a simple literal, otherwise
* a {@link org.springframework.expression.spel.standard.SpelExpression}.
* @param expression the expression string.
* @return the expression.
* @since 5.0
*/
public static Expression intExpression(String expression) {
try {
return new ValueExpression<>(Integer.parseInt(expression));
}
catch (NumberFormatException e) {
// empty
}
return EXPRESSION_PARSER.parseExpression(expression);
}
/**
* Return a {@link ValueExpression} for a simple literal, otherwise
* a {@link org.springframework.expression.spel.standard.SpelExpression}.
* @param expression the expression string.
* @return the expression.
* @since 5.0
*/
public static Expression longExpression(String expression) {
try {
return new ValueExpression<>(Long.parseLong(expression));
}
catch (NumberFormatException e) {
// empty
}
return EXPRESSION_PARSER.parseExpression(expression);
}
}

View File

@@ -30,6 +30,7 @@ import org.springframework.util.Assert;
* @param <V> - The expected value type.
*
* @author Artem Bilan
* @author Gary Russell
* @since 4.0
*/
public class ValueExpression<V> implements Expression {
@@ -169,4 +170,9 @@ public class ValueExpression<V> implements Expression {
return this.value.toString();
}
@Override
public String toString() {
return "ValueExpression [value=" + this.value + "]";
}
}

View File

@@ -47,6 +47,7 @@ import org.springframework.integration.support.MessageBuilderFactory;
import org.springframework.integration.util.MessagingAnnotationUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.core.GenericMessagingTemplate;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
@@ -77,11 +78,12 @@ import org.springframework.util.StringUtils;
* @author Oleg Zhurakousky
* @author Gary Russell
* @author Artem Bilan
*
* @since 2.0
*/
class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]>, BeanFactoryAware {
private final Log logger = LogFactory.getLog(this.getClass());
private final static Log logger = LogFactory.getLog(GatewayMethodInboundMessageMapper.class);
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
@@ -97,6 +99,8 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
private final MethodArgsMessageMapper argsMapper;
private final MessageBuilderFactory messageBuilderFactory;
private volatile Expression payloadExpression;
private final Map<String, Expression> parameterPayloadExpressions = new HashMap<String, Expression>();
@@ -105,7 +109,9 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
private volatile BeanFactory beanFactory;
private final MessageBuilderFactory messageBuilderFactory;
private Expression sendTimeoutExpression;
private Expression replyTimeoutExpression;
GatewayMethodInboundMessageMapper(Method method) {
this(method, null);
@@ -152,11 +158,17 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
}
@Override
public void setBeanFactory(final BeanFactory beanFactory) {
if (beanFactory != null) {
this.beanFactory = beanFactory;
this.payloadExpressionEvaluationContext = ExpressionUtils.createStandardEvaluationContext(beanFactory);
}
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
this.payloadExpressionEvaluationContext = ExpressionUtils.createStandardEvaluationContext(beanFactory);
}
public void setSendTimeoutExpression(Expression sendTimeoutExpression) {
this.sendTimeoutExpression = sendTimeoutExpression;
}
public void setReplyTimeoutExpression(Expression replyTimeoutExpression) {
this.replyTimeoutExpression = replyTimeoutExpression;
}
@Override
@@ -186,12 +198,10 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
private Map<String, Object> evaluateHeaders(EvaluationContext methodInvocationEvaluationContext,
Map<String, Expression> headerExpressions) {
Map<String, Object> evaluatedHeaders = new HashMap<String, Object>();
Map<String, Object> evaluatedHeaders = new HashMap<>();
for (Map.Entry<String, Expression> entry : headerExpressions.entrySet()) {
Object value = entry.getValue().getValue(methodInvocationEvaluationContext);
if (value != null) {
evaluatedHeaders.put(entry.getKey(), value);
}
evaluatedHeaders.put(entry.getKey(), value);
}
return evaluatedHeaders;
}
@@ -205,11 +215,8 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
}
private Object evaluatePayloadExpression(String expressionString, Object argumentValue) {
Expression expression = this.parameterPayloadExpressions.get(expressionString);
if (expression == null) {
expression = PARSER.parseExpression(expressionString);
this.parameterPayloadExpressions.put(expressionString, expression);
}
Expression expression =
this.parameterPayloadExpressions.computeIfAbsent(expressionString, PARSER::parseExpression);
return expression.getValue(this.payloadExpressionEvaluationContext, argumentValue);
}
@@ -218,8 +225,8 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
for (Entry<?, ?> entry : argumentValue.entrySet()) {
Object key = entry.getKey();
if (!(key instanceof String)) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Invalid header name [" + key +
if (logger.isWarnEnabled()) {
logger.warn("Invalid header name [" + key +
"], name type must be String. Skipping mapping of this header to MessageHeaders.");
}
}
@@ -232,7 +239,7 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
private void throwExceptionForMultipleMessageOrPayloadParameters(MethodParameter methodParameter) {
throw new MessagingException(
"At most one parameter (or expression via method-level @Payload) may be mapped to the " +
"payload or Message. Found more than one on method [" + methodParameter.getMethod() + "]");
"payload or Message. Found more than one on method [" + methodParameter.getMethod() + "]");
}
private String determineHeaderName(Annotation headerAnnotation, MethodParameter methodParameter) {
@@ -244,7 +251,7 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
}
private static List<MethodParameter> getMethodParameterList(Method method) {
List<MethodParameter> parameterList = new LinkedList<MethodParameter>();
List<MethodParameter> parameterList = new LinkedList<>();
ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
int parameterCount = method.getParameterTypes().length;
for (int i = 0; i < parameterCount; i++) {
@@ -335,14 +342,26 @@ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]
GatewayMethodInboundMessageMapper.this.copyHeaders((Map<?, ?>) argumentValue, headers);
}
else if (GatewayMethodInboundMessageMapper.this.payloadExpression == null) {
GatewayMethodInboundMessageMapper.this.throwExceptionForMultipleMessageOrPayloadParameters(methodParameter);
GatewayMethodInboundMessageMapper.this
.throwExceptionForMultipleMessageOrPayloadParameters(methodParameter);
}
}
Assert.isTrue(messageOrPayload != null,
"unable to determine a Message or payload parameter on method [" + GatewayMethodInboundMessageMapper.this.method + "]");
AbstractIntegrationMessageBuilder<?> builder = (messageOrPayload instanceof Message)
? GatewayMethodInboundMessageMapper.this.messageBuilderFactory.fromMessage((Message<?>) messageOrPayload)
: GatewayMethodInboundMessageMapper.this.messageBuilderFactory.withPayload(messageOrPayload);
Assert.isTrue(messageOrPayload != null, "unable to determine a Message or payload parameter on method ["
+ GatewayMethodInboundMessageMapper.this.method + "]");
if (GatewayMethodInboundMessageMapper.this.sendTimeoutExpression != null) {
headers.computeIfAbsent(GenericMessagingTemplate.DEFAULT_SEND_TIMEOUT_HEADER,
v -> GatewayMethodInboundMessageMapper.this.sendTimeoutExpression
.getValue(methodInvocationEvaluationContext, Long.class));
}
if (GatewayMethodInboundMessageMapper.this.replyTimeoutExpression != null) {
headers.computeIfAbsent(GenericMessagingTemplate.DEFAULT_RECEIVE_TIMEOUT_HEADER,
v -> GatewayMethodInboundMessageMapper.this.replyTimeoutExpression
.getValue(methodInvocationEvaluationContext, Long.class));
}
AbstractIntegrationMessageBuilder<?> builder =
(messageOrPayload instanceof Message)
? GatewayMethodInboundMessageMapper.this.messageBuilderFactory.fromMessage((Message<?>) messageOrPayload)
: GatewayMethodInboundMessageMapper.this.messageBuilderFactory.withPayload(messageOrPayload);
builder.copyHeadersIfAbsent(headers);
// Explicit headers in XML override any @Header annotations...
if (!CollectionUtils.isEmpty(GatewayMethodInboundMessageMapper.this.headerExpressions)) {

View File

@@ -45,11 +45,15 @@ import org.springframework.core.task.AsyncListenableTaskExecutor;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.support.TaskExecutorAdapter;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.GatewayHeader;
import org.springframework.integration.endpoint.AbstractEndpoint;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.expression.ValueExpression;
import org.springframework.integration.support.channel.BeanFactoryChannelResolver;
import org.springframework.integration.support.management.TrackableComponent;
import org.springframework.integration.support.utils.IntegrationUtils;
@@ -99,9 +103,9 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
private volatile String errorChannelName;
private volatile Long defaultRequestTimeout;
private volatile Expression defaultRequestTimeout;
private volatile Long defaultReplyTimeout;
private volatile Expression defaultReplyTimeout;
private volatile DestinationResolver<MessageChannel> channelResolver;
@@ -131,6 +135,8 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
private volatile MethodArgsMessageMapper argsMapper;
private EvaluationContext evaluationContext = new StandardEvaluationContext();
/**
* Create a Factory whose service interface type can be configured by setter injection.
* If none is set, it will fall back to the default service interface type,
@@ -223,25 +229,77 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
}
/**
* Set the default timeout value for sending request messages. If not
* explicitly configured with an annotation, this value will be used.
* Set the default timeout value for sending request messages. If not explicitly
* configured with an annotation, or on a method element, this value will be used.
*
* @param defaultRequestTimeout the timeout value in milliseconds
*/
public void setDefaultRequestTimeout(Long defaultRequestTimeout) {
this.defaultRequestTimeout = new ValueExpression<>(defaultRequestTimeout);
}
/**
* Set an expression to be evaluated to determine the default timeout value for
* sending request messages. If not explicitly configured with an annotation, or on a
* method element, this value will be used.
*
* @param defaultRequestTimeout the timeout value in milliseconds
* @since 5.0
*/
public void setDefaultRequestTimeoutExpression(Expression defaultRequestTimeout) {
this.defaultRequestTimeout = defaultRequestTimeout;
}
/**
* Set the default timeout value for receiving reply messages. If not
* explicitly configured with an annotation, this value will be used.
* Set an expression to be evaluated to determine the default timeout value for
* sending request messages. If not explicitly configured with an annotation, or on a
* method element, this value will be used.
*
* @param defaultRequestTimeout the timeout value in milliseconds
* @since 5.0
*/
public void setDefaultRequestTimeoutExpressionString(String defaultRequestTimeout) {
if (StringUtils.hasText(defaultRequestTimeout)) {
this.defaultRequestTimeout = ExpressionUtils.longExpression(defaultRequestTimeout);
}
}
/**
* Set the default timeout value for receiving reply messages. If not explicitly
* configured with an annotation, or on a method element, this value will be used.
*
* @param defaultReplyTimeout the timeout value in milliseconds
*/
public void setDefaultReplyTimeout(Long defaultReplyTimeout) {
this.defaultReplyTimeout = new ValueExpression<>(defaultReplyTimeout);
}
/**
* Set an expression to be evaluated to determine the default timeout value for
* receiving reply messages. If not explicitly configured with an annotation, or on a
* method element, this value will be used.
*
* @param defaultReplyTimeout the timeout value in milliseconds
* @since 5.0
*/
public void setDefaultReplyTimeoutExpression(Expression defaultReplyTimeout) {
this.defaultReplyTimeout = defaultReplyTimeout;
}
/**
* Set an expression to be evaluated to determine the default timeout value for
* receiving reply messages. If not explicitly configured with an annotation, or on a
* method element, this value will be used.
*
* @param defaultReplyTimeout the timeout value in milliseconds
* @since 5.0
*/
public void setDefaultReplyTimeoutExpressionString(String defaultReplyTimeout) {
if (StringUtils.hasText(defaultReplyTimeout)) {
this.defaultReplyTimeout = ExpressionUtils.longExpression(defaultReplyTimeout);
}
}
@Override
public void setShouldTrack(boolean shouldTrack) {
this.shouldTrack = shouldTrack;
@@ -336,7 +394,7 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
this.asyncSubmitListenableType = submitType.getClass();
}
}
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
this.initialized = true;
}
}
@@ -428,11 +486,25 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
hasPayloadExpression = (metadata != null) && StringUtils.hasText(metadata.getPayloadExpression());
}
if (paramCount == 0 && !hasPayloadExpression) {
Long receiveTimeout = null;
if (gateway.getReceiveTimeoutExpression() != null) {
receiveTimeout = gateway.getReceiveTimeoutExpression().getValue(this.evaluationContext, Long.class);
}
if (shouldReply) {
if (shouldReturnMessage) {
return gateway.receiveMessage();
if (receiveTimeout != null) {
return gateway.receiveMessage(receiveTimeout);
}
else {
return gateway.receiveMessage();
}
}
if (receiveTimeout != null) {
response = gateway.receive(receiveTimeout);
}
else {
response = gateway.receive();
}
response = gateway.receive();
}
}
else {
@@ -472,8 +544,8 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
Gateway gatewayAnnotation = method.getAnnotation(Gateway.class);
String requestChannelName = null;
String replyChannelName = null;
Long requestTimeout = this.defaultRequestTimeout;
Long replyTimeout = this.defaultReplyTimeout;
Expression requestTimeout = this.defaultRequestTimeout;
Expression replyTimeout = this.defaultReplyTimeout;
String payloadExpression = this.globalMethodMetadata != null
? this.globalMethodMetadata.getPayloadExpression()
: null;
@@ -489,10 +561,16 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
* no longer work as expected; they will need to use, say, -1 instead.
*/
if (requestTimeout == null || gatewayAnnotation.requestTimeout() != Long.MIN_VALUE) {
requestTimeout = gatewayAnnotation.requestTimeout();
requestTimeout = new ValueExpression<>(gatewayAnnotation.requestTimeout());
}
if (StringUtils.hasText(gatewayAnnotation.requestTimeoutExpression())) {
requestTimeout = ExpressionUtils.longExpression(gatewayAnnotation.requestTimeoutExpression());
}
if (replyTimeout == null || gatewayAnnotation.replyTimeout() != Long.MIN_VALUE) {
replyTimeout = gatewayAnnotation.replyTimeout();
replyTimeout = new ValueExpression<>(gatewayAnnotation.replyTimeout());
}
if (StringUtils.hasText(gatewayAnnotation.replyTimeoutExpression())) {
replyTimeout = ExpressionUtils.longExpression(gatewayAnnotation.replyTimeoutExpression());
}
if (payloadExpression == null || StringUtils.hasText(gatewayAnnotation.payloadExpression())) {
payloadExpression = gatewayAnnotation.payloadExpression();
@@ -529,11 +607,11 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
replyChannelName = methodMetadata.getReplyChannelName();
String reqTimeout = methodMetadata.getRequestTimeout();
if (StringUtils.hasText(reqTimeout)) {
requestTimeout = this.convert(reqTimeout, Long.class);
requestTimeout = ExpressionUtils.longExpression(reqTimeout);
}
String repTimeout = methodMetadata.getReplyTimeout();
if (StringUtils.hasText(repTimeout)) {
replyTimeout = this.convert(repTimeout, Long.class);
replyTimeout = ExpressionUtils.longExpression(repTimeout);
}
}
}
@@ -551,7 +629,7 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
if (StringUtils.hasText(payloadExpression)) {
messageMapper.setPayloadExpression(payloadExpression);
}
messageMapper.setBeanFactory(this.getBeanFactory());
messageMapper.setBeanFactory(getBeanFactory());
MethodInvocationGateway gateway = new MethodInvocationGateway(messageMapper);
if (this.errorChannel != null) {
@@ -589,18 +667,27 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
if (requestTimeout == null) {
gateway.setRequestTimeout(-1);
}
else if (requestTimeout instanceof ValueExpression) {
gateway.setRequestTimeout(requestTimeout.getValue(Long.class));
}
else {
gateway.setRequestTimeout(requestTimeout);
messageMapper.setSendTimeoutExpression(requestTimeout);
}
if (replyTimeout == null) {
gateway.setReplyTimeout(-1);
}
else if (replyTimeout instanceof ValueExpression) {
gateway.setReplyTimeout(replyTimeout.getValue(Long.class));
}
else {
gateway.setReplyTimeout(replyTimeout);
messageMapper.setReplyTimeoutExpression(replyTimeout);
}
if (this.getBeanFactory() != null) {
gateway.setBeanFactory(this.getBeanFactory());
}
if (replyTimeout != null) {
gateway.setReceiveTimeoutExpression(replyTimeout);
}
gateway.setShouldTrack(this.shouldTrack);
gateway.afterPropertiesSet();
return gateway;
@@ -641,7 +728,7 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
private static boolean hasReturnParameterizedWithMessage(Method method, boolean runningOnCallerThread) {
if (!runningOnCallerThread &&
(Future.class.isAssignableFrom(method.getReturnType())
|| Mono.class.isAssignableFrom(method.getReturnType()))) {
|| Mono.class.isAssignableFrom(method.getReturnType()))) {
Type returnType = method.getGenericReturnType();
if (returnType instanceof ParameterizedType) {
Type[] typeArgs = ((ParameterizedType) returnType).getActualTypeArguments();
@@ -662,8 +749,18 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
private static final class MethodInvocationGateway extends MessagingGatewaySupport {
Expression receiveTimeoutExpression;
MethodInvocationGateway(GatewayMethodInboundMessageMapper messageMapper) {
this.setRequestMapper(messageMapper);
setRequestMapper(messageMapper);
}
Expression getReceiveTimeoutExpression() {
return this.receiveTimeoutExpression;
}
void setReceiveTimeoutExpression(Expression receiveTimeoutExpression) {
this.receiveTimeoutExpression = receiveTimeoutExpression;
}
}

View File

@@ -418,6 +418,22 @@ public abstract class MessagingGatewaySupport extends AbstractEndpoint
return this.messagingTemplate.receive(replyChannel);
}
protected Object receive(long timeout) {
this.initializeIfNecessary();
MessageChannel replyChannel = getReplyChannel();
Assert.state(replyChannel != null && (replyChannel instanceof PollableChannel),
"receive is not supported, because no pollable reply channel has been configured");
return this.messagingTemplate.receiveAndConvert(replyChannel, timeout);
}
protected Message<?> receiveMessage(long timeout) {
initializeIfNecessary();
MessageChannel replyChannel = getReplyChannel();
Assert.state(replyChannel instanceof PollableChannel,
"receive is not supported, because no pollable reply channel has been configured");
return this.messagingTemplate.receive(replyChannel, timeout);
}
protected Object sendAndReceive(Object object) {
return this.doSendAndReceive(object, true);
}

View File

@@ -735,7 +735,8 @@
Provides the amount of time dispatcher would wait to send a message.
This timeout would only apply if there is a potential to block in the send call.
For example if this gateway is hooked up to a Queue channel. 
Value is specified in milliseconds.
Value is specified in milliseconds; it can be a simple long value or a SpEL
expression; array variable #args is available.
]]>
</xsd:documentation>
</xsd:annotation>
@@ -747,7 +748,9 @@
Allows you to specify how long this gateway will wait for the reply message
before returning. By default it will wait indefinitely. 'null' is returned
if the gateway times out.
Value is specified in milliseconds.
Value is specified in milliseconds; it can be a simple long value or a SpEL
expression; array variable #args is available.
Also used for receive-only operations as the receive timeout.
]]>
</xsd:documentation>
</xsd:annotation>
@@ -850,7 +853,8 @@
Provides the amount of time dispatcher would wait to send a message.
This timeout would only apply if there is a potential to block in the send call.
For example if this gateway is hooked up to a Queue channel. 
Value is specified in milliseconds.
Value is specified in milliseconds; it can be a simple long value or a SpEL
expression; array variable '#args' is available.
]]>
</xsd:documentation>
</xsd:annotation>
@@ -871,7 +875,9 @@
Specifies how long this gateway will wait for the reply message
before returning. By default it will wait indefinitely. 'null' is returned
if the gateway times out.
Value is specified in milliseconds.
Value is specified in milliseconds; it can be a simple long value or a SpEL
expression; array variable '#args' is available.
Also used for receive-only operations as the receive timeout.
]]>
</xsd:documentation>
</xsd:annotation>

View File

@@ -46,6 +46,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.MessageRejectedException;
import org.springframework.integration.channel.QueueChannel;
@@ -391,8 +392,10 @@ public class ChainParserTests {
GatewayProxyFactoryBean.class);
assertEquals("strings", TestUtils.getPropertyValue(gatewayProxyFactoryBean, "defaultRequestChannelName"));
assertEquals("numbers", TestUtils.getPropertyValue(gatewayProxyFactoryBean, "defaultReplyChannelName"));
assertEquals(new Long(1000), TestUtils.getPropertyValue(gatewayProxyFactoryBean, "defaultRequestTimeout", Long.class));
assertEquals(new Long(100), TestUtils.getPropertyValue(gatewayProxyFactoryBean, "defaultReplyTimeout", Long.class));
assertEquals(new Long(1000), TestUtils
.getPropertyValue(gatewayProxyFactoryBean, "defaultRequestTimeout", Expression.class).getValue());
assertEquals(new Long(100), TestUtils
.getPropertyValue(gatewayProxyFactoryBean, "defaultReplyTimeout", Expression.class).getValue());
assertTrue(this.beanFactory.containsBean("subComponentsIdSupport1$child.objectToStringTransformerWithinChain.handler"));
assertTrue(this.beanFactory.containsBean("subComponentsIdSupport1$child.objectToMapTransformerWithinChain.handler"));

View File

@@ -79,6 +79,8 @@
service-interface="org.springframework.integration.gateway.TestService"
default-request-channel="requestChannel"
default-reply-channel="replyChannel"
default-request-timeout="1000"
default-reply-timeout="2000"
async-executor="testExecutor">
<default-header name="baz" value="qux"/>
<method name="oneWay" request-channel="otherRequestChannel"
@@ -88,6 +90,10 @@
reply-channel="foo">
<header name="foo" value="bar"/>
</method>
<method name="oneWayWithTimeouts" request-channel="otherRequestChannel"
request-timeout="#args[1]"
reply-timeout="#args[2]">
</method>
</gateway>
<!-- no assertions for this. The fact that this config does not result in error is sufficient -->

View File

@@ -45,6 +45,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.expression.Expression;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.config.IntegrationConfigUtils;
import org.springframework.integration.gateway.GatewayMethodMetadata;
@@ -95,16 +96,26 @@ public class GatewayParserTests {
service.oneWay("foo");
PollableChannel channel = (PollableChannel) context.getBean("otherRequestChannel");
Message<?> result = channel.receive(10000);
assertNotNull(result);
assertEquals("fiz", result.getPayload());
assertEquals("bar", result.getHeaders().get("foo"));
assertEquals("qux", result.getHeaders().get("baz"));
GatewayProxyFactoryBean fb = context.getBean("&methodOverride", GatewayProxyFactoryBean.class);
assertEquals(1000L, TestUtils.getPropertyValue(fb, "defaultRequestTimeout", Expression.class).getValue());
assertEquals(2000L, TestUtils.getPropertyValue(fb, "defaultReplyTimeout", Expression.class).getValue());
Map<?, ?> methods = TestUtils.getPropertyValue(fb, "methodMetadataMap", Map.class);
GatewayMethodMetadata meta = (GatewayMethodMetadata) methods.get("oneWay");
assertNotNull(meta);
assertEquals("456", meta.getRequestTimeout());
assertEquals("123", meta.getReplyTimeout());
assertEquals("foo", meta.getReplyChannelName());
meta = (GatewayMethodMetadata) methods.get("oneWayWithTimeouts");
assertNotNull(meta);
assertEquals("#args[1]", meta.getRequestTimeout());
assertEquals("#args[2]", meta.getReplyTimeout());
service.oneWayWithTimeouts("foo", 100L, 200L);
result = channel.receive(10000);
assertNotNull(result);
}
@Test

View File

@@ -19,7 +19,6 @@
<int:method name="baz">
<int:header name="name" value="overrideGlobal"/>
</int:method>
<int:method name="lateReply" request-channel="lateReplyChannel" reply-timeout="0"/>
</int:gateway>
<int:chain input-channel="lateReplyChannel" >

View File

@@ -46,7 +46,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -63,6 +62,7 @@ import org.springframework.context.annotation.FilterType;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.annotation.AnnotationConstants;
import org.springframework.integration.annotation.BridgeTo;
@@ -357,15 +357,17 @@ public class GatewayInterfaceTests {
@Test
public void testLateReply() throws Exception {
ConfigurableApplicationContext ac = new ClassPathXmlApplicationContext("GatewayInterfaceTests-context.xml", this.getClass());
ConfigurableApplicationContext ac = new ClassPathXmlApplicationContext("GatewayInterfaceTests-context.xml",
this.getClass());
Bar baz = ac.getBean(Bar.class);
String reply = baz.lateReply("hello");
String reply = baz.lateReply("hello", 1000, 0);
assertNull(reply);
PollableChannel errorChannel = ac.getBean("errorChannel", PollableChannel.class);
Message<?> receive = errorChannel.receive(5000);
assertNotNull(receive);
MessagingException messagingException = (MessagingException) receive.getPayload();
assertThat(messagingException.getMessage(), Matchers.startsWith("Reply message received but the receiving thread has exited due to a timeout"));
assertThat(messagingException.getMessage(),
startsWith("Reply message received but the receiving thread has exited due to a timeout"));
ac.close();
}
@@ -436,10 +438,12 @@ public class GatewayInterfaceTests {
assertNotNull(this.gatewayByAnnotationGPFB);
assertSame(this.exec, this.annotationGatewayProxyFactoryBean.getAsyncExecutor());
assertEquals(1111L,
TestUtils.getPropertyValue(this.annotationGatewayProxyFactoryBean, "defaultRequestTimeout"));
assertEquals(222L,
TestUtils.getPropertyValue(this.annotationGatewayProxyFactoryBean, "defaultReplyTimeout"));
assertEquals(1111L, TestUtils
.getPropertyValue(this.annotationGatewayProxyFactoryBean, "defaultRequestTimeout", Expression.class)
.getValue());
assertEquals(222L, TestUtils
.getPropertyValue(this.annotationGatewayProxyFactoryBean, "defaultReplyTimeout", Expression.class)
.getValue());
Collection<MessagingGatewaySupport> messagingGateways =
this.annotationGatewayProxyFactoryBean.getGateways().values();
@@ -490,7 +494,9 @@ public class GatewayInterfaceTests {
void baz(String payload);
String lateReply(String payload);
@Gateway(payloadExpression = "#args[0]", requestChannel = "lateReplyChannel",
requestTimeoutExpression = "#args[1]", replyTimeoutExpression = "#args[2]")
String lateReply(String payload, long requestTimeout, long replyTimeout);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* 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.
@@ -24,7 +24,9 @@ import java.util.Map.Entry;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.Expression;
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.test.context.ContextConfiguration;
@@ -44,7 +46,8 @@ public class GatewayXmlAndAnnotationTests {
@Test
public void test() {
assertEquals(123L, TestUtils.getPropertyValue(gatewayProxyFactoryBean, "defaultReplyTimeout"));
assertEquals(123L, TestUtils.getPropertyValue(gatewayProxyFactoryBean, "defaultReplyTimeout", Expression.class)
.getValue());
@SuppressWarnings("unchecked")
Map<Method, MessagingGatewaySupport> gatewayMap = TestUtils.getPropertyValue(gatewayProxyFactoryBean,
"gatewayMap", Map.class);

View File

@@ -37,6 +37,9 @@ public interface TestService {
void oneWay(String input);
@Payload("#args[0]")
void oneWayWithTimeouts(String input, Long sendTimeout, Long receiveTimeout);
String solicitResponse();
Message<String> getMessage();