INT-3477: Add Reactor Promise<?> for Gateway
JIRA: https://jira.spring.io/browse/INT-3477 INT-3477: Fix `reactorEnvironment` propagation * `MessagingGatewayRegistrar` parser the value for the `reactorEnvironment` * Provide more interest test-case * Polishing docs INT-3477 Require `Environment` in case of Promise * Do not use Reactor `Environment` as default instance. * Require `Environment` reference, when is `Promise` method * Polishing for tests * Apply Gary's polishing for docs INT-3477: Assert.notNull -> Assert.state INT-3477: PR review
This commit is contained in:
committed by
Gary Russell
parent
0674abcacb
commit
8ac4bfbd2c
@@ -101,7 +101,8 @@ subprojects { subproject ->
|
||||
openJpaVersion = '2.3.0'
|
||||
pahoMqttClientVersion = '0.4.0'
|
||||
postgresVersion = '9.1-901-1.jdbc4'
|
||||
reactorVersion = '1.1.2.RELEASE'
|
||||
reactorVersion = '1.1.4.RELEASE'
|
||||
reactorSpringVersion = '1.1.3.RELEASE'
|
||||
romeToolsVersion = '1.5.0'
|
||||
romeVersion = '1.0.0'
|
||||
saajApiVersion = '1.3.5'
|
||||
@@ -243,6 +244,7 @@ project('spring-integration-core') {
|
||||
compile "org.springframework:spring-tx:$springVersion"
|
||||
compile "org.springframework.retry:spring-retry:$springRetryVersion"
|
||||
compile "org.projectreactor:reactor-core:$reactorVersion"
|
||||
compile("org.projectreactor.spring:reactor-spring-context:$reactorSpringVersion", optional)
|
||||
compile("org.codehaus.jackson:jackson-mapper-asl:$jacksonVersion", optional)
|
||||
compile("com.fasterxml.jackson.core:jackson-databind:$jackson2Version", optional)
|
||||
compile("com.jayway.jsonpath:json-path:$jsonpathVersion", optional)
|
||||
|
||||
@@ -36,14 +36,12 @@ public @interface MessagingGateway {
|
||||
/**
|
||||
* The value may indicate a suggestion for a logical component name,
|
||||
* to be turned into a Spring bean in case of an autodetected component.
|
||||
*
|
||||
* @return the suggested component name, if any
|
||||
*/
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* Identifies default channel the messages will be sent to upon invocation of methods of the gateway proxy.
|
||||
*
|
||||
* @return the suggested channel name, if any
|
||||
*/
|
||||
String defaultRequestChannel() default "";
|
||||
@@ -51,7 +49,6 @@ public @interface MessagingGateway {
|
||||
/**
|
||||
* Identifies default channel the gateway proxy will subscribe to to receive reply {@code Message}s, which will then be
|
||||
* converted to the return type of the method signature.
|
||||
*
|
||||
* @return the suggested channel name, if any
|
||||
*/
|
||||
String defaultReplyChannel() default "";
|
||||
@@ -61,7 +58,6 @@ public @interface MessagingGateway {
|
||||
* gateway's proxy invocation. If no {@code errorChannel} reference is provided, the gateway will
|
||||
* propagate {@code Exception}s to the caller. To completely suppress {@code Exception}s, provide a
|
||||
* reference to the {@code nullChannel} here.
|
||||
*
|
||||
* @return the suggested channel name, if any
|
||||
*/
|
||||
String errorChannel() default "";
|
||||
@@ -70,7 +66,6 @@ public @interface MessagingGateway {
|
||||
* Provides the amount of time dispatcher would wait to send a {@code Message}.
|
||||
* This timeout would only apply if there is a potential to block in the send call.
|
||||
* For example if this gateway is hooked up to a {@code QueueChannel}.
|
||||
*
|
||||
* @return the suggested timeout in milliseconds, if any
|
||||
*/
|
||||
long defaultRequestTimeout() default Long.MIN_VALUE;
|
||||
@@ -79,7 +74,6 @@ public @interface MessagingGateway {
|
||||
* Allows to specify how long this gateway will wait for the reply {@code Message}
|
||||
* before returning. By default it will wait indefinitely. {@code null} is returned
|
||||
if the gateway times out.
|
||||
*
|
||||
* @return the suggested timeout in milliseconds, if any
|
||||
*/
|
||||
long defaultReplyTimeout() default Long.MIN_VALUE;
|
||||
@@ -89,7 +83,6 @@ 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.
|
||||
*
|
||||
* @return the suggested executor bean name, if any
|
||||
*/
|
||||
String asyncExecutor() default "";
|
||||
@@ -99,7 +92,6 @@ public @interface MessagingGateway {
|
||||
* unless explicitly overridden by a method declaration. Variables include {@code #args}, {@code #methodName},
|
||||
* {@code #methodString} and {@code #methodObject};
|
||||
* a bean resolver is also available, enabling expressions like {@code @someBean(#args)}.
|
||||
*
|
||||
* @return the suggested payload expression, if any
|
||||
*/
|
||||
String defaultPayloadExpression() default "";
|
||||
@@ -107,7 +99,6 @@ public @interface MessagingGateway {
|
||||
/**
|
||||
* Provides custom message headers. These default headers are created for
|
||||
* all methods on the service-interface (unless overridden by a specific method).
|
||||
*
|
||||
* @return the suggested payload expression, if any
|
||||
*/
|
||||
GatewayHeader[] defaultHeaders() default {};
|
||||
@@ -117,9 +108,19 @@ public @interface MessagingGateway {
|
||||
* to map the method arguments to a {@link org.springframework.messaging.Message}. When this
|
||||
* is provided, no {@code payload-expression}s or {@code header}s are allowed; the custom mapper is
|
||||
* responsible for creating the message.
|
||||
*
|
||||
* @return the suggested mapper bean name, if any
|
||||
*/
|
||||
String mapper() default "";
|
||||
|
||||
/**
|
||||
* Provide a reference to an {@link reactor.core.Environment}
|
||||
* to use for any of the interface methods that have a {@link reactor.core.composable.Promise} return type.
|
||||
* This {@code reactor.core.Environment} will only be used for those async methods; the sync methods
|
||||
* will be invoked in the caller's thread.
|
||||
* <p> This attribute is required in case of {@link reactor.core.composable.Promise} usage.
|
||||
* @return the suggested reactor Environment bean name.
|
||||
* @since 4.1
|
||||
*/
|
||||
String reactorEnvironment() default "";
|
||||
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar
|
||||
String defaultReplyChannel = (String) gatewayAttributes.get("defaultReplyChannel");
|
||||
String errorChannel = (String) gatewayAttributes.get("errorChannel");
|
||||
String asyncExecutor = (String) gatewayAttributes.get("asyncExecutor");
|
||||
String reactorEnvironment = (String) gatewayAttributes.get("reactorEnvironment");
|
||||
String mapper = (String) gatewayAttributes.get("mapper");
|
||||
|
||||
boolean hasMapper = StringUtils.hasText(mapper);
|
||||
@@ -135,6 +136,9 @@ public class MessagingGatewayRegistrar implements ImportBeanDefinitionRegistrar
|
||||
if (StringUtils.hasText(asyncExecutor)) {
|
||||
gatewayProxyBuilder.addPropertyReference("asyncExecutor", asyncExecutor);
|
||||
}
|
||||
if (StringUtils.hasText(reactorEnvironment)) {
|
||||
gatewayProxyBuilder.addPropertyReference("reactorEnvironment", reactorEnvironment);
|
||||
}
|
||||
if (StringUtils.hasText(mapper)) {
|
||||
gatewayProxyBuilder.addPropertyReference("mapper", mapper);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ public class GatewayParser implements BeanDefinitionParser {
|
||||
gatewayAttributes.put("errorChannel", element.getAttribute("error-channel"));
|
||||
gatewayAttributes.put("asyncExecutor", element.getAttribute("async-executor"));
|
||||
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"));
|
||||
|
||||
|
||||
@@ -62,6 +62,11 @@ 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
|
||||
@@ -75,7 +80,8 @@ import org.springframework.util.StringUtils;
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
public class GatewayProxyFactoryBean extends AbstractEndpoint implements TrackableComponent, FactoryBean<Object>, MethodInterceptor, BeanClassLoaderAware {
|
||||
public class GatewayProxyFactoryBean extends AbstractEndpoint
|
||||
implements TrackableComponent, FactoryBean<Object>, MethodInterceptor, BeanClassLoaderAware {
|
||||
|
||||
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
|
||||
|
||||
@@ -105,6 +111,8 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint implements Trackab
|
||||
|
||||
private volatile AsyncTaskExecutor asyncExecutor = new SimpleAsyncTaskExecutor();
|
||||
|
||||
private volatile Environment reactorEnvironment;
|
||||
|
||||
private volatile boolean initialized;
|
||||
|
||||
private final Object initializationMonitor = new Object();
|
||||
@@ -225,6 +233,17 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint implements Trackab
|
||||
this.globalMethodMetadata = globalMethodMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Reactor {@link Environment} to be used for processing methods with a
|
||||
* {@link Promise} return type. (Required when any such methods are declared on the
|
||||
* service interface).
|
||||
* @param reactorEnvironment the Reactor Environment.
|
||||
* @since 4.1
|
||||
*/
|
||||
public void setReactorEnvironment(Environment reactorEnvironment) {
|
||||
this.reactorEnvironment = reactorEnvironment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanClassLoader(ClassLoader beanClassLoader) {
|
||||
this.beanClassLoader = beanClassLoader;
|
||||
@@ -289,9 +308,18 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint implements Trackab
|
||||
|
||||
@Override
|
||||
public Object invoke(final MethodInvocation invocation) throws Throwable {
|
||||
if (Future.class.isAssignableFrom(invocation.getMethod().getReturnType())) {
|
||||
final Class<?> returnType = invocation.getMethod().getReturnType();
|
||||
if (Future.class.isAssignableFrom(returnType)) {
|
||||
return this.asyncExecutor.submit(new AsyncInvocationTask(invocation));
|
||||
}
|
||||
if (Promise.class.isAssignableFrom(returnType)) {
|
||||
if (this.reactorEnvironment == null) {
|
||||
throw new IllegalStateException("'reactorEnvironment' is required in case of 'Promise' return type.");
|
||||
}
|
||||
return Promises.<Object>task(Functions.supplier(new AsyncInvocationTask(invocation)))
|
||||
.env(this.reactorEnvironment)
|
||||
.get();
|
||||
}
|
||||
return this.doInvoke(invocation);
|
||||
}
|
||||
|
||||
@@ -317,7 +345,7 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint implements Trackab
|
||||
MethodInvocationGateway gateway = this.gatewayMap.get(method);
|
||||
Class<?> returnType = method.getReturnType();
|
||||
boolean shouldReturnMessage = Message.class.isAssignableFrom(returnType)
|
||||
|| hasFutureParameterizedWithMessage(method);
|
||||
|| hasReturnParameterizedWithMessage(method);
|
||||
boolean shouldReply = returnType != void.class;
|
||||
int paramCount = method.getParameterTypes().length;
|
||||
Object response = null;
|
||||
@@ -510,6 +538,9 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint implements Trackab
|
||||
if (Future.class.isAssignableFrom(expectedReturnType)) {
|
||||
return (T) source;
|
||||
}
|
||||
if (Promise.class.isAssignableFrom(expectedReturnType)) {
|
||||
return (T) source;
|
||||
}
|
||||
if (this.getConversionService() != null) {
|
||||
return this.getConversionService().convert(source, expectedReturnType);
|
||||
}
|
||||
@@ -518,8 +549,8 @@ public class GatewayProxyFactoryBean extends AbstractEndpoint implements Trackab
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasFutureParameterizedWithMessage(Method method) {
|
||||
if (Future.class.isAssignableFrom(method.getReturnType())) {
|
||||
private static boolean hasReturnParameterizedWithMessage(Method method) {
|
||||
if (Future.class.isAssignableFrom(method.getReturnType()) || Promise.class.isAssignableFrom(method.getReturnType())) {
|
||||
Type returnType = method.getGenericReturnType();
|
||||
if (returnType instanceof ParameterizedType) {
|
||||
Type[] typeArgs = ((ParameterizedType) returnType).getActualTypeArguments();
|
||||
|
||||
@@ -778,6 +778,20 @@
|
||||
</xsd:appinfo>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
<xsd:attribute name="reactor-environment" type="xsd:string">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation>
|
||||
<![CDATA[
|
||||
Provide a reference to 'reactor.core.Environment'
|
||||
to use for any of the interface methods that have a 'reactor.core.composable.Promise' return type.
|
||||
The Reactor's Environment will only be used for those async methods; the sync methods
|
||||
will be invoked in the caller's thread.
|
||||
This attribute is required if any 'service-interface' methods
|
||||
have a 'reactor.core.composable.Promise' return type.
|
||||
]]>
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
|
||||
@@ -1462,7 +1476,7 @@
|
||||
<xsd:annotation>
|
||||
<xsd:documentation><![CDATA[
|
||||
The expression to be evaluated, when the underlying enricher sub-flow returns null.
|
||||
The evaluation context root object is requestMessage.
|
||||
The evaluation context root object is requestMessage.
|
||||
]]></xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:attribute>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans:beans xmlns="http://www.springframework.org/schema/integration"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:beans="http://www.springframework.org/schema/beans"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration
|
||||
http://www.springframework.org/schema/integration/spring-integration.xsd">
|
||||
@@ -16,29 +16,37 @@
|
||||
</channel>
|
||||
|
||||
<gateway id="oneWay"
|
||||
service-interface="org.springframework.integration.gateway.TestService"
|
||||
default-request-channel="requestChannel"/>
|
||||
service-interface="org.springframework.integration.gateway.TestService"
|
||||
default-request-channel="requestChannel"/>
|
||||
|
||||
<gateway id="solicitResponse"
|
||||
service-interface="org.springframework.integration.gateway.TestService"
|
||||
default-reply-channel="replyChannel"
|
||||
default-reply-timeout="3000"/>
|
||||
service-interface="org.springframework.integration.gateway.TestService"
|
||||
default-reply-channel="replyChannel"
|
||||
default-reply-timeout="3000"/>
|
||||
|
||||
<gateway id="requestReply"
|
||||
service-interface="org.springframework.integration.gateway.TestService"
|
||||
default-request-channel="requestChannel"
|
||||
default-reply-channel="replyChannel"
|
||||
default-reply-timeout="5000"/>
|
||||
service-interface="org.springframework.integration.gateway.TestService"
|
||||
default-request-channel="requestChannel"
|
||||
default-reply-channel="replyChannel"
|
||||
default-reply-timeout="5000"/>
|
||||
|
||||
<gateway id="async"
|
||||
service-interface="org.springframework.integration.gateway.TestService"
|
||||
default-request-channel="requestChannel"
|
||||
default-reply-channel="replyChannel"
|
||||
async-executor="testExecutor"/>
|
||||
|
||||
|
||||
<gateway id="promise"
|
||||
service-interface="org.springframework.integration.gateway.TestService"
|
||||
default-request-channel="requestChannel"
|
||||
default-reply-channel="replyChannel"
|
||||
reactor-environment="reactorEnvironment"/>
|
||||
|
||||
<!-- no assertions for this. The fact that this config does not result in error is sufficient -->
|
||||
<gateway default-request-channel="nullChannel"/>
|
||||
|
||||
<beans:bean id="testExecutor" class="org.springframework.integration.config.xml.GatewayParserTests$TestExecutor"/>
|
||||
|
||||
<beans:bean id="reactorEnvironment" class="reactor.core.Environment" destroy-method="shutdown"/>
|
||||
|
||||
</beans:beans>
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
* Copyright 2002-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.
|
||||
@@ -24,26 +24,39 @@ import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.BeanNameAware;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.support.ClassPathXmlApplicationContext;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.integration.gateway.TestService;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.integration.gateway.TestService;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
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 reactor.core.composable.Promise;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
@ContextConfiguration
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||
public class GatewayParserTests {
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext context;
|
||||
|
||||
@Test
|
||||
public void testOneWay() {
|
||||
ApplicationContext context = new ClassPathXmlApplicationContext("gatewayParserTests.xml", this.getClass());
|
||||
TestService service = (TestService) context.getBean("oneWay");
|
||||
service.oneWay("foo");
|
||||
PollableChannel channel = (PollableChannel) context.getBean("requestChannel");
|
||||
@@ -53,7 +66,6 @@ public class GatewayParserTests {
|
||||
|
||||
@Test
|
||||
public void testSolicitResponse() {
|
||||
ApplicationContext context = new ClassPathXmlApplicationContext("gatewayParserTests.xml", this.getClass());
|
||||
PollableChannel channel = (PollableChannel) context.getBean("replyChannel");
|
||||
channel.send(new GenericMessage<String>("foo"));
|
||||
TestService service = (TestService) context.getBean("solicitResponse");
|
||||
@@ -63,18 +75,16 @@ public class GatewayParserTests {
|
||||
|
||||
@Test
|
||||
public void testRequestReply() {
|
||||
ApplicationContext context = new ClassPathXmlApplicationContext("gatewayParserTests.xml", this.getClass());
|
||||
PollableChannel requestChannel = (PollableChannel) context.getBean("requestChannel");
|
||||
MessageChannel replyChannel = (MessageChannel) context.getBean("replyChannel");
|
||||
this.startResponder(requestChannel, replyChannel);
|
||||
TestService service = (TestService) context.getBean("requestReply");
|
||||
String result = service.requestReply("foo");
|
||||
assertEquals("foo", result);
|
||||
assertEquals("foo", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAsyncGateway() throws Exception {
|
||||
ApplicationContext context = new ClassPathXmlApplicationContext("gatewayParserTests.xml", this.getClass());
|
||||
PollableChannel requestChannel = (PollableChannel) context.getBean("requestChannel");
|
||||
MessageChannel replyChannel = (MessageChannel) context.getBean("replyChannel");
|
||||
this.startResponder(requestChannel, replyChannel);
|
||||
@@ -85,6 +95,16 @@ public class GatewayParserTests {
|
||||
assertEquals("testExecutor", reply.getHeaders().get("executor"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPromiseGateway() throws Exception {
|
||||
PollableChannel requestChannel = context.getBean("requestChannel", PollableChannel.class);
|
||||
MessageChannel replyChannel = context.getBean("replyChannel", MessageChannel.class);
|
||||
this.startResponder(requestChannel, replyChannel);
|
||||
TestService service = context.getBean("promise", TestService.class);
|
||||
Promise<Message<?>> result = service.promise("foo");
|
||||
Message<?> reply = result.await(1, TimeUnit.SECONDS);
|
||||
assertEquals("foo", reply.getPayload());
|
||||
}
|
||||
|
||||
private void startResponder(final PollableChannel requestChannel, final MessageChannel replyChannel) {
|
||||
Executors.newSingleThreadExecutor().execute(new Runnable() {
|
||||
@@ -123,6 +143,7 @@ public class GatewayParserTests {
|
||||
throw new IllegalStateException("unexpected exception in testExecutor", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
@@ -116,6 +117,14 @@ import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.support.AnnotationConfigContextLoader;
|
||||
|
||||
import reactor.core.Environment;
|
||||
import reactor.core.composable.Composable;
|
||||
import reactor.core.composable.Promise;
|
||||
import reactor.core.composable.spec.Streams;
|
||||
import reactor.function.Consumer;
|
||||
import reactor.function.Function;
|
||||
import reactor.spring.context.config.EnableReactor;
|
||||
|
||||
/**
|
||||
* @author Artem Bilan
|
||||
* @since 4.0
|
||||
@@ -541,6 +550,49 @@ public class EnableIntegrationTests {
|
||||
assertNull(replyChannel.receive(10));
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private Environment environment;
|
||||
|
||||
@Test
|
||||
public void testPromiseGateway() throws Exception {
|
||||
|
||||
final AtomicReference<List<Integer>> ref = new AtomicReference<List<Integer>>();
|
||||
final CountDownLatch consumeLatch = new CountDownLatch(1);
|
||||
|
||||
Streams.defer(Arrays.asList("1", "2", "3", "4", "5"))
|
||||
.env(this.environment)
|
||||
.get()
|
||||
.map(new Function<String, Integer>() {
|
||||
@Override
|
||||
public Integer apply(String s) {
|
||||
return Integer.parseInt(s);
|
||||
}
|
||||
})
|
||||
.mapMany(new Function<Integer, Composable<Integer>>() {
|
||||
@Override
|
||||
public Composable<Integer> apply(Integer integer) {
|
||||
return testGateway.multiply(integer);
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.consume(new Consumer<List<Integer>>() {
|
||||
@Override
|
||||
public void accept(List<Integer> integers) {
|
||||
ref.set(integers);
|
||||
consumeLatch.countDown();
|
||||
}
|
||||
})
|
||||
.flush();
|
||||
|
||||
|
||||
assertTrue(consumeLatch.await(2, TimeUnit.SECONDS));
|
||||
|
||||
List<Integer> integers = ref.get();
|
||||
assertEquals(5, integers.size());
|
||||
|
||||
assertThat(integers, Matchers.<Integer>contains(2, 4, 6, 8, 10));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ComponentScan
|
||||
@IntegrationComponentScan
|
||||
@@ -775,6 +827,7 @@ public class EnableIntegrationTests {
|
||||
@EnableMessageHistory("${message.history.tracked.components}")
|
||||
@EnablePublisher("publishedChannel")
|
||||
@EnableAsync
|
||||
@EnableReactor
|
||||
public static class ContextConfiguration2 {
|
||||
|
||||
@Bean
|
||||
@@ -873,6 +926,11 @@ public class EnableIntegrationTests {
|
||||
return new SerializingConverter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DirectChannel promiseChannel() {
|
||||
return new DirectChannel();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@@ -1038,6 +1096,10 @@ public class EnableIntegrationTests {
|
||||
/*@BridgeFrom("")
|
||||
public void invalidBridgeAnnotationMethod(Object payload) {}*/
|
||||
|
||||
@ServiceActivator(inputChannel = "promiseChannel")
|
||||
public Integer multiply(Integer value) {
|
||||
return value * 2;
|
||||
}
|
||||
}
|
||||
|
||||
@TestMessagingGateway
|
||||
@@ -1050,6 +1112,9 @@ public class EnableIntegrationTests {
|
||||
@Async
|
||||
void sendAsync(String payload);
|
||||
|
||||
@Gateway(requestChannel = "promiseChannel")
|
||||
Promise<Integer> multiply(Integer value);
|
||||
|
||||
}
|
||||
|
||||
@TestMessagingGateway2
|
||||
@@ -1062,7 +1127,7 @@ public class EnableIntegrationTests {
|
||||
|
||||
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@MessagingGateway(defaultRequestChannel = "gatewayChannel",
|
||||
@MessagingGateway(defaultRequestChannel = "gatewayChannel", reactorEnvironment = "reactorEnv",
|
||||
defaultHeaders = @GatewayHeader(name = "foo", value = "FOO"))
|
||||
public static @interface TestMessagingGateway {
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-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.
|
||||
@@ -17,32 +17,48 @@
|
||||
package org.springframework.integration.gateway;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
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.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.integration.channel.QueueChannel;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.PollableChannel;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import reactor.core.Environment;
|
||||
import reactor.core.composable.Promise;
|
||||
import reactor.function.Consumer;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
* @since 2.0
|
||||
*/
|
||||
public class AsyncGatewayTests {
|
||||
|
||||
private final Environment reactorEnvironment = new Environment();
|
||||
|
||||
// TODO: changed from 0 because of recurrent failure: is this right?
|
||||
private final long safety = 100;
|
||||
|
||||
public void tearDown() {
|
||||
this.reactorEnvironment.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void futureWithMessageReturned() throws Exception {
|
||||
QueueChannel requestChannel = new QueueChannel();
|
||||
@@ -79,7 +95,7 @@ public class AsyncGatewayTests {
|
||||
Object result = f.get(1000, TimeUnit.MILLISECONDS);
|
||||
long elapsed = System.currentTimeMillis() - start;
|
||||
|
||||
assertTrue(elapsed >= 200-safety);
|
||||
assertTrue(elapsed >= 200 - safety);
|
||||
assertTrue(result instanceof String);
|
||||
assertEquals("foobar", result);
|
||||
}
|
||||
@@ -100,12 +116,127 @@ public class AsyncGatewayTests {
|
||||
Object result = f.get(1000, TimeUnit.MILLISECONDS);
|
||||
long elapsed = System.currentTimeMillis() - start;
|
||||
|
||||
assertTrue(elapsed >= 200-safety);
|
||||
assertTrue(elapsed >= 200 - safety);
|
||||
assertTrue(result instanceof String);
|
||||
assertEquals("foobar", result);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void promiseWithMessageReturned() throws Exception {
|
||||
QueueChannel requestChannel = new QueueChannel();
|
||||
startResponder(requestChannel);
|
||||
GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean();
|
||||
proxyFactory.setDefaultRequestChannel(requestChannel);
|
||||
proxyFactory.setServiceInterface(TestEchoService.class);
|
||||
proxyFactory.setBeanFactory(mock(BeanFactory.class));
|
||||
proxyFactory.setBeanName("testGateway");
|
||||
proxyFactory.setReactorEnvironment(this.reactorEnvironment);
|
||||
proxyFactory.afterPropertiesSet();
|
||||
TestEchoService service = (TestEchoService) proxyFactory.getObject();
|
||||
Promise<Message<?>> promise = service.returnMessagePromise("foo");
|
||||
long start = System.currentTimeMillis();
|
||||
Object result = promise.await(1, TimeUnit.SECONDS);
|
||||
long elapsed = System.currentTimeMillis() - start;
|
||||
assertTrue(elapsed >= 200);
|
||||
assertEquals("foobar", ((Message<?>) result).getPayload());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void promiseWithPayloadReturned() throws Exception {
|
||||
QueueChannel requestChannel = new QueueChannel();
|
||||
startResponder(requestChannel);
|
||||
GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean();
|
||||
proxyFactory.setDefaultRequestChannel(requestChannel);
|
||||
proxyFactory.setServiceInterface(TestEchoService.class);
|
||||
proxyFactory.setBeanFactory(mock(BeanFactory.class));
|
||||
proxyFactory.setReactorEnvironment(this.reactorEnvironment);
|
||||
proxyFactory.setBeanName("testGateway");
|
||||
proxyFactory.afterPropertiesSet();
|
||||
TestEchoService service = (TestEchoService) proxyFactory.getObject();
|
||||
Promise<String> promise = service.returnStringPromise("foo");
|
||||
long start = System.currentTimeMillis();
|
||||
Object result = promise.await(1, TimeUnit.SECONDS);
|
||||
long elapsed = System.currentTimeMillis() - start;
|
||||
|
||||
assertTrue(elapsed >= 200 - safety);
|
||||
assertEquals("foobar", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void promiseWithWildcardReturned() throws Exception {
|
||||
QueueChannel requestChannel = new QueueChannel();
|
||||
startResponder(requestChannel);
|
||||
GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean();
|
||||
proxyFactory.setDefaultRequestChannel(requestChannel);
|
||||
proxyFactory.setServiceInterface(TestEchoService.class);
|
||||
proxyFactory.setBeanFactory(mock(BeanFactory.class));
|
||||
proxyFactory.setBeanName("testGateway");
|
||||
proxyFactory.setReactorEnvironment(this.reactorEnvironment);
|
||||
proxyFactory.afterPropertiesSet();
|
||||
TestEchoService service = (TestEchoService) proxyFactory.getObject();
|
||||
Promise<?> promise = service.returnSomethingPromise("foo");
|
||||
long start = System.currentTimeMillis();
|
||||
Object result = promise.await(1, TimeUnit.SECONDS);
|
||||
long elapsed = System.currentTimeMillis() - start;
|
||||
|
||||
assertTrue(elapsed >= 200 - safety);
|
||||
assertTrue(result instanceof String);
|
||||
assertEquals("foobar", result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void promiseWithConsumer() throws Exception {
|
||||
QueueChannel requestChannel = new QueueChannel();
|
||||
startResponder(requestChannel);
|
||||
GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean();
|
||||
proxyFactory.setDefaultRequestChannel(requestChannel);
|
||||
proxyFactory.setServiceInterface(TestEchoService.class);
|
||||
proxyFactory.setBeanFactory(mock(BeanFactory.class));
|
||||
proxyFactory.setBeanName("testGateway");
|
||||
proxyFactory.setReactorEnvironment(this.reactorEnvironment);
|
||||
proxyFactory.afterPropertiesSet();
|
||||
TestEchoService service = (TestEchoService) proxyFactory.getObject();
|
||||
Promise<String> promise = service.returnStringPromise("foo");
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
final AtomicReference<String> result = new AtomicReference<String>();
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
promise.consume(new Consumer<String>() {
|
||||
@Override
|
||||
public void accept(String s) {
|
||||
result.set(s);
|
||||
latch.countDown();
|
||||
}
|
||||
})
|
||||
.flush();
|
||||
|
||||
latch.await(1, TimeUnit.SECONDS);
|
||||
long elapsed = System.currentTimeMillis() - start;
|
||||
|
||||
assertTrue(elapsed >= 200 - safety);
|
||||
assertEquals("foobar", result.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void promiseMethodWithoutEnvironment() throws Exception {
|
||||
GatewayProxyFactoryBean proxyFactory = new GatewayProxyFactoryBean();
|
||||
proxyFactory.setServiceInterface(TestEchoService.class);
|
||||
proxyFactory.setBeanFactory(mock(BeanFactory.class));
|
||||
proxyFactory.setBeanName("testGateway");
|
||||
proxyFactory.afterPropertiesSet();
|
||||
TestEchoService service = (TestEchoService) proxyFactory.getObject();
|
||||
try {
|
||||
service.returnStringPromise("foo");
|
||||
fail("IllegalStateException expected");
|
||||
}
|
||||
catch (Exception e) {
|
||||
assertThat(e, Matchers.instanceOf(IllegalStateException.class));
|
||||
assertEquals(e.getMessage(), "'reactorEnvironment' is required in case of 'Promise' return type.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void startResponder(final PollableChannel requestChannel) {
|
||||
new Thread(new Runnable() {
|
||||
public void run() {
|
||||
@@ -132,6 +263,12 @@ public class AsyncGatewayTests {
|
||||
|
||||
Future<?> returnSomething(String s);
|
||||
|
||||
Promise<String> returnStringPromise(String s);
|
||||
|
||||
Promise<Message<?>> returnMessagePromise(String s);
|
||||
|
||||
Promise<?> returnSomethingPromise(String s);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2011 the original author or authors.
|
||||
* Copyright 2002-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.
|
||||
@@ -18,17 +18,20 @@ package org.springframework.integration.gateway;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.integration.annotation.Payload;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
import reactor.core.composable.Promise;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Artem Bilan
|
||||
*/
|
||||
public interface TestService {
|
||||
|
||||
String requestReply(String input);
|
||||
|
||||
|
||||
byte[] requestReplyInBytes(String input);
|
||||
|
||||
void oneWay(String input);
|
||||
@@ -46,4 +49,6 @@ public interface TestService {
|
||||
|
||||
Future<Message<?>> async(String s);
|
||||
|
||||
Promise<Message<?>> promise(String s);
|
||||
|
||||
}
|
||||
|
||||
@@ -484,7 +484,7 @@ int finalResult = result.get(1000, TimeUnit.SECONDS);</programlisting>
|
||||
<emphasis>async-gateway</emphasis></ulink> sample distributed within the Spring Integration samples.
|
||||
</para>
|
||||
|
||||
<para><emphasis>Asynchronous Gateway and AsyncTaskExecutor</emphasis></para>
|
||||
<para><emphasis role="bold">Asynchronous Gateway and AsyncTaskExecutor</emphasis></para>
|
||||
<para>
|
||||
By default <classname>GatewayProxyFactoryBean</classname> uses <classname>org.springframework.core.task.SimpleAsyncTaskExecutor</classname>
|
||||
when submitting internal <classname>AsyncInvocationTask</classname> instances for any gateway method whose
|
||||
@@ -492,6 +492,72 @@ int finalResult = result.get(1000, TimeUnit.SECONDS);</programlisting>
|
||||
<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><emphasis role="bold">Asynchronous Gateway and Reactor Promise</emphasis></para>
|
||||
<para>
|
||||
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
|
||||
<interfacename>reactor.function.Supplier</interfacename> with the provided <code>reactorEnvironment</code>, using
|
||||
a default <interfacename>RingBufferDispatcher</interfacename> for the <classname>Promise</classname>
|
||||
consumption. Note, a <code>reactorEnvironment</code> reference is required whenever a service interface
|
||||
has at least one method with a <classname>Promise<?></classname> return type. (Only those methods run
|
||||
on the reactor's dispatcher).
|
||||
</para>
|
||||
<para>
|
||||
A <classname>Promise</classname> can be used to retrieve the result later (similar to a
|
||||
<classname>Future<?></classname>) or you can consume from it with the dispatcher invoking
|
||||
your <interfacename>Consumer</interfacename> when the result is returned to the gateway.
|
||||
</para>
|
||||
<para>
|
||||
<important>
|
||||
The <classname>Promise</classname> isn't <emphasis>flushed</emphasis> immediately by the framework.
|
||||
Hence the underlying message flow won't be started before the gateway method returns (as it is with
|
||||
<classname>Future<?></classname> <classname>Executor</classname> task).
|
||||
The flow will be started when the <classname>Promise</classname> is <emphasis>flushed</emphasis> or via
|
||||
<code>Promise.await()</code>. Alternatively, the <classname>Promise</classname> (being a
|
||||
<interfacename>Composable</interfacename>) might be a part of Reactor <classname>Stream<?></classname>,
|
||||
when the <code>flush()</code> is related to the entire <classname>Stream</classname>. For example:
|
||||
</important>
|
||||
<programlisting language="java">@MessagingGateway(reactorEnvironment = "reactorEnv")
|
||||
public static interface TestGateway {
|
||||
|
||||
@Gateway(requestChannel = "promiseChannel")
|
||||
Promise<Integer> multiply(Integer value);
|
||||
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
@ServiceActivator(inputChannel = "promiseChannel")
|
||||
public Integer multiply(Integer value) {
|
||||
return value * 2;
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
Streams.defer(Arrays.asList("1", "2", "3", "4", "5"))
|
||||
.env(this.environment)
|
||||
.get()
|
||||
.map(Integer::parseInt)
|
||||
.mapMany(integer -> testGateway.multiply(integer))
|
||||
.collect()
|
||||
.consume(integers -> ...)
|
||||
.flush();</programlisting>
|
||||
</para>
|
||||
<para>
|
||||
Another example is a simple callback scenario:
|
||||
<programlisting language="java"><![CDATA[Promise<Invoice> promise = service.process(myOrder);
|
||||
|
||||
promise.consume(new Consumer<Invoice>() {
|
||||
@Override
|
||||
public void accept(Invoice invoice) {
|
||||
handleInvoice(invoice);
|
||||
}
|
||||
})
|
||||
.flush();]]></programlisting>
|
||||
The calling thread continues, with <code>handleInvoice()</code> being called when the flow completes.
|
||||
</para>
|
||||
</section>
|
||||
<section>
|
||||
<title>Gateway behavior when no response arrives</title>
|
||||
|
||||
@@ -9,6 +9,16 @@
|
||||
in more details, please see the Issue Tracker tickets that
|
||||
were resolved as part of the 4.1 development process.
|
||||
</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>
|
||||
<section id="4.1-general">
|
||||
<title>General Changes</title>
|
||||
<section id="4.1-amqp-inbound-missing-queues">
|
||||
|
||||
Reference in New Issue
Block a user