ReactiveConsumer Improvements

* Add `ConsumerSubscriber` to adapt `Consumer<?>` to the `Subscriber<?>`
* Add `SubscribableChannelPublisherAdapter` to adapt `SubscribableChannel` to `Publisher<?>` via `Flux.create()` on subscription
* Add `PollableChannelPublisherAdapter` to adapt `PollableChannel` to `Publisher<?>` via `Mono.delayMillis()` and `flatMap()` for `channel.receive()`

Use `concatMap()` for `PollableChannel`

Iterate `PollableChannel` until there is a data

Fix Lambda signature error via explicit class declaration:
```
java.lang.ClassFormatError: Duplicate field name&signature in class file org/springframework/integration/endpoint/ReactiveConsumer$PollableChannelPublisherAdapter$1
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at org.springframework.integration.endpoint.ReactiveConsumer$PollableChannelPublisherAdapter.lambda$new$2(ReactiveConsumer.java:163)
```
Looks like Java bug:

Move `Iterator<Message<?>>` instance to the `subscribe()` to avoid race conditions when we have several subscribers.

In other words make `Iterator<Message<?>>` subscriber-specific, but at the same time avoid re-instantiation for each `Flux.concatMap()` call caused by the previous `repeat()`

Upgrade to Reactor 3.0.3 and others

* Make fixes according Reactor 3.0.3 changes
* Remove redundant `TestSubscriber` in favor of `StepVerifier` and `mock(Subscriber)`
* Rework `PublisherIntegrationFlow` Reactive Streams implementation to the out-of-the-box `ReactiveConsumer` and `ReactiveChannel`
This commit is contained in:
Artem Bilan
2016-08-25 12:13:08 -04:00
committed by Gary Russell
parent 8070f72c44
commit ba576bbc0d
9 changed files with 280 additions and 1490 deletions

View File

@@ -116,7 +116,7 @@ subprojects { subproject ->
nettyVersion = '4.1.4.Final'
pahoMqttClientVersion = '1.0.2'
postgresVersion = '9.1-901-1.jdbc4'
reactorVersion = '3.0.0.BUILD-SNAPSHOT'
reactorVersion = '3.0.3.RELEASE'
romeToolsVersion = '1.7.0'
servletApiVersion = '3.1.0'
slf4jVersion = "1.7.21"
@@ -261,7 +261,6 @@ project('spring-integration-test') {
compile "org.springframework:spring-context:$springVersion"
compile "org.springframework:spring-messaging:$springVersion"
compile "org.springframework:spring-test:$springVersion"
compile ("io.projectreactor:reactor-core:$reactorVersion", optional)
compile ("log4j:log4j:$log4jVersion", optional)
}
}
@@ -303,6 +302,7 @@ project('spring-integration-core') {
compile("com.esotericsoftware:kryo-shaded:$kryoShadedVersion", optional)
testCompile ("org.aspectj:aspectjweaver:$aspectjVersion")
compile "io.projectreactor.addons:reactor-test:$reactorVersion"
}
}

View File

