INT-3944: Async JMS Outbound Gateway

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

Polishing (PR comments) and Doc Polish

Polishing - PR Comments

Remove custom `async` boolean in favor of the now public setter.

Add exception to `JmsException` hierarchy.

INT-3944: Polishing

* Rename `asyncReplySupported` just to `async`. As well as its getter and setter. Plus fix docs on the matter
* Polishing for the `JmsOutboundGateway` according PR comments
* Rework `JmsOutboundGateway` to return `AbstractIntegrationMessageBuilder` instead of `Message`
since `AbstractMessageProducingHandler` rebuilds `Message` for a new one to copy headers from request
* Add `getPayload()` and `getHeaders()` to the `AbstractIntegrationMessageBuilder` to make the `Routing Slip`
users happy, when the `AbstractIntegrationMessageBuilder` is pushed to the `Routing Slip path` function
* Fix race condition in the `ChatMessageListeningEndpointTests`: the `stanza` parsing is done in the different Thread.

Doc Polishing (Routing Slip)

Fix timing issue in the `LastModifiedFileListFilterTests`:
`Thread.sleep()` not always reflects the reality.
Switch to the "past simulation" via explicit `File.setLastModified()`
This commit is contained in:
Gary Russell
2016-03-23 10:12:13 -04:00
committed by Artem Bilan
parent 0fd72d2d18
commit 4bfcdb9dfa
24 changed files with 539 additions and 325 deletions

View File

@@ -49,7 +49,7 @@ public class AsyncAmqpOutboundGateway extends AbstractAmqpOutboundEndpoint {
this.messageConverter = template.getMessageConverter();
Assert.notNull(this.messageConverter, "the template's message converter cannot be null");
setConnectionFactory(this.template.getConnectionFactory());
setAsyncReplySupported(true);
setAsync(true);
}
@Override

View File

