INT-3506 Async Gateway Improvements
JIRA: https://jira.spring.io/browse/INT-3506 JIRA: https://jira.spring.io/browse/INT-3428 Support flows downstream of the gateway that support returning a `Future<?>` payload. Currently, any method that returns a type that is assignable to `Future<?>` runs async and returns a `FutureTask<?>`. This prevents a service-interface method that returns a custom `Future<?>` object from being invoked without wrapping that `Future<?>` in a `FutureTask<?>`. Allow the async-executor to be set to `null` causing any method returning `Future<?>` to run on the calling thread. Add support for `ListenableFuture<?>`. If the return type is a `RunnableFuture`, `ListenableFuture` or `Future`, or exactly a `FutureTask` or `ListenableFutureTask`, run the flow on the executor (if present); otherwise run on the calling thread. INT-3506 Fix Object returnType INT-3506 Polishing - PR Comments Perform dummy invocations of `submit` and `submitListenable` to determine the actual return types so that we can determine at runtime whether the executor will return a type that is compatible with the method return type. If not, run on the caller's thread. Add DEBUG Log If Incompatible Future<?> INT-3506 Add Support for MessagingGateway Add a constant to indicate no executor. Add tests. INT-3506 Polishing and Docs - Docbook - XSD - Change test to send calling thread in payload so we can determine whether we need to return a Future or not; previously relied on the thread name which was brittle. Polishing JavaDocs. Change `amqp.xml` to use `org.springframework.amqp.support.AmqpHeaders` instead of an old one.
This commit is contained in:
committed by
Artem Bilan
parent
e4eac27b12
commit
b64bf03fce
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2014 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.annotation;
|
||||
|
||||
/**
|
||||
* Common value constants for annotation attributes.
|
||||
*
|
||||
* @author Gary Russell
|
||||
* @since 4.1
|
||||
*
|
||||
*/
|
||||
public final class AnnotationConstants {
|
||||
|
||||
/**
|
||||
* Constant defining a value as a replacement for {@code null} which
|
||||
* we cannot use in annotation attributes.
|
||||
*/
|
||||
public static final String NULL = "__NULL__";
|
||||
|
||||
private AnnotationConstants() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -83,6 +83,8 @@ public @interface MessagingGateway {
|
||||
* to use for any of the interface methods that have a {@link java.util.concurrent.Future} return type.
|
||||
* This {@code Executor} will only be used for those async methods; the sync methods
|
||||
* will be invoked in the caller's thread.
|
||||
* Use {@link AnnotationConstants#NULL} to specify no async executor - for example
|
||||
* if your downstream flow returns a {@link java.util.concurrent.Future}.
|
||||
* @return the suggested executor bean name, if any
|
||||
*/
|
||||
String asyncExecutor() default "";
|
||||
|
||||
@@ -37,10 +37,11 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.expression.common.LiteralExpression;
|
||||
import org.springframework.integration.annotation.AnnotationConstants;
|
||||
import org.springframework.integration.annotation.MessagingGateway;
|
||||
import org.springframework.integration.util.MessagingAnnotationUtils;
|
||||
import org.springframework.integration.gateway.GatewayMethodMetadata;
|
||||
import org.springframework.integration.gateway.GatewayProxyFactoryBean;
|
||||
import org.springframework.integration.util.MessagingAnnotationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
@@ -133,7 +134,10 @@ public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar
|
||||
if (StringUtils.hasText(errorChannel)) {
|
||||
gatewayProxyBuilder.addPropertyReference("errorChannel", errorChannel);
|
||||
}
|
||||
if (StringUtils.hasText(asyncExecutor)) {
|
||||
if (asyncExecutor == null || AnnotationConstants.NULL.equals(asyncExecutor)) {
|
||||
gatewayProxyBuilder.addPropertyValue("asyncExecutor", null);
|
||||
}
|
||||
else if (StringUtils.hasText(asyncExecutor)) {
|
||||
gatewayProxyBuilder.addPropertyReference("asyncExecutor", asyncExecutor);
|
||||
}
|
||||
if (StringUtils.hasText(reactorEnvironment)) {
|
||||
|
||||
@@ -58,14 +58,26 @@ public class GatewayParser implements BeanDefinitionParser {
|
||||
final Map<String, Object> gatewayAttributes = new HashMap<String, Object>();
|
||||
gatewayAttributes.put("name", element.getAttribute(AbstractBeanDefinitionParser.ID_ATTRIBUTE));
|
||||
gatewayAttributes.put("defaultPayloadExpression", element.getAttribute("default-payload-expression"));
|
||||
gatewayAttributes.put("defaultRequestChannel", element.getAttribute(isNested ? "request-channel" : "default-request-channel"));
|
||||
gatewayAttributes.put("defaultReplyChannel", element.getAttribute(isNested ? "reply-channel" : "default-reply-channel"));
|
||||
gatewayAttributes.put("defaultRequestChannel",
|
||||
element.getAttribute(isNested ? "request-channel" : "default-request-channel"));
|
||||
gatewayAttributes.put("defaultReplyChannel",
|
||||
element.getAttribute(isNested ? "reply-channel" : "default-reply-channel"));
|
||||
gatewayAttributes.put("errorChannel", element.getAttribute("error-channel"));
|
||||
gatewayAttributes.put("asyncExecutor", element.getAttribute("async-executor"));
|
||||
|
||||
String asyncExecutor = element.getAttribute("async-executor");
|
||||
if (!element.hasAttribute("async-executor") || StringUtils.hasLength(asyncExecutor)) {
|
||||
gatewayAttributes.put("asyncExecutor", asyncExecutor);
|
||||
}
|
||||
else {
|
||||
gatewayAttributes.put("asyncExecutor", null);
|
||||
}
|
||||
|
||||
gatewayAttributes.put("mapper", element.getAttribute("mapper"));
|
||||
gatewayAttributes.put("reactorEnvironment", element.getAttribute("reactor-environment"));
|
||||
gatewayAttributes.put("defaultReplyTimeout", element.getAttribute(isNested ? "reply-timeout" : "default-reply-timeout"));
|
||||
gatewayAttributes.put("defaultRequestTimeout", element.getAttribute(isNested ? "request-timeout" : "default-request-timeout"));
|
||||
gatewayAttributes.put("defaultReplyTimeout",
|
||||
element.getAttribute(isNested ? "reply-timeout" : "default-reply-timeout"));
|
||||
gatewayAttributes.put("defaultRequestTimeout",
|
||||
element.getAttribute(isNested ? "request-timeout" : "default-request-timeout"));
|
||||
|
||||
|
||||
List<Element> headerElements = DomUtils.getChildElementsByTagName(element, "default-header");
|
||||
@@ -88,7 +100,8 @@ public class GatewayParser implements BeanDefinitionParser {
|
||||
String methodName = methodElement.getAttribute("name");
|
||||
BeanDefinitionBuilder methodMetadataBuilder = BeanDefinitionBuilder.genericBeanDefinition(
|
||||
GatewayMethodMetadata.class);
|
||||
methodMetadataBuilder.addPropertyValue("requestChannelName", methodElement.getAttribute("request-channel"));
|
||||
methodMetadataBuilder.addPropertyValue("requestChannelName",
|
||||
methodElement.getAttribute("request-channel"));
|
||||
methodMetadataBuilder.addPropertyValue("replyChannelName", methodElement.getAttribute("reply-channel"));
|
||||
methodMetadataBuilder.addPropertyValue("requestTimeout", methodElement.getAttribute("request-timeout"));
|
||||
methodMetadataBuilder.addPropertyValue("replyTimeout", methodElement.getAttribute("reply-timeout"));
|
||||
@@ -97,7 +110,8 @@ public class GatewayParser implements BeanDefinitionParser {
|
||||
Assert.state(!hasMapper || !StringUtils.hasText(element.getAttribute("payload-expression")),
|
||||
"'payload-expression' is not allowed when a 'mapper' is provided");
|
||||
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(methodMetadataBuilder, methodElement, "payload-expression");
|
||||
IntegrationNamespaceUtils.setValueIfAttributeDefined(methodMetadataBuilder, methodElement,
|
||||
"payload-expression");
|
||||
|
||||
List<Element> invocationHeaders = DomUtils.getChildElementsByTagName(methodElement, "header");
|
||||
if (!CollectionUtils.isEmpty(invocationHeaders)) {
|
||||
@@ -106,7 +120,8 @@ public class GatewayParser implements BeanDefinitionParser {
|
||||
Map<String, Object> headerExpressions = new ManagedMap<String, Object>();
|
||||
for (Element headerElement : invocationHeaders) {
|
||||
BeanDefinition expressionDef = IntegrationNamespaceUtils
|
||||
.createExpressionDefinitionFromValueOrExpression("value", "expression", parserContext, headerElement, true);
|
||||
.createExpressionDefinitionFromValueOrExpression("value", "expression", parserContext,
|
||||
headerElement, true);
|
||||
|
||||
headerExpressions.put(headerElement.getAttribute("name"), expressionDef);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ import java.util.concurrent.Future;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import reactor.core.Environment;
|
||||
import reactor.core.composable.Promise;
|
||||
import reactor.core.composable.spec.Promises;
|
||||
import reactor.function.Functions;
|
||||
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.SimpleTypeConverter;
|
||||
@@ -38,6 +43,7 @@ import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
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;
|
||||
@@ -62,11 +68,6 @@ import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import reactor.core.Environment;
|
||||
import reactor.core.composable.Promise;
|
||||
import reactor.core.composable.spec.Promises;
|
||||
import reactor.function.Functions;
|
||||
|
||||
/**
|
||||
* Generates a proxy for the provided service interface to enable interaction
|
||||
* with messaging components without application code being aware of them allowing
|
||||
@@ -111,6 +112,10 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
|
||||
|
||||
private volatile AsyncTaskExecutor asyncExecutor = new SimpleAsyncTaskExecutor();
|
||||
|
||||
private volatile Class<?> asyncSubmitType;
|
||||
|
||||
private volatile Class<?> asyncSubmitListenableType;
|
||||
|
||||
private volatile Environment reactorEnvironment;
|
||||
|
||||
private volatile boolean initialized;
|
||||
@@ -214,9 +219,19 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the executor for use when the gateway method returns
|
||||
* {@link java.util.concurrent.Future} or {@link org.springframework.util.concurrent.ListenableFuture}.
|
||||
* Set it to null to disable the async processing, and any
|
||||
* {@link java.util.concurrent.Future} return types must be returned by the downstream flow.
|
||||
* @param executor The executor.
|
||||
*/
|
||||
public void setAsyncExecutor(Executor executor) {
|
||||
Assert.notNull(executor, "executor must not be null");
|
||||
this.asyncExecutor = (executor instanceof AsyncTaskExecutor) ? (AsyncTaskExecutor) executor
|
||||
if (executor == null && logger.isInfoEnabled()) {
|
||||
logger.info("A null executor disables the async gateway; " +
|
||||
"methods returning Future<?> will run on the calling thread");
|
||||
}
|
||||
this.asyncExecutor = (executor instanceof AsyncTaskExecutor || executor == null) ? (AsyncTaskExecutor) executor
|
||||
: new TaskExecutorAdapter(executor);
|
||||
}
|
||||
|
||||
@@ -275,6 +290,21 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
|
||||
this.gatewayMap.put(method, gateway);
|
||||
}
|
||||
this.serviceProxy = new ProxyFactory(proxyInterface, this).getProxy(this.beanClassLoader);
|
||||
if (this.asyncExecutor != null) {
|
||||
Callable<String> task = new Callable<String>() {
|
||||
|
||||
@Override
|
||||
public String call() throws Exception {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
Future<String> submitType = this.asyncExecutor.submit(task);
|
||||
this.asyncSubmitType = submitType.getClass();
|
||||
if (this.asyncExecutor instanceof AsyncListenableTaskExecutor) {
|
||||
submitType = ((AsyncListenableTaskExecutor) this.asyncExecutor).submitListenable(task);
|
||||
this.asyncSubmitListenableType = submitType.getClass();
|
||||
}
|
||||
}
|
||||
this.start();
|
||||
this.initialized = true;
|
||||
}
|
||||
@@ -309,8 +339,20 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
|
||||
@Override
|
||||
public Object invoke(final MethodInvocation invocation) throws Throwable {
|
||||
final Class<?> returnType = invocation.getMethod().getReturnType();
|
||||
if (Future.class.isAssignableFrom(returnType)) {
|
||||
return this.asyncExecutor.submit(new AsyncInvocationTask(invocation));
|
||||
if (this.asyncExecutor != null && !Object.class.equals(returnType)) {
|
||||
if (returnType.isAssignableFrom(this.asyncSubmitType)) {
|
||||
return this.asyncExecutor.submit(new AsyncInvocationTask(invocation));
|
||||
}
|
||||
else if (returnType.isAssignableFrom(asyncSubmitListenableType)) {
|
||||
return ((AsyncListenableTaskExecutor) this.asyncExecutor).submitListenable(new AsyncInvocationTask(invocation));
|
||||
}
|
||||
else if (Future.class.isAssignableFrom(returnType)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("AsyncTaskExecutor submit*() return types are incompatible with the method return type; "
|
||||
+ "running on calling thread; the downstream flow must return the required Future: "
|
||||
+ returnType.getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Promise.class.isAssignableFrom(returnType)) {
|
||||
if (this.reactorEnvironment == null) {
|
||||
@@ -442,9 +484,12 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
|
||||
boolean hasValue = StringUtils.hasText(value);
|
||||
|
||||
if (!(hasValue ^ StringUtils.hasText(expression))) {
|
||||
throw new BeanDefinitionStoreException("exactly one of 'value' or 'expression' is required on a gateway's header.");
|
||||
throw new BeanDefinitionStoreException("exactly one of 'value' or 'expression' " +
|
||||
"is required on a gateway's header.");
|
||||
}
|
||||
headerExpressions.put(name, hasValue ? new LiteralExpression(value): PARSER.parseExpression(expression));
|
||||
headerExpressions.put(name, hasValue
|
||||
? new LiteralExpression(value)
|
||||
: PARSER.parseExpression(expression));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,7 +521,8 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
GatewayMethodInboundMessageMapper messageMapper = new GatewayMethodInboundMessageMapper(method, headerExpressions,
|
||||
GatewayMethodInboundMessageMapper messageMapper = new GatewayMethodInboundMessageMapper(method,
|
||||
headerExpressions,
|
||||
this.globalMethodMetadata != null ? this.globalMethodMetadata.getHeaderExpressions() : null,
|
||||
this.argsMapper, this.getMessageBuilderFactory());
|
||||
if (StringUtils.hasText(payloadExpression)) {
|
||||
@@ -506,9 +552,7 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
|
||||
if (this.getBeanFactory() != null) {
|
||||
gateway.setBeanFactory(this.getBeanFactory());
|
||||
}
|
||||
if (this.shouldTrack) {
|
||||
gateway.setShouldTrack(this.shouldTrack);
|
||||
}
|
||||
gateway.setShouldTrack(this.shouldTrack);
|
||||
gateway.afterPropertiesSet();
|
||||
return gateway;
|
||||
}
|
||||
@@ -577,6 +621,7 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
|
||||
private MethodInvocationGateway(GatewayMethodInboundMessageMapper messageMapper) {
|
||||
this.setRequestMapper(messageMapper);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -600,6 +645,7 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint
|
||||
throw new MessagingException("asynchronous gateway invocation failed", t);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -755,9 +755,17 @@
|
||||
<xsd:documentation>
|
||||
<![CDATA[
|
||||
Provide a reference to an implementation of java.util.concurrent.Executor
|
||||
to use for any of the interface methods that have a Future return type.
|
||||
to use for any of the interface methods that have a Future or
|
||||
ListenableFuture return type.
|
||||
This Executor will only be used for those async methods; the sync methods
|
||||
will be invoked in the caller's thread.
|
||||
will be invoked on the caller's thread. By default, a SimpleAsyncTaskExecutor
|
||||
is used. Most executors return a FutureTask or FutureListenableTask respectively
|
||||
but a custom executor can return any Future. If the method return type is
|
||||
not compatible with the executor the flow will run on the caller's thread and
|
||||
the flow must return an appropriate Future. Finally, you can disable the
|
||||
gateway ansync handling by setting this attribute to "". This allows the downstream
|
||||
flow to return a Future that would otherwise have been compatible with the
|
||||
default executor.
|
||||
]]>
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
|
||||
@@ -36,6 +36,12 @@
|
||||
default-reply-channel="replyChannel"
|
||||
async-executor="testExecutor"/>
|
||||
|
||||
<gateway id="asyncOff"
|
||||
service-interface="org.springframework.integration.gateway.TestService"
|
||||
default-request-channel="requestChannel"
|
||||
default-reply-channel="replyChannel"
|
||||
async-executor=""/>
|
||||
|
||||
<gateway id="promise"
|
||||
service-interface="org.springframework.integration.gateway.TestService"
|
||||
default-request-channel="requestChannel"
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package org.springframework.integration.config.xml;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -32,6 +34,7 @@ import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.integration.gateway.TestService;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
@@ -93,6 +96,13 @@ public class GatewayParserTests {
|
||||
Message<?> reply = result.get(1, TimeUnit.SECONDS);
|
||||
assertEquals("foo", reply.getPayload());
|
||||
assertEquals("testExecutor", reply.getHeaders().get("executor"));
|
||||
assertNotNull(TestUtils.getPropertyValue(context.getBean("&async"), "asyncExecutor"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncDisabledGateway() throws Exception {
|
||||
Object service = context.getBean("&asyncOff");
|
||||
assertNull(TestUtils.getPropertyValue(service, "asyncExecutor"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -104,10 +114,12 @@ public class GatewayParserTests {
|
||||
Promise<Message<?>> result = service.promise("foo");
|
||||
Message<?> reply = result.await(1, TimeUnit.SECONDS);
|
||||
assertEquals("foo", reply.getPayload());
|
||||
assertNotNull(TestUtils.getPropertyValue(context.getBean("&promise"), "asyncExecutor"));
|
||||
}
|
||||
|
||||
private void startResponder(final PollableChannel requestChannel, final MessageChannel replyChannel) {
|
||||
Executors.newSingleThreadExecutor().execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Message<?> request = requestChannel.receive();
|
||||
Message<?> reply = MessageBuilder.fromMessage(request)
|
||||
@@ -125,6 +137,7 @@ public class GatewayParserTests {
|
||||
|
||||
private volatile String beanName;
|
||||
|
||||
@Override
|
||||
public void setBeanName(String beanName) {
|
||||
this.beanName = beanName;
|
||||
}
|
||||
@@ -135,8 +148,15 @@ public class GatewayParserTests {
|
||||
try {
|
||||
Future<?> result = super.submit(task);
|
||||
Message<?> message = (Message<?>) result.get(1, TimeUnit.SECONDS);
|
||||
Message<?> modifiedMessage = MessageBuilder.fromMessage(message)
|
||||
Message<?> modifiedMessage;
|
||||
if (message == null) {
|
||||
modifiedMessage = MessageBuilder.withPayload("foo")
|
||||
.setHeader("executor", this.beanName).build();
|
||||
}
|
||||
else {
|
||||
modifiedMessage = MessageBuilder.fromMessage(message)
|
||||
.setHeader("executor", this.beanName).build();
|
||||
}
|
||||
return new AsyncResult(modifiedMessage);
|
||||
}
|
||||
catch (Exception e) {
|
||||
|
||||
@@ -17,25 +17,33 @@
|
||||
package org.springframework.integration.gateway;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.integration.annotation.Gateway;
|
||||
import org.springframework.integration.annotation.GatewayHeader;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.messaging.support.ChannelInterceptorAdapter;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.concurrent.ListenableFuture;
|
||||
import org.springframework.util.concurrent.ListenableFutureCallback;
|
||||
|
||||
import reactor.core.Environment;
|
||||
import reactor.core.composable.Promise;
|
||||
@@ -79,6 +87,97 @@ public class AsyncGatewayTests {
|
||||
assertEquals("foobar", ((Message<?>) result).getPayload());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listenableFutureWithMessageReturned() throws Exception {
|
||||
QueueChannel requestChannel = new QueueChannel();
|
||||
addThreadEnricher(requestChannel);
|
||||
startResponder(requestChannel);
|
||||
GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean();
|
||||
proxyFactory.setDefaultRequestChannel(requestChannel);
|
||||
proxyFactory.setServiceInterface(TestEchoService.class);
|
||||
proxyFactory.setBeanName("testGateway");
|
||||
proxyFactory.setBeanFactory(mock(BeanFactory.class));
|
||||
proxyFactory.afterPropertiesSet();
|
||||
TestEchoService service = (TestEchoService) proxyFactory.getObject();
|
||||
ListenableFuture<Message<?>> f = service.returnMessageListenable("foo");
|
||||
long start = System.currentTimeMillis();
|
||||
final AtomicReference<Message<?>> result = new AtomicReference<Message<?>>();
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
f.addCallback(new ListenableFutureCallback<Message<?>>() {
|
||||
|
||||
@Override
|
||||
public void onSuccess(Message<?> msg) {
|
||||
result.set(msg);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
}
|
||||
|
||||
});
|
||||
assertTrue(latch.await(10, TimeUnit.SECONDS));
|
||||
long elapsed = System.currentTimeMillis() - start;
|
||||
assertTrue(elapsed >= 200);
|
||||
assertEquals("foobar", result.get().getPayload());
|
||||
|
||||
Object thread = result.get().getHeaders().get("thread");
|
||||
assertNotEquals(Thread.currentThread(), thread);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customFutureReturned() throws Exception {
|
||||
QueueChannel requestChannel = new QueueChannel();
|
||||
addThreadEnricher(requestChannel);
|
||||
startResponder(requestChannel);
|
||||
GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean();
|
||||
proxyFactory.setDefaultRequestChannel(requestChannel);
|
||||
proxyFactory.setServiceInterface(TestEchoService.class);
|
||||
proxyFactory.setBeanName("testGateway");
|
||||
proxyFactory.setBeanFactory(mock(BeanFactory.class));
|
||||
proxyFactory.afterPropertiesSet();
|
||||
TestEchoService service = (TestEchoService) proxyFactory.getObject();
|
||||
CustomFuture f = service.returnCustomFuture("foo");
|
||||
String result = f.get(1000, TimeUnit.MILLISECONDS);
|
||||
assertEquals("foobar", result);
|
||||
|
||||
assertEquals(Thread.currentThread(), f.thread);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nonAsyncFutureReturned() throws Exception {
|
||||
QueueChannel requestChannel = new QueueChannel();
|
||||
addThreadEnricher(requestChannel);
|
||||
startResponder(requestChannel);
|
||||
GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean();
|
||||
proxyFactory.setDefaultRequestChannel(requestChannel);
|
||||
proxyFactory.setServiceInterface(TestEchoService.class);
|
||||
proxyFactory.setBeanName("testGateway");
|
||||
proxyFactory.setBeanFactory(mock(BeanFactory.class));
|
||||
|
||||
proxyFactory.setAsyncExecutor(null); // Not async - user flow returns Future<?>
|
||||
|
||||
proxyFactory.afterPropertiesSet();
|
||||
TestEchoService service = (TestEchoService) proxyFactory.getObject();
|
||||
CustomFuture f = (CustomFuture) service.returnCustomFutureWithTypeFuture("foo");
|
||||
String result = f.get(1000, TimeUnit.MILLISECONDS);
|
||||
assertEquals("foobar", result);
|
||||
|
||||
assertEquals(Thread.currentThread(), f.thread);
|
||||
}
|
||||
|
||||
protected void addThreadEnricher(QueueChannel requestChannel) {
|
||||
requestChannel.addInterceptor(new ChannelInterceptorAdapter() {
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
return MessageBuilder.fromMessage(message)
|
||||
.setHeader("thread", Thread.currentThread())
|
||||
.build();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void futureWithPayloadReturned() throws Exception {
|
||||
QueueChannel requestChannel = new QueueChannel();
|
||||
@@ -239,9 +338,13 @@ public class AsyncGatewayTests {
|
||||
|
||||
private static void startResponder(final PollableChannel requestChannel) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Message<?> input = requestChannel.receive();
|
||||
GenericMessage<String> reply = new GenericMessage<String>(input.getPayload() + "bar");
|
||||
String payload = input.getPayload() + "bar";
|
||||
Message<?> reply = MessageBuilder.withPayload(payload)
|
||||
.copyHeaders(input.getHeaders())
|
||||
.build();
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
}
|
||||
@@ -249,6 +352,13 @@ public class AsyncGatewayTests {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
String header = (String) input.getHeaders().get("method");
|
||||
if (header != null && header.startsWith("returnCustomFuture")) {
|
||||
reply = MessageBuilder.withPayload(new CustomFuture(payload,
|
||||
(Thread) input.getHeaders().get("thread")))
|
||||
.copyHeaders(input.getHeaders())
|
||||
.build();
|
||||
}
|
||||
((MessageChannel) input.getHeaders().getReplyChannel()).send(reply);
|
||||
}
|
||||
}).start();
|
||||
@@ -263,6 +373,14 @@ public class AsyncGatewayTests {
|
||||
|
||||
Future<?> returnSomething(String s);
|
||||
|
||||
ListenableFuture<Message<?>> returnMessageListenable(String s);
|
||||
|
||||
@Gateway(headers=@GatewayHeader(name="method", expression="#gatewayMethod.name"))
|
||||
CustomFuture returnCustomFuture(String s);
|
||||
|
||||
@Gateway(headers=@GatewayHeader(name="method", expression="#gatewayMethod.name"))
|
||||
Future<?> returnCustomFutureWithTypeFuture(String s);
|
||||
|
||||
Promise<String> returnStringPromise(String s);
|
||||
|
||||
Promise<Message<?>> returnMessagePromise(String s);
|
||||
@@ -271,4 +389,43 @@ public class AsyncGatewayTests {
|
||||
|
||||
}
|
||||
|
||||
private static class CustomFuture implements Future<String> {
|
||||
|
||||
private final String result;
|
||||
|
||||
private final Thread thread;
|
||||
|
||||
private CustomFuture(String result, Thread thread) {
|
||||
this.result = result;
|
||||
this.thread = thread;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel(boolean mayInterruptIfRunning) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get() throws InterruptedException, ExecutionException {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException,
|
||||
TimeoutException {
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@
|
||||
package org.springframework.integration.gateway;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
@@ -31,7 +33,11 @@ import static org.mockito.Mockito.verify;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Future;
|
||||
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;
|
||||
@@ -39,17 +45,19 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
import org.springframework.core.task.AsyncTaskExecutor;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.integration.annotation.AnnotationConstants;
|
||||
import org.springframework.integration.annotation.BridgeTo;
|
||||
import org.springframework.integration.annotation.Gateway;
|
||||
import org.springframework.messaging.handler.annotation.Header;
|
||||
import org.springframework.integration.annotation.IntegrationComponentScan;
|
||||
import org.springframework.integration.annotation.MessagingGateway;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
import org.springframework.integration.channel.DirectChannel;
|
||||
import org.springframework.integration.config.EnableIntegration;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
@@ -60,9 +68,15 @@ import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.MessagingException;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.handler.annotation.Header;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
import org.springframework.messaging.support.ChannelInterceptorAdapter;
|
||||
import org.springframework.scheduling.annotation.AsyncResult;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.util.concurrent.ListenableFuture;
|
||||
import org.springframework.util.concurrent.ListenableFutureCallback;
|
||||
|
||||
/**
|
||||
* @author Oleg Zhurakousky
|
||||
@@ -77,6 +91,24 @@ public class GatewayInterfaceTests {
|
||||
@Autowired
|
||||
private Int2634Gateway int2634Gateway;
|
||||
|
||||
@Autowired
|
||||
private ExecGateway execGateway;
|
||||
|
||||
@Autowired
|
||||
private NoExecGateway noExecGateway;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("&gatewayInterfaceTests$ExecGateway")
|
||||
private GatewayProxyFactoryBean execGatewayFB;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("&gatewayInterfaceTests$NoExecGateway")
|
||||
private GatewayProxyFactoryBean noExecGatewayFB;
|
||||
|
||||
@Autowired
|
||||
private SimpleAsyncTaskExecutor exec;
|
||||
|
||||
|
||||
@Test
|
||||
public void testWithServiceSuperclassAnnotatedMethod() throws Exception {
|
||||
ConfigurableApplicationContext ac = new ClassPathXmlApplicationContext("GatewayInterfaceTests-context.xml", this.getClass());
|
||||
@@ -326,6 +358,45 @@ public class GatewayInterfaceTests {
|
||||
assertEquals(param, result);
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests use current thread in payload and reply has the thread that actually
|
||||
* performed the send() on gatewayThreadChannel.
|
||||
*/
|
||||
@Test
|
||||
public void testExecs() throws Exception {
|
||||
assertSame(exec, TestUtils.getPropertyValue(execGatewayFB, "asyncExecutor"));
|
||||
assertNull(TestUtils.getPropertyValue(noExecGatewayFB, "asyncExecutor"));
|
||||
|
||||
Future<Thread> result = this.int2634Gateway.test3(Thread.currentThread());
|
||||
assertNotEquals(Thread.currentThread(), result.get());
|
||||
assertThat(result.get().getName(), startsWith("SimpleAsync"));
|
||||
|
||||
result = this.execGateway.test1(Thread.currentThread());
|
||||
assertNotEquals(Thread.currentThread(), result.get());
|
||||
assertThat(result.get().getName(), startsWith("exec-"));
|
||||
|
||||
result = this.noExecGateway.test1(Thread.currentThread());
|
||||
assertEquals(Thread.currentThread(), result.get());
|
||||
|
||||
ListenableFuture<Thread> result2 = this.execGateway.test2(Thread.currentThread());
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
final AtomicReference<Thread> thread = new AtomicReference<Thread>();
|
||||
result2.addCallback(new ListenableFutureCallback<Thread>() {
|
||||
|
||||
@Override
|
||||
public void onSuccess(Thread result) {
|
||||
thread.set(result);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
}
|
||||
});
|
||||
assertTrue(latch.await(10, TimeUnit.SECONDS));
|
||||
assertThat(result2.get().getName(), startsWith("exec-"));
|
||||
}
|
||||
|
||||
|
||||
public interface Foo {
|
||||
|
||||
@@ -373,6 +444,39 @@ public class GatewayInterfaceTests {
|
||||
public MessageChannel gatewayChannel() {
|
||||
return new DirectChannel();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@BridgeTo
|
||||
public MessageChannel gatewayThreadChannel() {
|
||||
DirectChannel channel = new DirectChannel();
|
||||
channel.addInterceptor(new ChannelInterceptorAdapter() {
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
Object payload;
|
||||
if (Thread.currentThread().equals(message.getPayload())) {
|
||||
// running on calling thread - need to return a Future.
|
||||
payload = new AsyncResult<Thread>(Thread.currentThread());
|
||||
}
|
||||
else {
|
||||
payload = Thread.currentThread();
|
||||
}
|
||||
return MessageBuilder.withPayload(payload)
|
||||
.copyHeaders(message.getHeaders())
|
||||
.build();
|
||||
}
|
||||
|
||||
});
|
||||
return channel;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AsyncTaskExecutor exec() {
|
||||
SimpleAsyncTaskExecutor simpleAsyncTaskExecutor = new SimpleAsyncTaskExecutor();
|
||||
simpleAsyncTaskExecutor.setThreadNamePrefix("exec-");
|
||||
return simpleAsyncTaskExecutor;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@MessagingGateway
|
||||
@@ -384,6 +488,28 @@ public class GatewayInterfaceTests {
|
||||
@Gateway(requestChannel = "gatewayChannel")
|
||||
Object test2(@Payload Map<Object, ?> map);
|
||||
|
||||
@Gateway(requestChannel = "gatewayThreadChannel")
|
||||
Future<Thread> test3(Thread caller);
|
||||
|
||||
}
|
||||
|
||||
@MessagingGateway(asyncExecutor = "exec")
|
||||
public interface ExecGateway {
|
||||
|
||||
@Gateway(requestChannel = "gatewayThreadChannel")
|
||||
Future<Thread> test1(Thread caller);
|
||||
|
||||
@Gateway(requestChannel = "gatewayThreadChannel")
|
||||
ListenableFuture<Thread> test2(Thread caller);
|
||||
|
||||
}
|
||||
|
||||
@MessagingGateway(asyncExecutor = AnnotationConstants.NULL)
|
||||
public interface NoExecGateway {
|
||||
|
||||
@Gateway(requestChannel = "gatewayThreadChannel")
|
||||
Future<Thread> test1(Thread caller);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -749,7 +749,7 @@ public Object handle(@Payload String payload, @Header(AmqpHeaders.CHANNEL) Chann
|
||||
generic <code>*</code>, to avoid mapping of <emphasis>request</emphasis> headers to the reply.
|
||||
</para>
|
||||
<para>
|
||||
Class <classname><ulink url="http://static.springsource.org/spring-integration/api/org/springframework/integration/amqp/AmqpHeaders.html">AmqpHeaders</ulink></classname>
|
||||
Class <classname>org.springframework.amqp.support.AmqpHeaders</classname>
|
||||
identifies the default headers that will be used by the
|
||||
<classname>DefaultAmqpHeaderMapper</classname>:
|
||||
</para>
|
||||
|
||||
@@ -442,13 +442,13 @@ of this chapter.
|
||||
<section id="async-gateway">
|
||||
<title>Asynchronous Gateway</title>
|
||||
<para>
|
||||
As a pattern the Messaging Gateway is a very nice way to hide messaging-specific code while still exposing the full capabilities of the
|
||||
As a pattern, the Messaging Gateway is a very nice way to hide messaging-specific code while still exposing the full capabilities of the
|
||||
messaging system. As you've seen, the <classname>GatewayProxyFactoryBean</classname> provides a convenient way to expose a Proxy over a service-interface
|
||||
thus giving you POJO-based access to a messaging system (based on objects in your own domain, or primitives/Strings, etc). But when a
|
||||
gateway is exposed via simple POJO methods which return values it does imply that for each Request message (generated when the method is invoked)
|
||||
there must be a Reply message (generated when the method has returned). Since Messaging systems naturally are asynchronous you may not always be
|
||||
able to guarantee the contract where <emphasis>"for each request there will always be be a reply"</emphasis>.
|
||||
With Spring Integration 2.0 we are introducing support for an <emphasis>Asynchronous Gateway</emphasis> which is a convenient way to initiate
|
||||
With Spring Integration 2.0 we introduced support for an <emphasis>Asynchronous Gateway</emphasis> which is a convenient way to initiate
|
||||
flows where you may not know if a reply is expected or how long will it take for replies to arrive.
|
||||
</para>
|
||||
<para>
|
||||
@@ -462,14 +462,16 @@ of this chapter.
|
||||
service-interface="org.springframework.integration.sample.gateway.futures.MathServiceGateway"
|
||||
default-request-channel="requestChannel"/>]]></programlisting>
|
||||
<para>
|
||||
However the Gateway Interface (service-interface) is a bit different.
|
||||
However the Gateway Interface (service-interface) is a little different:
|
||||
</para>
|
||||
<programlisting language="java">public interface MathServiceGateway {
|
||||
|
||||
Future<Integer> multiplyByTwo(int i);
|
||||
|
||||
}</programlisting>
|
||||
|
||||
<para>
|
||||
As you can see from the example above the return type for the gateway method is a <classname>Future</classname>. When
|
||||
As you can see from the example above, the return type for the gateway method is a <classname>Future</classname>. When
|
||||
<classname>GatewayProxyFactoryBean</classname> sees that the
|
||||
return type of the gateway method is a <classname>Future</classname>, it immediately switches to the async mode by utilizing
|
||||
an <classname>AsyncTaskExecutor</classname>. That is all. The call to such a method always returns immediately with a <classname>Future</classname> instance.
|
||||
@@ -483,18 +485,85 @@ int finalResult = result.get(1000, TimeUnit.SECONDS);</programlisting>
|
||||
<ulink url="https://github.com/SpringSource/spring-integration-samples/tree/master/intermediate/async-gateway">
|
||||
<emphasis>async-gateway</emphasis></ulink> sample distributed within the Spring Integration samples.
|
||||
</para>
|
||||
<para>
|
||||
<emphasis role="bold">ListenableFuture</emphasis>
|
||||
</para>
|
||||
<para>
|
||||
Starting with <emphasis>version 4.1</emphasis>, async gateway methods can also return
|
||||
<interfacename>ListenableFuture</interfacename> (introduced in Spring Framework 4.0). These
|
||||
return types allow you to provide a callback which is invoked when the result is available
|
||||
(or an exception occurs). When the gateway detects this return type, and the task executor
|
||||
(see below) is an <interfacename>AsyncListenableTaskExecutor</interfacename>, the executor's
|
||||
<code>submitListenable()</code> method is invoked.
|
||||
</para>
|
||||
<programlisting language="java"><![CDATA[ListenableFuture<String> result = this.asyncGateway.async("foo");
|
||||
result.addCallback(new ListenableFutureCallback<Thread>() {
|
||||
|
||||
@Override
|
||||
public void onSuccess(Thread result) {
|
||||
...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable t) {
|
||||
...
|
||||
}
|
||||
});]]></programlisting>
|
||||
<para><emphasis role="bold">Asynchronous Gateway and AsyncTaskExecutor</emphasis></para>
|
||||
<para>
|
||||
By default <classname>GatewayProxyFactoryBean</classname> uses <classname>org.springframework.core.task.SimpleAsyncTaskExecutor</classname>
|
||||
By default, the <classname>GatewayProxyFactoryBean</classname> uses <classname>org.springframework.core.task.SimpleAsyncTaskExecutor</classname>
|
||||
when submitting internal <classname>AsyncInvocationTask</classname> instances for any gateway method whose
|
||||
return type is <classname>Future.class</classname>. However the <literal>async-executor</literal> attribute in the
|
||||
return type is <classname>Future</classname>. However the <literal>async-executor</literal> attribute in the
|
||||
<literal><gateway/></literal> element's configuration allows you to provide a reference to any implementation of
|
||||
<classname>java.util.concurrent.Executor</classname> available within the Spring application context.
|
||||
</para>
|
||||
<para>
|
||||
The (default) <classname>SimpleAsyncTaskExecutor</classname> supports both
|
||||
<interfacename>Future</interfacename> and <interfacename>ListenableFuture</interfacename>
|
||||
return types, returning <classname>FutureTask</classname> or <classname>ListenableFutureTask</classname>
|
||||
respectively. Even though there is a default executor, it is often useful to provide an external
|
||||
one so that you can identify its threads in logs (when using XML, the thread name is based on the executor's
|
||||
bean name):
|
||||
<para>
|
||||
<programlisting language="java"><![CDATA[@Bean
|
||||
public AsyncTaskExecutor exec() {
|
||||
SimpleAsyncTaskExecutor simpleAsyncTaskExecutor = new SimpleAsyncTaskExecutor();
|
||||
simpleAsyncTaskExecutor.setThreadNamePrefix("exec-");
|
||||
return simpleAsyncTaskExecutor;
|
||||
}
|
||||
|
||||
@MessagingGateway(asyncExecutor = "exec")
|
||||
public interface ExecGateway {
|
||||
|
||||
@Gateway(requestChannel = "gatewayChannel")
|
||||
Future<?> doAsync(String foo);
|
||||
|
||||
}]]></programlisting>
|
||||
</para>
|
||||
If you wish to return a different <interfacename>Future</interfacename>
|
||||
implementation, you can provide a custom executor, or disable the executor altogether and
|
||||
return the <interfacename>Future</interfacename> in the reply message payload from the downstream flow.
|
||||
To disable the executor, simply set it to <code>null</code> in the
|
||||
<classname>GatewayProxyFactoryBean</classname> (<code>setAsyncTaskExecutor(null)</code>). When configuring
|
||||
the gateway with XML, use <code>async-executor=""</code>; when configuring using the
|
||||
<classname>@MessagingGateway</classname> annotation, use:
|
||||
</para>
|
||||
<programlisting language="java"><![CDATA[@MessagingGateway(asyncExecutor = AnnotationConstants.NULL)
|
||||
public interface NoExecGateway {
|
||||
|
||||
@Gateway(requestChannel = "gatewayChannel")
|
||||
Future<?> doAsync(String foo);
|
||||
|
||||
}]]></programlisting>
|
||||
<important>
|
||||
If the return type is a specific concrete <interfacename>Future</interfacename> implementation
|
||||
or some other subinterface that is not supported by the configured executor, the flow will
|
||||
run on the caller's thread and the flow must return the required type in the reply message
|
||||
payload.
|
||||
</important>
|
||||
<para><emphasis role="bold">Asynchronous Gateway and Reactor Promise</emphasis></para>
|
||||
<para>
|
||||
Starting with <emphasis>version 4.1</emphasis>, the <classname>GatewayProxyFactoryBean</classname> allows the
|
||||
Also starting with <emphasis>version 4.1</emphasis>, the <classname>GatewayProxyFactoryBean</classname> allows the
|
||||
use of a <classname>Reactor</classname> with gateway interface methods, utilizing a
|
||||
<ulink url="https://github.com/reactor/reactor/wiki/Promises"><classname>Promise<?></classname></ulink>
|
||||
return type. The internal <classname>AsyncInvocationTask</classname> is wrapped in a
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
</para>
|
||||
<section id="4.1-new-components">
|
||||
<title>New Components</title>
|
||||
<section id="4.1-promise-gateway">
|
||||
<title>Promise<?> Gateway</title>
|
||||
<para>
|
||||
A Reactor <classname>Promise</classname> return type is now supported for Messaging Gateway methods.
|
||||
See <xref linkend="async-gateway"/>.
|
||||
</para>
|
||||
</section>
|
||||
<section id="4.1-promise-gateway">
|
||||
<title>Promise<?> Gateway</title>
|
||||
<para>
|
||||
A Reactor <classname>Promise</classname> return type is now supported for Messaging Gateway methods.
|
||||
See <xref linkend="async-gateway"/>.
|
||||
</para>
|
||||
</section>
|
||||
</section>
|
||||
<section id="4.1-general">
|
||||
<title>General Changes</title>
|
||||
@@ -177,5 +177,15 @@
|
||||
See <xref linkend="syslog-inbound-adapter"/> for more information.
|
||||
</para>
|
||||
</section>
|
||||
<section id="4.1-async-gateway">
|
||||
<title>Async Gateway</title>
|
||||
<para>
|
||||
In addition to the <classname>Promise</classname> return type mentioned above,
|
||||
gateway methods may now return a <classname>ListenableFuture</classname>, introduced
|
||||
in Spring Framework 4.0. You can also disable the async processing in the gateway,
|
||||
allowing a downstream flow to directly return a <classname>Future</classname>.
|
||||
See <xref linkend="async-gateway"/>.
|
||||
</para>
|
||||
</section>
|
||||
</section>
|
||||
</chapter>
|
||||
|
||||
Reference in New Issue
Block a user