@@ -22,7 +22,6 @@ import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -39,7 +38,7 @@ import org.springframework.integration.aggregator.ResequencingMessageHandler;
import org.springframework.integration.channel.ChannelInterceptorAware;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.FixedSubscriberChannel;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.integration.channel.ReactiveChannel;
import org.springframework.integration.channel.interceptor.WireTap;
import org.springframework.integration.config.ConsumerEndpointFactoryBean;
import org.springframework.integration.config.SourcePollingChannelAdapterFactoryBean;
@@ -49,6 +48,7 @@ import org.springframework.integration.dsl.channel.MessageChannelSpec;
import org.springframework.integration.dsl.channel.WireTapSpec;
import org.springframework.integration.dsl.support.FixedSubscriberChannelPrototype;
import org.springframework.integration.dsl.support.MessageChannelReference;
import org.springframework.integration.endpoint.ReactiveConsumer;
import org.springframework.integration.expression.ControlBusMethodFilter;
import org.springframework.integration.expression.FunctionExpression;
import org.springframework.integration.filter.ExpressionEvaluatingSelector;
@@ -89,7 +89,6 @@ import org.springframework.integration.transformer.Transformer;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.PollableChannel;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
@@ -2762,32 +2761,36 @@ public abstract class IntegrationFlowDefinition<B extends IntegrationFlowDefinit
/**
* Represent an Integration Flow as a Reactive Streams {@link Publisher} bean.
* @param <T> the {@code payload} type
* @return the Reactive Streams {@link Publisher}
*/
@SuppressWarnings("unchecked")
public <T> Publisher<Message<T>> toReactivePublisher() {
return toReactivePublisher(Executors.newSingleThreadExecutor());
}
/**
* Represent an Integration Flow as a Reactive Streams {@link Publisher} bean.
* @param executor the managed {@link Executor} to be used for the background task to
* poll messages from the {@link PollableChannel}.
* Defaults to {@link Executors#newSingleThreadExecutor()}.
* @param <T> the {@code payload} type
* @return the Reactive Streams {@link Publisher}
*/
public <T> Publisher<Message<T>> toReactivePublisher(Executor executor) {
Assert.notNull(executor);
MessageChannel channelForPublisher = this.currentMessageChannel;
if (channelForPublisher == null) {
PublishSubscribeChannel publishSubscribeChannel = new PublishSubscribeChannel();
publishSubscribeChannel.setMinSubscribers(1);
channelForPublisher = publishSubscribeChannel;
channel(channelForPublisher);
Publisher<Message<T>> publisher;
if (channelForPublisher instanceof Publisher) {
publisher = (Publisher<Message<T>>) channelForPublisher;
}
else {
MessageChannel reactiveChannel = new ReactiveChannel();
publisher = (Publisher<Message<T>>) reactiveChannel;
if (channelForPublisher != null) {
BridgeHandler bridge = new BridgeHandler();
bridge.setOutputChannel(reactiveChannel);
addComponent(bridge)
.addComponent(new ReactiveConsumer(channelForPublisher, bridge))
.addComponent(reactiveChannel);
}
else {
channel(reactiveChannel);
}
}
get();
return new PublisherIntegrationFlow<T>(this.integrationComponents, channelForPublisher, executor);
return new PublisherIntegrationFlow<T>(this.integrationComponents, publisher);
}
private <S extends ConsumerEndpointSpec<S, ? extends MessageHandler>> B register(S endpointSpec,

View File

@@ -16,24 +16,12 @@
package org.springframework.integration.dsl;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.SubscribableChannel;
/**
*
@@ -45,205 +33,16 @@ import org.springframework.messaging.SubscribableChannel;
*/
class PublisherIntegrationFlow<T> extends StandardIntegrationFlow implements Publisher<Message<T>> {
private static final Subscription NO_OP_SUBSCRIPTION = new Subscription() {
private final Publisher<Message<T>> delegate;
@Override
public void request(long n) {
}
@Override
public void cancel() {
}
};
private final Queue<Subscriber<? super Message<T>>> subscribers = new LinkedBlockingQueue<>();
private final MessageChannel messageChannel;
private final Executor executor;
PublisherIntegrationFlow(Set<Object> integrationComponents, MessageChannel messageChannel, Executor executor) {
PublisherIntegrationFlow(Set<Object> integrationComponents, Publisher<Message<T>> publisher) {
super(integrationComponents);
this.messageChannel = messageChannel;
this.executor = executor;
start();
this.delegate = publisher;
}
@Override
@SuppressWarnings("unchecked")
public void subscribe(Subscriber<? super Message<T>> subscriber) {
if (!isRunning()) {
//Reactive Streams Specification: https://github.com/reactive-streams/reactive-streams-jvm#1.4
subscriber.onSubscribe(NO_OP_SUBSCRIPTION);
subscriber.onError(
new IllegalStateException("The Publisher must be started ('Lifecycle.start()') " +
"before accepting subscription."));
return;
}
this.subscribers.add(subscriber);
if (this.messageChannel instanceof SubscribableChannel) {
subscriber.onSubscribe(new MessageHandlerSubscription((Subscriber<Message<?>>) subscriber));
}
else if (this.messageChannel instanceof PollableChannel) {
subscriber.onSubscribe(new PollableSubscription((Subscriber<Message<?>>) subscriber));
}
else {
//Reactive Streams Specification: https://github.com/reactive-streams/reactive-streams-jvm#1.4
subscriber.onSubscribe(NO_OP_SUBSCRIPTION);
subscriber.onError(
new IllegalStateException("Unsupported MessageChannel type ["
+ this.messageChannel + "]. Must be 'SubscribableChannel' or 'PollableChannel'."));
}
}
@Override
public void stop() {
super.stop();
shutdown();
}
public void shutdown() {
Subscriber<? super Message<T>> subscriber;
while ((subscriber = this.subscribers.poll()) != null) {
subscriber.onComplete();
}
}
private abstract class SubscriberSubscription implements Subscription {
final Subscriber<Message<?>> subscriber;
volatile boolean terminated;
SubscriberSubscription(Subscriber<Message<?>> subscriber) {
this.subscriber = subscriber;
}
@Override
public void request(long n) {
//Reactive Streams Specification: https://github.com/reactive-streams/reactive-streams-jvm#3.9
if (n <= 0L) {
this.subscriber.onError(
new IllegalArgumentException("Spec. Rule 3.9 - " +
"Cannot request a non strictly positive number: " + n));
}
//Reactive Streams Specification: https://github.com/reactive-streams/reactive-streams-jvm#3.6
else if (!this.terminated && isRunning()) {
onRequest(n);
}
}
@Override
public void cancel() {
PublisherIntegrationFlow.this.subscribers.remove(this.subscriber);
this.terminated = true;
}
protected abstract void onRequest(long n);
}
private final class MessageHandlerSubscription extends SubscriberSubscription implements MessageHandler {
private final Queue<Long> pendingRequests = new LinkedBlockingQueue<>();
private final AtomicReference<Long> currentRequest = new AtomicReference<>();
private final AtomicLong count = new AtomicLong();
private volatile boolean unbounded;
MessageHandlerSubscription(Subscriber<Message<?>> subscriber) {
super(subscriber);
}
@Override
public void onRequest(long n) {
if (n == Long.MAX_VALUE) {
this.unbounded = true;
this.pendingRequests.clear();
this.currentRequest.set(null);
this.count.set(0);
}
else if (!this.unbounded) {
if (this.currentRequest.get() != null) {
this.pendingRequests.offer(n);
}
else {
this.currentRequest.set(n);
this.count.set(0);
}
}
((SubscribableChannel) PublisherIntegrationFlow.this.messageChannel).subscribe(this);
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
if (this.terminated || !PublisherIntegrationFlow.this.isRunning()) {
((SubscribableChannel) PublisherIntegrationFlow.this.messageChannel).unsubscribe(this);
throw new MessageDeliveryException(message);
}
if (this.unbounded) {
this.subscriber.onNext(message);
}
else {
if (this.currentRequest.get() == null || this.count.getAndIncrement() == this.currentRequest.get()) {
this.currentRequest.set(this.pendingRequests.poll());
this.count.set(0);
if (this.currentRequest.get() == null) {
((SubscribableChannel) PublisherIntegrationFlow.this.messageChannel).unsubscribe(this);
throw new MessageDeliveryException(message);
}
}
this.subscriber.onNext(message);
}
}
@Override
public void cancel() {
((SubscribableChannel) PublisherIntegrationFlow.this.messageChannel).unsubscribe(this);
super.cancel();
}
}
private final class PollableSubscription extends SubscriberSubscription {
PollableSubscription(Subscriber<Message<?>> subscriber) {
super(subscriber);
}
@Override
public void onRequest(final long n) {
PublisherIntegrationFlow.this.executor.execute(() -> {
if (n == Long.MAX_VALUE) {
while (!terminated && isRunning()) {
Message<?> receive =
((PollableChannel) PublisherIntegrationFlow.this.messageChannel).receive(50);
if (receive != null) {
subscriber.onNext(receive);
}
}
}
else {
long i = 0;
while (!terminated && isRunning() && i < n) {
Message<?> receive =
((PollableChannel) PublisherIntegrationFlow.this.messageChannel).receive(50);
if (receive != null) {
subscriber.onNext(receive);
i++;
}
}
}
});
}
this.delegate.subscribe(subscriber);
}
}

View File

@@ -16,6 +16,7 @@
package org.springframework.integration.endpoint;
import java.util.Iterator;
import java.util.function.Consumer;
import org.reactivestreams.Publisher;
@@ -32,8 +33,14 @@ import org.springframework.messaging.SubscribableChannel;
import org.springframework.util.Assert;
import org.springframework.util.ErrorHandler;
import reactor.core.publisher.DirectProcessor;
import reactor.core.Cancellation;
import reactor.core.Exceptions;
import reactor.core.Receiver;
import reactor.core.Trackable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Operators;
/**
@@ -42,48 +49,43 @@ import reactor.core.publisher.Flux;
*/
public class ReactiveConsumer extends AbstractEndpoint {
private final Subscriber<Message<?>> subscriber;
private final Operators.SubscriberAdapter<Message<?>, Message<?>> subscriber;
private final Consumer<Message<?>> consumer;
private volatile Flux<Message<?>> publisher;
private volatile Subscription subscription;
private volatile Publisher<Message<?>> publisher;
private ErrorHandler errorHandler;
public ReactiveConsumer(MessageChannel inputChannel, Subscriber<Message<?>> subscriber) {
this(inputChannel, subscriber, null);
Assert.notNull(subscriber);
}
public ReactiveConsumer(MessageChannel inputChannel, Consumer<Message<?>> consumer) {
this(inputChannel, null, consumer);
Assert.notNull(consumer);
this(inputChannel, new ConsumerSubscriber(consumer));
}
@SuppressWarnings("unchecked")
private ReactiveConsumer(MessageChannel inputChannel, Subscriber<Message<?>> subscriber,
Consumer<Message<?>> consumer) {
public ReactiveConsumer(MessageChannel inputChannel, Subscriber<Message<?>> subscriber) {
Assert.notNull(inputChannel);
Assert.notNull(subscriber);
Publisher<Message<?>> publisher;
if (inputChannel instanceof Publisher) {
publisher = (Publisher<Message<?>>) inputChannel;
this.publisher = (Publisher<Message<?>>) inputChannel;
}
else {
publisher = adaptToPublisher(inputChannel);
this.publisher = adaptToPublisher(inputChannel);
}
this.publisher = Flux.from(publisher)
.doOnError(t -> this.errorHandler.handleError(t)) // NPE if method reference
.doOnSubscribe(s -> this.subscription = s)
.retry();
this.subscriber = new Operators.SubscriberAdapter<Message<?>, Message<?>>(subscriber) {
this.subscriber = subscriber;
this.consumer = consumer;
@Override
protected void doNext(Message<?> message) {
try {
super.doNext(message);
}
catch (Exception e) {
ReactiveConsumer.this.errorHandler.handleError(e);
doOnSubscriberError(e);
}
}
};
}
public void setErrorHandler(ErrorHandler errorHandler) {
@@ -95,26 +97,18 @@ public class ReactiveConsumer extends AbstractEndpoint {
super.onInit();
if (this.errorHandler == null) {
Assert.notNull(getBeanFactory(), "BeanFactory is required");
this.errorHandler = new MessagePublishingErrorHandler(
new BeanFactoryChannelResolver(getBeanFactory()));
this.errorHandler = new MessagePublishingErrorHandler(new BeanFactoryChannelResolver(getBeanFactory()));
}
}
@Override
protected void doStart() {
if (this.subscriber != null) {
this.publisher.subscribe(this.subscriber);
}
else {
this.publisher.subscribe(this.consumer);
}
this.publisher.subscribe(this.subscriber);
}
@Override
protected void doStop() {
if (this.subscription != null) {
this.subscription.cancel();
}
this.subscriber.cancel();
}
private Publisher<Message<?>> adaptToPublisher(MessageChannel inputChannel) {
@@ -135,65 +129,137 @@ public class ReactiveConsumer extends AbstractEndpoint {
}
private Publisher<Message<?>> adaptPollableChannelToPublisher(PollableChannel inputChannel) {
return null;
return new PollableChannelPublisherAdapter(inputChannel);
}
private final static class SubscribableChannelPublisherAdapter
implements Publisher<Message<?>>, Subscriber<Message<?>>, Subscription {
private final DirectProcessor<Message<?>> delegate = DirectProcessor.create();
private final MessageHandler subscriberAdapter = this.delegate.connectSink()::accept;
private final static class SubscribableChannelPublisherAdapter implements Publisher<Message<?>> {
private final SubscribableChannel channel;
private Subscriber<? super Message<?>> actualSubscriber;
private Subscription actualSubscription;
SubscribableChannelPublisherAdapter(SubscribableChannel channel) {
this.channel = channel;
}
@Override
public void subscribe(Subscriber<? super Message<?>> subscriber) {
this.actualSubscriber = subscriber;
this.delegate.subscribe(this);
this.channel.subscribe(this.subscriberAdapter);
Flux.
<Message<?>>create(emitter -> {
MessageHandler messageHandler = emitter::next;
this.channel.subscribe(messageHandler);
emitter.setCancellation(() -> this.channel.unsubscribe(messageHandler));
},
FluxSink.OverflowStrategy.IGNORE)
.subscribe(subscriber);
}
}
private final static class PollableChannelPublisherAdapter implements Publisher<Message<?>> {
private final PollableChannel channel;
PollableChannelPublisherAdapter(final PollableChannel channel) {
this.channel = channel;
}
@Override
public void onSubscribe(Subscription subscription) {
this.actualSubscription = subscription;
this.actualSubscriber.onSubscribe(this);
public void subscribe(Subscriber<? super Message<?>> subscriber) {
Iterator<Message<?>> messageIterator = new Iterator<Message<?>>() {
private Message<?> next = null;
@Override
public Message<?> next() {
Message<?> message = this.next;
this.next = null;
return message;
}
@Override
public boolean hasNext() {
if (this.next == null) {
this.next = PollableChannelPublisherAdapter.this.channel.receive(0);
}
return this.next != null;
}
};
Mono.<Message<?>>delayMillis(100)
.repeat()
.concatMap(value -> Flux.fromIterable(() -> messageIterator))
.subscribe(subscriber);
}
}
private static final class ConsumerSubscriber implements Subscriber<Message<?>>, Receiver, Cancellation, Trackable {
private final Consumer<Message<?>> consumer;
private Subscription subscription;
ConsumerSubscriber(Consumer<Message<?>> consumer) {
Assert.notNull(consumer);
this.consumer = consumer;
}
@Override
public void onSubscribe(Subscription s) {
this.subscription = s;
s.request(Long.MAX_VALUE);
}
@Override
public void onNext(Message<?> message) {
this.actualSubscriber.onNext(message);
this.consumer.accept(message);
}
@Override
public void onError(Throwable t) {
this.actualSubscriber.onError(t);
if (t == null) {
throw Exceptions.argumentIsNullException();
}
onComplete();
Operators.onErrorDropped(t);
}
@Override
public void onComplete() {
this.actualSubscriber.onComplete();
if (this.subscription != null) {
this.subscription = null;
}
}
@Override
public void request(long n) {
this.actualSubscription.request(n);
public Object upstream() {
return this.subscription;
}
@Override
public void cancel() {
this.channel.unsubscribe(this.subscriberAdapter);
this.actualSubscription.cancel();
public void dispose() {
Subscription s = this.subscription;
if (s != null) {
this.subscription = null;
s.cancel();
}
}
@Override
public long getCapacity() {
return Long.MAX_VALUE;
}
@Override
public boolean isStarted() {
return this.subscription != null;
}
@Override
public boolean isTerminated() {
return false;
}
}

View File

@@ -37,7 +37,7 @@ import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
import reactor.core.Exceptions;
import reactor.core.publisher.Operators;
/**
* Base class for MessageHandler implementations that provides basic validation
@@ -159,11 +159,7 @@ public abstract class AbstractMessageHandler extends IntegrationObjectSupport im
@Override
public void onError(Throwable throwable) {
Exceptions.throwIfFatal(throwable);
if (throwable instanceof MessagingException) {
throw (MessagingException) throwable;
}
throw new MessagingException("Error occurred in message handler [" + this + "]", throwable);
Operators.onErrorDropped(throwable);
}
@Override

View File

@@ -16,6 +16,8 @@
package org.springframework.integration.channel.reactive;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.isOneOf;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
@@ -32,6 +34,7 @@ import org.springframework.integration.channel.ReactiveChannel;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
@@ -53,13 +56,20 @@ public class ReactiveChannelTests {
QueueChannel replyChannel = new QueueChannel();
for (int i = 0; i < 10; i++) {
this.reactiveChannel.send(MessageBuilder.withPayload(i).setReplyChannel(replyChannel).build());
try {
this.reactiveChannel.send(MessageBuilder.withPayload(i).setReplyChannel(replyChannel).build());
}
catch (Exception e) {
assertThat(e.getCause(), instanceOf(MessageHandlingException.class));
assertThat(e.getCause().getCause(), instanceOf(IllegalStateException.class));
assertThat(e.getMessage(), containsString("intentional"));
}
}
for (int i = 0; i < 10; i++) {
for (int i = 0; i < 9; i++) {
Message<?> receive = replyChannel.receive(10000);
assertNotNull(receive);
assertThat(receive.getPayload(), isOneOf("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"));
assertThat(receive.getPayload(), isOneOf("0", "1", "2", "3", "4", "6", "7", "8", "9"));
}
}
@@ -74,10 +84,9 @@ public class ReactiveChannelTests {
@ServiceActivator(inputChannel = "reactiveChannel")
public String handle(int payload) {
/* TODO doesn't work yet
if (payload == 5) {
throw new IllegalStateException("intentional");
}*/
}
return "" + payload;
}

View File

@@ -18,29 +18,38 @@ package org.springframework.integration.channel.reactive;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.channel.ReactiveChannel;
import org.springframework.integration.config.ConsumerEndpointFactoryBean;
import org.springframework.integration.endpoint.ReactiveConsumer;
import org.springframework.integration.handler.MethodInvokingMessageHandler;
import org.springframework.integration.test.reactive.TestSubscriber;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.MessageHandler;
@@ -91,10 +100,20 @@ public class ReactiveConsumerTests {
@Test
public void testReactiveConsumerDirectChannel() {
@SuppressWarnings("unchecked")
public void testReactiveConsumerDirectChannel() throws InterruptedException {
DirectChannel testChannel = new DirectChannel();
TestSubscriber<Message<?>> testSubscriber = TestSubscriber.create();
Subscriber<Message<?>> testSubscriber = (Subscriber<Message<?>>) Mockito.mock(Subscriber.class);
BlockingQueue<Message<?>> messages = new LinkedBlockingQueue<>();
willAnswer(i -> {
messages.put(i.getArgumentAt(0, Message.class));
return null;
})
.given(testSubscriber)
.onNext(any(Message.class));
ReactiveConsumer reactiveConsumer = new ReactiveConsumer(testChannel, testSubscriber);
reactiveConsumer.setBeanFactory(mock(BeanFactory.class));
@@ -104,11 +123,14 @@ public class ReactiveConsumerTests {
Message<?> testMessage = new GenericMessage<>("test");
testChannel.send(testMessage);
testSubscriber.assertSubscribed();
testSubscriber.assertNoError();
testSubscriber.assertNotComplete();
ArgumentCaptor<Subscription> subscriptionArgumentCaptor = ArgumentCaptor.forClass(Subscription.class);
verify(testSubscriber).onSubscribe(subscriptionArgumentCaptor.capture());
Subscription subscription = subscriptionArgumentCaptor.getValue();
testSubscriber.assertValues(testMessage);
subscription.request(1);
Message<?> message = messages.poll(10, TimeUnit.SECONDS);
assertSame(testMessage, message);
reactiveConsumer.stop();
@@ -120,18 +142,79 @@ public class ReactiveConsumerTests {
assertThat(e, instanceOf(MessageDeliveryException.class));
}
new DirectFieldAccessor(testSubscriber).setPropertyValue("s", null);
TestUtils.getPropertyValue(testSubscriber, "values", List.class).clear();
reactiveConsumer.start();
testSubscriber.request(1);
subscription.request(1);
testMessage = new GenericMessage<>("test2");
testChannel.send(testMessage);
testSubscriber.assertValues(testMessage);
message = messages.poll(10, TimeUnit.SECONDS);
assertSame(testMessage, message);
verify(testSubscriber, never()).onError(any(Throwable.class));
verify(testSubscriber, never()).onComplete();
assertTrue(messages.isEmpty());
}
@Test
@SuppressWarnings("unchecked")
public void testReactiveConsumerPollableChannel() throws InterruptedException {
QueueChannel testChannel = new QueueChannel();
Subscriber<Message<?>> testSubscriber = (Subscriber<Message<?>>) Mockito.mock(Subscriber.class);
BlockingQueue<Message<?>> messages = new LinkedBlockingQueue<>();
willAnswer(i -> {
messages.put(i.getArgumentAt(0, Message.class));
return null;
})
.given(testSubscriber)
.onNext(any(Message.class));
ReactiveConsumer reactiveConsumer = new ReactiveConsumer(testChannel, testSubscriber);
reactiveConsumer.setBeanFactory(mock(BeanFactory.class));
reactiveConsumer.afterPropertiesSet();
reactiveConsumer.start();
Message<?> testMessage = new GenericMessage<>("test");
testChannel.send(testMessage);
ArgumentCaptor<Subscription> subscriptionArgumentCaptor = ArgumentCaptor.forClass(Subscription.class);
verify(testSubscriber).onSubscribe(subscriptionArgumentCaptor.capture());
Subscription subscription = subscriptionArgumentCaptor.getValue();
subscription.request(1);
Message<?> message = messages.poll(10, TimeUnit.SECONDS);
assertSame(testMessage, message);
reactiveConsumer.stop();
testChannel.send(testMessage);
reactiveConsumer.start();
subscription.request(2);
Message<?> testMessage2 = new GenericMessage<>("test2");
testChannel.send(testMessage2);
message = messages.poll(10, TimeUnit.SECONDS);
assertSame(testMessage, message);
message = messages.poll(10, TimeUnit.SECONDS);
assertSame(testMessage2, message);
verify(testSubscriber, never()).onError(any(Throwable.class));
verify(testSubscriber, never()).onComplete();
assertTrue(messages.isEmpty());
}
@Test

View File

@@ -33,9 +33,7 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.reactivestreams.Publisher;
@@ -100,7 +98,6 @@ public class ReactiveStreamsTests {
}
@Test
@Ignore("Until Reactor 3.0.x solution")
public void testPollableReactiveFlow() throws InterruptedException, TimeoutException, ExecutionException {
this.inputChannel.send(new GenericMessage<>("1,2,3,4,5"));
@@ -109,19 +106,20 @@ public class ReactiveStreamsTests {
Flux.from(this.pollablePublisher)
.filter(m -> m.getHeaders().containsKey(IntegrationMessageHeaderAccessor.SEQUENCE_NUMBER))
.doOnNext(p -> latch.countDown())
.subscribe(6);
.take(6)
.subscribe();
Future<List<Integer>> future =
Executors.newSingleThreadExecutor().submit(() ->
Flux.fromArray(new String[] { "11,12,13" })
Flux.just("11,12,13")
.map(v -> v.split(","))
.map(Arrays::asList)
.flatMapIterable(data -> data)
.flatMapIterable(Arrays::asList)
.map(Integer::parseInt)
.<Message<Integer>>map(GenericMessage<Integer>::new)
.concatWith(this.pollablePublisher)
.map(Message::getPayload)
.collect(Collectors.toList())
.take(7)
.collectList()
.block(Duration.ofSeconds(5)));
this.inputChannel.send(new GenericMessage<>("6,7,8,9,10"));