@@ -84,7 +84,7 @@ public abstract class AbstractSimpleMessageHandlerFactoryBean<H extends MessageH
private DestinationResolver<MessageChannel> channelResolver;
private Boolean asyncReplySupported;
private Boolean async;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
@@ -150,11 +150,11 @@ public abstract class AbstractSimpleMessageHandlerFactoryBean<H extends MessageH
* namespace. It's not clear that other endpoints would benefit from async support,
* but any subclass of {@link AbstractReplyProducingMessageHandler} can potentially
* return a {@code ListenableFuture<?>}.
* @param asyncReplySupported the asyncReplySupported to set.
* @param async the async to set.
* @since 4.3
*/
public void setAsyncReplySupported(Boolean asyncReplySupported) {
this.asyncReplySupported = asyncReplySupported;
public void setAsync(Boolean async) {
this.async = async;
}
/**
@@ -223,10 +223,10 @@ public abstract class AbstractSimpleMessageHandlerFactoryBean<H extends MessageH
+ (name == null ? "" : (", " + name)) + ".");
}
}
if (this.asyncReplySupported != null) {
if (this.async != null) {
if (actualHandler instanceof AbstractReplyProducingMessageHandler) {
((AbstractReplyProducingMessageHandler) actualHandler)
.setAsyncReplySupported(this.asyncReplySupported);
.setAsync(this.async);
}
}
if (this.handler instanceof Orderable && this.order != null) {

View File

@@ -43,7 +43,7 @@ public class ServiceActivatorParser extends AbstractDelegatingConsumerEndpointPa
@Override
void postProcess(BeanDefinitionBuilder builder, Element element, ParserContext parserContext) {
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "async", "asyncReplySupported");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "async");
}
}

View File

@@ -55,7 +55,7 @@ public abstract class AbstractMessageProducingHandler extends AbstractMessageHan
private volatile String outputChannelName;
private volatile boolean asyncReplySupported;
private volatile boolean async;
/**
* Set the timeout for sending reply Messages.
@@ -79,21 +79,20 @@ public abstract class AbstractMessageProducingHandler extends AbstractMessageHan
* Allow async replies. If the handler reply is a {@link ListenableFuture} send
* the output when it is satisfied rather than sending the future as the result.
* Only subclasses that support this feature should set it.
* @param asyncReplySupported true to allow.
*
* @param async true to allow.
* @since 4.3
*/
public final void setAsyncReplySupported(boolean asyncReplySupported) {
this.asyncReplySupported = asyncReplySupported;
public final void setAsync(boolean async) {
this.async = async;
}
/**
* @see #setAsyncReplySupported(boolean)
* @see #setAsync(boolean)
* @return true if this handler supports async replies.
* @since 4.3
*/
protected boolean getAsyncReplySupported() {
return this.asyncReplySupported;
protected boolean isAsync() {
return this.async;
}
@Override
@@ -177,7 +176,7 @@ public abstract class AbstractMessageProducingHandler extends AbstractMessageHan
}
}
if (this.asyncReplySupported && reply instanceof ListenableFuture<?>) {
if (this.async && reply instanceof ListenableFuture<?>) {
ListenableFuture<?> future = (ListenableFuture<?>) reply;
final Object theReplyChannel = replyChannel;
future.addCallback(new ListenableFutureCallback<Object>() {
@@ -230,7 +229,7 @@ public abstract class AbstractMessageProducingHandler extends AbstractMessageHan
}
else {
throw new IllegalArgumentException("The RoutingSlip 'path' can be of " +
"String or RoutingSlipRouteStrategy type, but gotten: " + path);
"String or RoutingSlipRouteStrategy type, but got: " + path.getClass());
}
if (routingSlipPathValue instanceof MessageChannel) {

View File

@@ -107,11 +107,11 @@ public abstract class AbstractReplyProducingMessageHandler extends AbstractMessa
if (result != null) {
sendOutputs(result, message);
}
else if (this.requiresReply && !getAsyncReplySupported()) {
else if (this.requiresReply && !isAsync()) {
throw new ReplyRequiredException(message, "No reply produced by handler '" +
getComponentName() + "', and its 'requiresReply' property is set to true.");
}
else if (!getAsyncReplySupported() && logger.isDebugEnabled()) {
else if (!isAsync() && logger.isDebugEnabled()) {
logger.debug("handler '" + this + "' produced no reply for request Message: " + message);
}
}

View File

@@ -29,6 +29,13 @@ import org.springframework.messaging.Message;
*/
public interface RoutingSlipRouteStrategy {
/**
* Get the next path for this routing slip.
* @param requestMessage the request message.
* @param reply the reply - depending on context, this may be a user-level domain
* object, a {@link Message} or a {@code AbstractIntegrationMessageBuilder}.
* @return a channel name or another {@link RoutingSlipRouteStrategy}.
*/
Object getNextPath(Message<?> requestMessage, Object reply);
}

View File

@@ -36,6 +36,10 @@ import org.springframework.util.Assert;
*/
public abstract class AbstractIntegrationMessageBuilder<T> {
public abstract T getPayload();
public abstract Map<String, Object> getHeaders();
/**
* Set the value for the given header name. If the provided value is <code>null</code>, the header will be removed.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2016 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.
@@ -38,6 +38,7 @@ import org.springframework.util.Assert;
* @author Oleg Zhurakousky
* @author Dave Syer
* @author Gary Russell
* @author Artem Bilan
*/
public final class MessageBuilder<T> extends AbstractIntegrationMessageBuilder<T> {
@@ -62,6 +63,16 @@ public final class MessageBuilder<T> extends AbstractIntegrationMessageBuilder<T
}
}
@Override
public T getPayload() {
return this.payload;
}
@Override
public Map<String, Object> getHeaders() {
return this.headerAccessor.toMap();
}
/**
* Create a builder for a new {@link Message} instance pre-populated with all of the headers copied from the
* provided message. The payload of the provided Message will also be used as the payload for the new message.
@@ -72,8 +83,7 @@ public final class MessageBuilder<T> extends AbstractIntegrationMessageBuilder<T
*/
public static <T> MessageBuilder<T> fromMessage(Message<T> message) {
Assert.notNull(message, "message must not be null");
MessageBuilder<T> builder = new MessageBuilder<T>(message.getPayload(), message);
return builder;
return new MessageBuilder<T>(message.getPayload(), message);
}
/**
@@ -84,8 +94,7 @@ public final class MessageBuilder<T> extends AbstractIntegrationMessageBuilder<T
* @return A MessageBuilder.
*/
public static <T> MessageBuilder<T> withPayload(T payload) {
MessageBuilder<T> builder = new MessageBuilder<T>(payload, null);
return builder;
return new MessageBuilder<T>(payload, null);
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2015 the original author or authors.
* Copyright 2014-2016 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.
@@ -30,6 +30,7 @@ import org.springframework.util.StringUtils;
/**
* @author Gary Russell
* @author Artem Bilan
* @since 4.0
*
*/
@@ -53,6 +54,16 @@ public class MutableMessageBuilder<T> extends AbstractIntegrationMessageBuilder<
this.headers = this.mutableMessage.getRawHeaders();
}
@Override
public T getPayload() {
return this.mutableMessage.getPayload();
}
@Override
public Map<String, Object> getHeaders() {
return this.headers;
}
/**
* Create a builder for a new {@link Message} instance pre-populated with all of the headers copied from the
* provided message. The payload of the provided Message will also be used as the payload for the new message.

View File

@@ -111,7 +111,7 @@ public class AsyncHandlerTests {
}
};
this.handler.setAsyncReplySupported(true);
this.handler.setAsync(true);
this.handler.setOutputChannel(this.output);
this.handler.setBeanFactory(mock(BeanFactory.class));
this.latch = new CountDownLatch(1);

View File

@@ -45,7 +45,7 @@ public class LastModifiedFileListFilterTests {
fileOutputStream.write("x".getBytes());
fileOutputStream.close();
assertEquals(0, filter.filterFiles(new File[] { foo }).size());
Thread.sleep(2000);
foo.setLastModified(System.currentTimeMillis() - 10000);
assertEquals(1, filter.filterFiles(new File[] { foo }).size());
}

View File

@@ -51,6 +51,7 @@ import org.springframework.integration.MessageTimeoutException;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.integration.handler.ExpressionEvaluatingMessageProcessor;
import org.springframework.integration.jms.util.JmsAdapterUtils;
import org.springframework.integration.support.AbstractIntegrationMessageBuilder;
import org.springframework.jms.connection.ConnectionFactoryUtils;
import org.springframework.jms.listener.DefaultMessageListenerContainer;
import org.springframework.jms.support.JmsUtils;
@@ -66,6 +67,7 @@ import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.concurrent.SettableListenableFuture;
/**
* An outbound Messaging Gateway for request/reply JMS.
@@ -79,6 +81,23 @@ import org.springframework.util.StringUtils;
*/
public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler implements Lifecycle, MessageListener {
private final Object initializationMonitor = new Object();
private final AtomicLong correlationId = new AtomicLong();
private final String gatewayCorrelation = UUID.randomUUID().toString();
private final Map<String, LinkedBlockingQueue<javax.jms.Message>> replies =
new ConcurrentHashMap<String, LinkedBlockingQueue<javax.jms.Message>>();
private final ConcurrentHashMap<String, TimedReply> earlyOrLateReplies =
new ConcurrentHashMap<String, JmsOutboundGateway.TimedReply>();
private final Map<String, SettableListenableFuture<AbstractIntegrationMessageBuilder<?>>> futures =
new ConcurrentHashMap<String, SettableListenableFuture<AbstractIntegrationMessageBuilder<?>>>();
private final Object lifeCycleMonitor = new Object();
private volatile Destination requestDestination;
private volatile String requestDestinationName;
@@ -127,24 +146,10 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
private volatile boolean useReplyContainer;
private final Object initializationMonitor = new Object();
private volatile boolean active;
private final AtomicLong correlationId = new AtomicLong();
private final String gatewayCorrelation = UUID.randomUUID().toString();
private final Map<String, LinkedBlockingQueue<javax.jms.Message>> replies =
new ConcurrentHashMap<String, LinkedBlockingQueue<javax.jms.Message>>();
private final ConcurrentHashMap<String, TimedReply> earlyOrLateReplies =
new ConcurrentHashMap<String, JmsOutboundGateway.TimedReply>();
private volatile ScheduledFuture<?> reaper;
private final Object lifeCycleMonitor = new Object();
private volatile boolean requiresReply;
private long lastSend;
@@ -210,7 +215,8 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
*/
public void setRequestDestinationExpression(Expression requestDestinationExpression) {
Assert.notNull(requestDestinationExpression, "'requestDestinationExpression' must not be null");
this.requestDestinationExpressionProcessor = new ExpressionEvaluatingMessageProcessor<Object>(requestDestinationExpression);
this.requestDestinationExpressionProcessor =
new ExpressionEvaluatingMessageProcessor<Object>(requestDestinationExpression);
}
/**
@@ -409,7 +415,7 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
}
/**
* @param replyContainerProperties the replyContainerproperties to set
* @param replyContainerProperties the replyContainerProperties to set
*/
public void setReplyContainerProperties(ReplyContainerProperties replyContainerProperties) {
this.replyContainerProperties = replyContainerProperties;
@@ -473,7 +479,8 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
return this.resolveRequestDestination((String) result, session);
}
throw new MessageDeliveryException(message,
"Evaluation of requestDestinationExpression failed to produce a Destination or destination name. Result was: " + result);
"Evaluation of requestDestinationExpression failed " +
"to produce a Destination or destination name. Result was: " + result);
}
throw new MessageDeliveryException(message,
"No requestDestination, requestDestinationName, or requestDestinationExpression has been configured.");
@@ -502,7 +509,8 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
return this.resolveReplyDestination((String) result, session);
}
throw new MessageDeliveryException(message,
"Evaluation of replyDestinationExpression failed to produce a Destination or destination name. Result was: " + result);
"Evaluation of replyDestinationExpression failed to produce a Destination or destination name. " +
"Result was: " + result);
}
return session.createTemporaryQueue();
}
@@ -524,7 +532,8 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
Assert.isTrue(this.requestDestination != null
^ this.requestDestinationName != null
^ this.requestDestinationExpressionProcessor != null,
"Exactly one of 'requestDestination', 'requestDestinationName', or 'requestDestinationExpression' is required.");
"Exactly one of 'requestDestination', 'requestDestinationName', " +
"or 'requestDestinationExpression' is required.");
if (this.requestDestinationExpressionProcessor != null) {
this.requestDestinationExpressionProcessor.setBeanFactory(getBeanFactory());
this.requestDestinationExpressionProcessor.setConversionService(getConversionService());
@@ -541,7 +550,8 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
(this.replyDestination != null || this.replyDestinationName != null) ||
this.replyDestinationExpressionProcessor != null)) {
if (logger.isWarnEnabled()) {
logger.warn("The gateway cannot use a reply listener container with a specified destination(Name/Expression) " +
logger.warn("The gateway cannot use a reply listener container with a specified " +
"destination(Name/Expression) " +
"without a 'correlation-key'; " +
"a container will NOT be used; " +
"to avoid this problem, set the 'correlation-key' attribute; " +
@@ -560,6 +570,16 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
setContainerProperties(container);
container.afterPropertiesSet();
this.replyContainer = container;
if (isAsync() && this.correlationKey == null) {
logger.warn("'async=true' requires a correlationKey; ignored");
setAsync(false);
}
}
else {
if (isAsync()) {
logger.warn("'async=true' is ignored when a reply container is not being used");
setAsync(false);
}
}
this.initialized = true;
}
@@ -611,7 +631,8 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
container.setRecoveryInterval(this.replyContainerProperties.getRecoveryInterval());
}
if (StringUtils.hasText(this.replyContainerProperties.getSessionAcknowledgeModeName())) {
Integer acknowledgeMode = JmsAdapterUtils.parseAcknowledgeMode(this.replyContainerProperties.getSessionAcknowledgeModeName());
Integer acknowledgeMode = JmsAdapterUtils.parseAcknowledgeMode(
this.replyContainerProperties.getSessionAcknowledgeModeName());
if (acknowledgeMode != null) {
if (JmsAdapterUtils.SESSION_TRANSACTED == acknowledgeMode) {
container.setSessionTransacted(true);
@@ -659,7 +680,7 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
else {
Assert.state(taskScheduler != null, "'taskScheduler' is required.");
}
if (this.receiveTimeout >= 0) {
if (!isAsync() && this.receiveTimeout >= 0) {
Assert.state(taskScheduler != null, "'taskScheduler' is required.");
this.reaper = taskScheduler.schedule(new LateReplyReaper(), new Date());
}
@@ -675,7 +696,9 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
if (this.replyContainer != null) {
this.replyContainer.stop();
this.deleteDestinationIfTemporary(this.replyContainer.getDestination());
this.reaper.cancel(false);
if (this.reaper != null) {
this.reaper.cancel(false);
}
}
if (this.idleTask != null) {
this.idleTask.cancel(true);
@@ -691,15 +714,14 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
}
@Override
protected Object handleRequestMessage(final Message<?> message) {
protected Object handleRequestMessage(final Message<?> requestMessage) {
if (!this.initialized) {
this.afterPropertiesSet();
afterPropertiesSet();
}
final Message<?> requestMessage = this.getMessageBuilderFactory().fromMessage(message).build();
try {
javax.jms.Message jmsReply;
Object reply;
if (this.replyContainer == null) {
jmsReply = this.sendAndReceiveWithoutContainer(requestMessage);
reply = sendAndReceiveWithoutContainer(requestMessage);
}
else {
if (this.idleReplyContainerTimeout > 0) {
@@ -715,45 +737,53 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
}
}
}
jmsReply = this.sendAndReceiveWithContainer(requestMessage);
reply = this.sendAndReceiveWithContainer(requestMessage);
}
if (jmsReply == null) {
if (reply == null) {
if (this.requiresReply) {
throw new MessageTimeoutException(message,
throw new MessageTimeoutException(requestMessage,
"failed to receive JMS response within timeout of: " + this.receiveTimeout + "ms");
}
else {
return null;
}
}
Object result = jmsReply;
if (this.extractReplyPayload) {
result = this.messageConverter.fromMessage(jmsReply);
if (logger.isDebugEnabled()) {
logger.debug("converted JMS Message [" + jmsReply + "] to integration Message payload [" + result + "]");
}
}
Map<String, Object> jmsReplyHeaders = this.headerMapper.toHeaders(jmsReply);
if (this.replyContainer != null && this.correlationKey != null) {
// do not propagate back the gateway's internal correlation id
jmsReplyHeaders.remove(this.correlationKey);
}
Message<?> replyMessage = null;
if (result instanceof Message){
replyMessage = this.getMessageBuilderFactory().fromMessage((Message<?>) result).copyHeaders(jmsReplyHeaders).build();
if (reply instanceof javax.jms.Message) {
return buildReply((javax.jms.Message) reply);
}
else {
replyMessage = this.getMessageBuilderFactory().withPayload(result).copyHeaders(jmsReplyHeaders).build();
return reply;
}
return replyMessage;
}
catch (JMSException e) {
throw new MessageHandlingException(requestMessage, e);
}
}
private javax.jms.Message sendAndReceiveWithContainer(Message<?> requestMessage) throws JMSException {
private AbstractIntegrationMessageBuilder<?> buildReply(javax.jms.Message jmsReply) throws JMSException {
Object result = jmsReply;
if (this.extractReplyPayload) {
result = this.messageConverter.fromMessage(jmsReply);
if (logger.isDebugEnabled()) {
logger.debug("converted JMS Message [" + jmsReply + "] to integration Message payload [" + result + "]");
}
}
Map<String, Object> jmsReplyHeaders = this.headerMapper.toHeaders(jmsReply);
if (this.replyContainer != null && this.correlationKey != null) {
// do not propagate back the gateway's internal correlation id
jmsReplyHeaders.remove(this.correlationKey);
}
if (result instanceof Message){
return getMessageBuilderFactory().fromMessage((Message<?>) result).copyHeaders(jmsReplyHeaders);
}
else {
return getMessageBuilderFactory().withPayload(result).copyHeaders(jmsReplyHeaders);
}
}
private Object sendAndReceiveWithContainer(Message<?> requestMessage) throws JMSException {
Connection connection = this.createConnection();
Session session = null;
Destination replyTo = this.replyContainer.getReplyDestination();
@@ -782,7 +812,7 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
}
Destination requestDestination = this.determineRequestDestination(requestMessage, session);
javax.jms.Message reply = null;
Object reply = null;
if (this.correlationKey == null) {
/*
* Remove any existing correlation id that was mapped from the inbound message
@@ -798,8 +828,8 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
* Remove the gateway's internal correlation Id to avoid conflicts with an upstream
* gateway.
*/
if (reply != null) {
reply.setJMSCorrelationID(null);
if (reply instanceof javax.jms.Message) {
((javax.jms.Message) reply).setJMSCorrelationID(null);
}
return reply;
}
@@ -840,13 +870,16 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
javax.jms.Message replyMessage = null;
Destination requestDestination = this.determineRequestDestination(requestMessage, session);
if (this.correlationKey != null) {
replyMessage = this.doSendAndReceiveWithGeneratedCorrelationId(requestDestination, jmsRequest, replyTo, session, priority);
replyMessage = doSendAndReceiveWithGeneratedCorrelationId(requestDestination, jmsRequest, replyTo,
session, priority);
}
else if (replyTo instanceof TemporaryQueue || replyTo instanceof TemporaryTopic) {
replyMessage = this.doSendAndReceiveWithTemporaryReplyToDestination(requestDestination, jmsRequest, replyTo, session, priority);
replyMessage = doSendAndReceiveWithTemporaryReplyToDestination(requestDestination, jmsRequest, replyTo,
session, priority);
}
else {
replyMessage = this.doSendAndReceiveWithMessageIdCorrelation(requestDestination, jmsRequest, replyTo, session, priority);
replyMessage = doSendAndReceiveWithMessageIdCorrelation(requestDestination, jmsRequest, replyTo,
session, priority);
}
return replyMessage;
}
@@ -858,7 +891,8 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
}
/**
* Creates the MessageConsumer before sending the request Message since we are generating our own correlationId value for the MessageSelector.
* Creates the MessageConsumer before sending the request Message since we are generating
* our own correlationId value for the MessageSelector.
*/
private javax.jms.Message doSendAndReceiveWithGeneratedCorrelationId(Destination requestDestination,
javax.jms.Message jmsRequest, Destination replyTo, Session session, int priority) throws JMSException {
@@ -911,7 +945,8 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
}
/**
* Creates the MessageConsumer after sending the request Message since we need the MessageID for correlation with a MessageSelector.
* Creates the MessageConsumer after sending the request Message since we need
* the MessageID for correlation with a MessageSelector.
*/
private javax.jms.Message doSendAndReceiveWithMessageIdCorrelation(Destination requestDestination,
javax.jms.Message jmsRequest, Destination replyTo, Session session, int priority) throws JMSException {
@@ -1009,7 +1044,8 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
}
}
private javax.jms.Message doSendAndReceiveAsync(Destination requestDestination, javax.jms.Message jmsRequest, Session session, int priority) throws JMSException {
private Object doSendAndReceiveAsync(Destination requestDestination, javax.jms.Message jmsRequest, Session session,
int priority) throws JMSException {
String correlationId = null;
MessageProducer messageProducer = null;
try {
@@ -1026,19 +1062,32 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
*/
jmsRequest.setJMSCorrelationID(null);
}
LinkedBlockingQueue<javax.jms.Message> replyQueue = new LinkedBlockingQueue<javax.jms.Message>(1);
LinkedBlockingQueue<javax.jms.Message> replyQueue = null;
if (logger.isDebugEnabled()) {
logger.debug(this.getComponentName() + " Sending message with correlationId " + correlationId);
}
this.replies.put(correlationId, replyQueue);
SettableListenableFuture<AbstractIntegrationMessageBuilder<?>> future = null;
boolean async = isAsync();
if (!async) {
replyQueue = new LinkedBlockingQueue<javax.jms.Message>(1);
this.replies.put(correlationId, replyQueue);
}
else {
future = createFuture(correlationId);
}
this.sendRequestMessage(jmsRequest, messageProducer, priority);
return obtainReplyFromContainer(correlationId, replyQueue);
if (async) {
return future;
}
else {
return obtainReplyFromContainer(correlationId, replyQueue);
}
}
finally {
JmsUtils.closeMessageProducer(messageProducer);
if (correlationId != null) {
if (correlationId != null && !isAsync()) {
this.replies.remove(correlationId);
}
}
@@ -1103,16 +1152,58 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
}
if (logger.isDebugEnabled()) {
if (reply == null) {
logger.debug(this.getComponentName() + " Timed out waiting for reply with CorrelationId " + correlationId);
if (logger.isDebugEnabled()) {
logger.debug(this.getComponentName() + " Timed out waiting for reply with CorrelationId "
+ correlationId);
}
}
else {
logger.debug(this.getComponentName() + " Obtained reply with CorrelationId " + correlationId);
if (logger.isDebugEnabled()) {
logger.debug(this.getComponentName() + " Obtained reply with CorrelationId " + correlationId);
}
}
}
return reply;
}
private void sendRequestMessage(javax.jms.Message jmsRequest, MessageProducer messageProducer, int priority) throws JMSException {
private SettableListenableFuture<AbstractIntegrationMessageBuilder<?>> createFuture(final String correlationId) {
SettableListenableFuture<AbstractIntegrationMessageBuilder<?>> future =
new SettableListenableFuture<AbstractIntegrationMessageBuilder<?>>();
this.futures.put(correlationId, future);
if (this.receiveTimeout > 0) {
getTaskScheduler().schedule(new Runnable() {
@Override
public void run() {
expire(correlationId);
}
}, new Date(System.currentTimeMillis() + this.receiveTimeout));
}
return future;
}
private void expire(String correlationId) {
final SettableListenableFuture<AbstractIntegrationMessageBuilder<?>> future = this.futures.remove(correlationId);
if (future != null) {
try {
if (getRequiresReply()) {
future.setException(new JmsTimeoutException("No reply in " + this.receiveTimeout + " ms"));
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Reply expired and reply not required for " + correlationId);
}
}
}
catch (Exception e) {
logger.error("Exception while expiring future");
}
}
}
private void sendRequestMessage(javax.jms.Message jmsRequest, MessageProducer messageProducer, int priority)
throws JMSException {
if (this.explicitQosEnabled) {
messageProducer.send(jmsRequest, this.deliveryMode, priority, this.timeToLive);
}
@@ -1181,6 +1272,33 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
correlationId = message.getStringProperty(this.correlationKey);
}
Assert.state(correlationId != null, "Message with no correlationId received");
if (isAsync()) {
onMessageAsync(message, correlationId);
}
else {
onMessageSync(message, correlationId);
}
}
catch (Exception e) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to consume reply with correlationId " + correlationId, e);
}
}
}
private void onMessageAsync(javax.jms.Message message, String correlationId) throws Exception {
SettableListenableFuture<AbstractIntegrationMessageBuilder<?>> future = this.futures.remove(correlationId);
if (future != null) {
message.setJMSCorrelationID(null);
future.set(buildReply(message));
}
else {
logger.warn("Late reply for " + correlationId);
}
}
private void onMessageSync(javax.jms.Message message, String correlationId) {
try {
LinkedBlockingQueue<javax.jms.Message> queue = this.replies.get(correlationId);
if (queue == null) {
if (this.correlationKey != null) {
@@ -1330,7 +1448,8 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
if (logger.isTraceEnabled()) {
logger.trace("Running late reply reaper");
}
Iterator<Entry<String, TimedReply>> lateReplyIterator = JmsOutboundGateway.this.earlyOrLateReplies.entrySet().iterator();
Iterator<Entry<String, TimedReply>> lateReplyIterator =
JmsOutboundGateway.this.earlyOrLateReplies.entrySet().iterator();
long now = System.currentTimeMillis();
long expired = now - (JmsOutboundGateway.this.receiveTimeout * 2);
while (lateReplyIterator.hasNext()) {
@@ -1355,7 +1474,8 @@ public class JmsOutboundGateway extends AbstractReplyProducingMessageHandler imp
@Override
public void run() {
synchronized(JmsOutboundGateway.this.lifeCycleMonitor) {
if (System.currentTimeMillis() - JmsOutboundGateway.this.lastSend > JmsOutboundGateway.this.idleReplyContainerTimeout
if (System.currentTimeMillis() - JmsOutboundGateway.this.lastSend >
JmsOutboundGateway.this.idleReplyContainerTimeout
&& JmsOutboundGateway.this.replies.size() == 0) {
if (JmsOutboundGateway.this.replyContainer.isRunning()) {
if (logger.isDebugEnabled()) {

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2016 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.integration.jms;
import org.springframework.jms.JmsException;
/**
* A timeout occurred within an async gateway.
*
* @author Gary Russell
* @since 4.3
*
*/
@SuppressWarnings("serial")
public class JmsTimeoutException extends JmsException {
public JmsTimeoutException(String description) {
super(description);
}
}

View File

@@ -69,6 +69,7 @@ public class JmsOutboundGatewayParser extends AbstractConsumerEndpointParser {
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "requires-reply");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "idle-reply-listener-timeout",
"idleReplyContainerTimeout");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "async");
String deliveryPersistent = element.getAttribute("delivery-persistent");
if (StringUtils.hasText(deliveryPersistent)) {

View File

@@ -1128,6 +1128,16 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="async" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation>
When false (default), the requesting thread is suspended until a reply is received
or a timeout occurs; when true, the requesting thread is released and the reply
is returned on the listener container thread. Requires a reply-listener child
element and a 'correlation-key'; otherwise this property is ignored.
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attributeGroup ref="integration:smartLifeCycleAttributeGroup"/>
</xsd:complexType>
</xsd:element>

View File

@@ -86,6 +86,7 @@ public class JmsOutboundGatewayParserTests {
accessor = new DirectFieldAccessor(gateway);
int deliveryMode = (Integer)accessor.getPropertyValue("deliveryMode");
assertEquals(DeliveryMode.PERSISTENT, deliveryMode);
assertTrue(TestUtils.getPropertyValue(gateway, "async", Boolean.class));
DefaultMessageListenerContainer container = TestUtils.getPropertyValue(gateway, "replyContainer",
DefaultMessageListenerContainer.class);
assertEquals(4, TestUtils.getPropertyValue(container, "concurrentConsumers"));
@@ -116,6 +117,7 @@ public class JmsOutboundGatewayParserTests {
"jmsOutboundGatewayWithDeliveryPersistent.xml", this.getClass());
EventDrivenConsumer endpoint = (EventDrivenConsumer) context.getBean("advised");
JmsOutboundGateway gateway = TestUtils.getPropertyValue(endpoint, "handler", JmsOutboundGateway.class);
assertFalse(TestUtils.getPropertyValue(gateway, "async", Boolean.class));
gateway.handleMessage(new GenericMessage<String>("foo"));
assertEquals(1, adviceCalled);
assertEquals(3, TestUtils.getPropertyValue(gateway, "replyContainer.sessionAcknowledgeMode"));
@@ -287,13 +289,17 @@ public class JmsOutboundGatewayParserTests {
public interface SampleGateway{
String echo(String value);
}
public static class SampleService{
public String echo(String value){
return value.toUpperCase();
}
}
public static class FooAdvice extends AbstractRequestHandlerAdvice {
@@ -305,4 +311,5 @@ public class JmsOutboundGatewayParserTests {
}
}
}

View File

@@ -16,14 +16,14 @@
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/integration/jms
http://www.springframework.org/schema/integration/jms/spring-integration-jms.xsd">
<context:property-placeholder properties-ref="props"/>
<util:properties id="props">
<prop key="jmsAcknowledgeModeTransacted">transacted</prop>
<prop key="jmsAcknowledgeModeDupsOk">dups-ok</prop>
</util:properties>
<si:channel id="requestChannel"/>
<jms:outbound-gateway id="jmsGateway"
@@ -31,6 +31,8 @@
request-channel="requestChannel"
delivery-persistent="true"
idle-reply-listener-timeout="1234"
async="true"
correlation-key="JMSCorrelationID"
auto-startup="false">
<jms:reply-listener
acknowledge="${jmsAcknowledgeModeTransacted}"

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2016 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.integration.jms.request_reply;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.ActiveMQConnectionFactory;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.jms.JmsOutboundGateway;
import org.springframework.integration.jms.JmsTimeoutException;
import org.springframework.jms.connection.CachingConnectionFactory;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import org.springframework.jms.support.JmsHeaders;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Gary Russell
* @since 4.3
*
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext
public class AsyncGatewayTests {
@Autowired
private CachingConnectionFactory ccf;
@Autowired
private JmsOutboundGateway gateway1;
@Autowired
private JmsOutboundGateway gateway2;
@Test
public void testWithReply() throws Exception {
QueueChannel replies = new QueueChannel();
this.gateway1.setOutputChannel(replies);
this.gateway1.start();
this.gateway1.handleMessage(MessageBuilder.withPayload("foo")
.setHeader(JmsHeaders.CORRELATION_ID, "baz")// make sure it's restored in case we're from an upstream gw
.build());
JmsTemplate template = new JmsTemplate(this.ccf);
template.setReceiveTimeout(10000);
final Message received = template.receive("asyncTest1");
assertNotNull(received);
template.send(received.getJMSReplyTo(), new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
TextMessage textMessage = session.createTextMessage("bar");
textMessage.setJMSCorrelationID(received.getJMSCorrelationID());
return textMessage;
}
});
org.springframework.messaging.Message<?> reply = replies.receive(10000);
assertNotNull(reply);
assertEquals("bar", reply.getPayload());
assertEquals("baz", reply.getHeaders().get(JmsHeaders.CORRELATION_ID));
this.gateway1.stop();
}
@Test
public void testWithTimeout() throws Exception {
QueueChannel errors = new QueueChannel();
this.gateway2.setOutputChannel(errors);
this.gateway2.start();
this.gateway2.handleMessage(MessageBuilder.withPayload("foo").setErrorChannel(errors).build());
JmsTemplate template = new JmsTemplate(this.ccf);
final Message received = template.receive("asyncTest3");
assertNotNull(received);
org.springframework.messaging.Message<?> error = errors.receive(10000);
assertNotNull(error);
assertThat(error, instanceOf(ErrorMessage.class));
assertThat(error.getPayload(), instanceOf(MessagingException.class));
assertThat(((MessagingException) error.getPayload()).getCause(), instanceOf(JmsTimeoutException.class));
assertEquals("foo", ((MessagingException) error.getPayload()).getFailedMessage().getPayload());
this.gateway1.stop();
}
@Test
@DirtiesContext
public void testWithTimeoutNoReplyRequired() throws Exception {
QueueChannel errors = new QueueChannel();
this.gateway2.setOutputChannel(errors);
this.gateway2.setRequiresReply(false);
this.gateway2.start();
this.gateway2.handleMessage(MessageBuilder.withPayload("foo").setErrorChannel(errors).build());
JmsTemplate template = new JmsTemplate(this.ccf);
final Message received = template.receive("asyncTest3");
assertNotNull(received);
org.springframework.messaging.Message<?> error = errors.receive(1000);
assertNull(error);
this.gateway1.stop();
}
@Configuration
@EnableIntegration
public static class Config {
@Bean
public CachingConnectionFactory ccf() {
return new CachingConnectionFactory(
new ActiveMQConnectionFactory("vm://localhosti?broker.persistent=false"));
}
@Bean
public JmsOutboundGateway gateway1() {
JmsOutboundGateway gateway = new JmsOutboundGateway();
gateway.setUseReplyContainer(true);
gateway.setConnectionFactory(ccf());
gateway.setRequestDestinationName("asyncTest1");
gateway.setReplyDestinationName("asyncTest2");
gateway.setRequiresReply(true);
gateway.setReceiveTimeout(10000);
gateway.setAsync(true);
gateway.setCorrelationKey("JMSCorrelationID");
return gateway;
}
@Bean
public JmsOutboundGateway gateway2() {
JmsOutboundGateway gateway = new JmsOutboundGateway();
gateway.setUseReplyContainer(true);
gateway.setConnectionFactory(ccf());
gateway.setRequestDestinationName("asyncTest3");
gateway.setReplyDestinationName("asyncTest4");
gateway.setRequiresReply(true);
gateway.setReceiveTimeout(10);
gateway.setAsync(true);
gateway.setCorrelationKey("JMSCorrelationID");
return gateway;
}
}
}

View File

@@ -20,14 +20,18 @@ import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.io.StringReader;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.jivesoftware.smack.SmackException.NotConnectedException;
@@ -198,6 +202,17 @@ public class ChatMessageListeningEndpointTests {
Log logger = Mockito.spy(TestUtils.getPropertyValue(endpoint, "logger", Log.class));
given(logger.isInfoEnabled()).willReturn(true);
final CountDownLatch logLatch = new CountDownLatch(1);
willAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Object result = invocation.callRealMethod();
logLatch.countDown();
return result;
}
}).given(logger).info(anyString());
new DirectFieldAccessor(endpoint).setPropertyValue("logger", logger);
@@ -210,8 +225,9 @@ public class ChatMessageListeningEndpointTests {
ArgumentCaptor<String> argumentCaptor = new ArgumentCaptor<String>();
verify(logger).info(argumentCaptor.capture());
assertTrue(logLatch.await(10, TimeUnit.SECONDS));
verify(logger).info(argumentCaptor.capture());
assertEquals("The XMPP Message [" + smackMessage + "] with empty body is ignored.",
argumentCaptor.getValue());

View File

@@ -915,8 +915,10 @@ public class AmqpJavaApplication {
The gateway discussed in the previous section is synchronous, in that the sending thread is suspended until a
reply is received (or a timeout occurs).
Spring Integration _version 4.3_ added this asynchronous gateway, which uses the `AsyncRabbitTemplate` from Spring AMQP.
When a message is sent, the thread returns immediately and the reply is sent on the template's listener container
thread when it is received.
When a message is sent, the thread returns immediately after the send completes, and the reply is sent on the template's
listener container thread when it is received.
This can be useful when the gateway is invoked on a poller thread; the thread is released and is available for other
tasks in the framework.
Configuration for an AMQP Async Outbound Gateway is shown below.

View File

@@ -397,6 +397,23 @@ __NOT ALLOWED__
User-supplied correlation ids are not permitted with a reply listener; the gateway will not initialize with this
configuration.
[[jms-async-gateway]]
==== Async Gateway
Starting with _version 4.3_, you can now specify `async="true"` (or `setAsync(true)`) when configuring the outbound
gateway.
By default, when a request is sent to the gateway, the requesting thread is suspended until the reply is received and
the flow then continues on that thread.
If `async` is true, the requesting thread is released immediately after the send completes, and the reply is returned
(and the flow continues) on the listener container thread.
This can be useful when the gateway is invoked on a poller thread; the thread is released and is available for other
tasks within the framework.
`async` requires a `<reply-listener/>` (or `setUseReplyContainer(true)` when using Java configuration); it also
requires a `correlationKey` (usually `JMSCorrelationID`) to be specified.
If either of these conditions are not met, `async` is ignored.
[[jms-og-attributes]]
==== Attribute Reference
@@ -426,9 +443,10 @@ configuration.
request-destination-name="" <21>
request-pub-sub-domain="" <22>
time-to-live="" <23>
requires-reply=""> <24>
idle-reply-listener-timeout <25>
<int-jms:reply-listener /> <26>
requires-reply="" <24>
idle-reply-listener-timeout="" <25>
async=""> <26>
<int-jms:reply-listener /> <27>
</int-jms:outbound-gateway>
----
@@ -541,7 +559,9 @@ are outstanding).
The container will be started again on the next request.
The stop time is a minimum and may actually be up to 1.5x this value.
<26> When this element is included, replies are received by an asynchronous `MessageListenerContainer` rather than
<26> See <<jms-async-gateway>>.
<27> When this element is included, replies are received by an asynchronous `MessageListenerContainer` rather than
creating a consumer for each reply.
This can be more efficient in many cases.

View File

@@ -28,16 +28,6 @@ In order to provide a quick overview, all available attributes are listed in the
[cols="2,1,1,1,1,1,1", options="header"]
|===
| Attribute
@@ -59,12 +49,6 @@ In order to provide a quick overview, all available attributes are listed in the
| exception type router
| apply-sequence
@@ -76,12 +60,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| default-output-channel
@@ -93,12 +71,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| resolution-required
@@ -110,12 +82,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| ignore-send-failures
@@ -127,12 +93,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| timeout
@@ -144,12 +104,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| id
@@ -161,12 +115,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| auto-startup
@@ -178,12 +126,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| input-channel
@@ -195,12 +137,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| order
@@ -212,12 +148,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| method
@@ -229,12 +159,6 @@ a| image::images/tickmark.png[]
|
| ref
@@ -246,12 +170,6 @@ a| image::images/tickmark.png[]
|
| expression
@@ -263,12 +181,6 @@ a| image::images/tickmark.png[]
|
| header-name
@@ -280,12 +192,6 @@ a| image::images/tickmark.png[]
|
| evaluate-as-string
@@ -297,12 +203,6 @@ a| image::images/tickmark.png[]
|
| xpath-expression-ref
@@ -314,12 +214,6 @@ a| image::images/tickmark.png[]
|
| converter
@@ -331,10 +225,6 @@ a| image::images/tickmark.png[]
|
|===
.Routers Inside of a Chain
@@ -342,15 +232,6 @@ a| image::images/tickmark.png[]
|===
| Attribute
@@ -372,12 +253,6 @@ a| image::images/tickmark.png[]
| exception type router
| apply-sequence
@@ -389,12 +264,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| default-output-channel
@@ -406,12 +275,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| resolution-required
@@ -423,12 +286,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| ignore-send-failures
@@ -440,12 +297,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| timeout
@@ -457,12 +308,6 @@ a| image::images/tickmark.png[]
a| image::images/tickmark.png[]
| id
@@ -474,12 +319,6 @@ a| image::images/tickmark.png[]
|
| auto-startup
@@ -491,12 +330,6 @@ a| image::images/tickmark.png[]
|
| input-channel
@@ -508,12 +341,6 @@ a| image::images/tickmark.png[]
|
| order
@@ -525,12 +352,6 @@ a| image::images/tickmark.png[]
|
| method
@@ -542,12 +363,6 @@ a| image::images/tickmark.png[]
|
| ref
@@ -559,12 +374,6 @@ a| image::images/tickmark.png[]
|
| expression
@@ -576,12 +385,6 @@ a| image::images/tickmark.png[]
|
| header-name
@@ -593,12 +396,6 @@ a| image::images/tickmark.png[]
|
| evaluate-as-string
@@ -610,12 +407,6 @@ a| image::images/tickmark.png[]
|
| xpath-expression-ref
@@ -627,12 +418,6 @@ a| image::images/tickmark.png[]
|
| converter
@@ -644,10 +429,6 @@ a| image::images/tickmark.png[]
|
|===
[IMPORTANT]
@@ -1310,6 +1091,15 @@ This is to avoid the overhead of `EvaluationContext` creation for each `Expressi
It is a simple Java Bean with two properties - `Message<?> request` and `Object reply`.
With this expression implementation, we can specify Routing Slip `path` entries using SpEL (`@routingSlipRoutingPojo.get(request, reply)`, `request.headers[myRoutingSlipChannel]`) avoiding a bean definition for the `RoutingSlipRouteStrategy`.
NOTE: The `requestMessage` argument is always a `Message<?>`; depending on context, the reply object may be a
`Message<?>`, an `AbstractIntegrationMessageBuilder` or an arbitrary application domain object (if, for example,
it is returned by a POJO method invoked by a service activator).
In the first two cases, the usual "message" properties are available (`payload` and `headers`) when using SpEL (or
a Java implementation).
When an arbitrary domain object, these properties are, obviously, not available.
Care should be taken when using routing slips in conjunction with POJO methods if the result is used to determine the
next path.
IMPORTANT: If a _Routing Slip_ is involved in a distributed environment - cross-JVM application, `request-reply` through a Message Broker (e.g.
<<amqp>>, <<jms>>), or persistence `MessageStore` (<<message-store>>) is used in the integration flow, etc., - it is recommended to *not* use _inline_ expressions for the Routing Slip `path`.
The framework (`RoutingSlipHeaderValueMessageProcessor`) converts them to `ExpressionEvaluatingRoutingSlipRouteStrategy` objects and they are used in the `routingSlip` message header.

View File

@@ -114,7 +114,7 @@ The service activator is invoked by the calling thread; this would be some upstr
`SubscribableChannel`, or a poller thread for a `PollableChannel`.
If the service returns a `ListenableFuture<?>` the default action is to send that as the payload of the message sent
to the output (or reply) channel.
Starting with _version 4.3_, you can now set the `async` attribute to true (`setAsyncReplySupported(true)` when using
Starting with _version 4.3_, you can now set the `async` attribute to true (`setAsync(true)` when using
Java configuration).
If the service returns a `ListenableFuture<?>` when this is true, the calling thread is released immediately, and the
reply message is sent on the thread (from within your service) that completes the future.

View File

@@ -48,10 +48,17 @@ See <<imap-seen>> for more information.
==== JMS Changes
===== Header Mapper
The `DefaultJmsHeaderMapper` now maps the standard `correlationId` header as a message property by invoking its
`toString()` method.
See <<jms-header-mapping>> for more information.
===== Async Gateway
The JMS Outbound gateway now has an `async` property.
See <<jms-async-gateway>> for more information.
==== Aggregator Changes
There is a change in behavior when a POJO aggregator releases a collection of `Message<?>` objects; this is rare but if