INT-4444: Introduce @Reactive & reactive() (#3503)
* INT-4444: Introduce `@Reactive` & `reactive()` JIRA: https://jira.spring.io/browse/INT-4444 Right now the high-level API creates a `ReactiveStreamsConsumer` only when the input channel is a `Publisher<?>` impl or target handler is a `ReactiveMessageHandler` * Add `@Reactive[] reactive()` attribute to messaging annotations * Add `ConsumerEndpointSpec.reactive()` Both options point to the same `ConsumerEndpointFactoryBean.setReactiveCustomizer()` making the target endpoint always as a `ReactiveStreamsConsumer` independently of the input channel and target handler * Use the `Function` to customize a source `Flux` from the channel * Test and document a new feature * * Fix links in docs * * Fix `ReactiveStreamsTests` * * Rework `reactive()` attribute of messaging annotations ot a single `@Reactive` value with default as `@Reactive(ValueConstants.DEFAULT_NONE)` * Fix language in docs * Fix `MessagingAnnotationUtils.resolveAttribute()` to use `requiredType.isInstance()` instead of comparing classes since annotation instances are `Proxy` at runtime
This commit is contained in:
@@ -33,6 +33,7 @@ import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -336,4 +337,36 @@ public class ReactiveStreamsConsumerTests {
|
||||
testChannel.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReactiveCustomizer() throws Exception {
|
||||
DirectChannel testChannel = new DirectChannel();
|
||||
|
||||
AtomicReference<Message<?>> spied = new AtomicReference<>();
|
||||
AtomicReference<Message<?>> result = new AtomicReference<>();
|
||||
CountDownLatch stopLatch = new CountDownLatch(1);
|
||||
|
||||
MessageHandler messageHandler = m -> {
|
||||
result.set(m);
|
||||
stopLatch.countDown();
|
||||
};
|
||||
|
||||
ConsumerEndpointFactoryBean endpointFactoryBean = new ConsumerEndpointFactoryBean();
|
||||
endpointFactoryBean.setBeanFactory(mock(ConfigurableBeanFactory.class));
|
||||
endpointFactoryBean.setInputChannel(testChannel);
|
||||
endpointFactoryBean.setHandler(messageHandler);
|
||||
endpointFactoryBean.setBeanName("reactiveConsumer");
|
||||
endpointFactoryBean.setReactiveCustomizer(flux -> flux.doOnNext(spied::set));
|
||||
endpointFactoryBean.afterPropertiesSet();
|
||||
endpointFactoryBean.start();
|
||||
|
||||
Message<?> testMessage = new GenericMessage<>("test");
|
||||
testChannel.send(testMessage);
|
||||
|
||||
assertThat(stopLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
endpointFactoryBean.stop();
|
||||
|
||||
assertThat(result.get()).isSameAs(testMessage);
|
||||
assertThat(spied.get()).isSameAs(testMessage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2020 the original author or authors.
|
||||
* Copyright 2014-2021 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.
|
||||
@@ -21,6 +21,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
@@ -48,6 +50,7 @@ import org.springframework.integration.annotation.BridgeTo;
|
||||
import org.springframework.integration.annotation.Filter;
|
||||
import org.springframework.integration.annotation.InboundChannelAdapter;
|
||||
import org.springframework.integration.annotation.Poller;
|
||||
import org.springframework.integration.annotation.Reactive;
|
||||
import org.springframework.integration.annotation.Router;
|
||||
import org.springframework.integration.annotation.ServiceActivator;
|
||||
import org.springframework.integration.annotation.Splitter;
|
||||
@@ -81,6 +84,7 @@ import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.Sinks;
|
||||
import reactor.test.StepVerifier;
|
||||
@@ -145,10 +149,12 @@ public class MessagingAnnotationsWithBeanAnnotationTests {
|
||||
@Autowired
|
||||
private MessageChannel messageConsumerServiceChannel;
|
||||
|
||||
@Autowired
|
||||
private CountDownLatch reactiveCustomizerLatch;
|
||||
|
||||
@Test
|
||||
public void testMessagingAnnotationsFlow() {
|
||||
public void testMessagingAnnotationsFlow() throws InterruptedException {
|
||||
Stream.of(this.sourcePollingChannelAdapters).forEach(AbstractEndpoint::start);
|
||||
//this.sourcePollingChannelAdapter.start();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
Message<?> receive = this.discardChannel.receive(10000);
|
||||
assertThat(receive).isNotNull();
|
||||
@@ -164,6 +170,9 @@ public class MessagingAnnotationsWithBeanAnnotationTests {
|
||||
"'messagingAnnotationsWithBeanAnnotationTests.ContextConfiguration.filter.filter.handler'");
|
||||
|
||||
}
|
||||
|
||||
assertThat(reactiveCustomizerLatch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
for (Message<?> message : this.collector) {
|
||||
assertThat(((Integer) message.getPayload()) % 2).isNotEqualTo(0);
|
||||
MessageHistory messageHistory = MessageHistory.read(message);
|
||||
@@ -336,7 +345,17 @@ public class MessagingAnnotationsWithBeanAnnotationTests {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Splitter(inputChannel = "splitterChannel")
|
||||
public CountDownLatch reactiveCustomizerLatch() {
|
||||
return new CountDownLatch(10);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Flux<?>, Flux<?>> reactiveCustomizer(CountDownLatch reactiveCustomizerLatch) {
|
||||
return flux -> flux.doOnNext(data -> reactiveCustomizerLatch.countDown());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Splitter(inputChannel = "splitterChannel", reactive = @Reactive("reactiveCustomizer"))
|
||||
public MessageHandler splitter() {
|
||||
DefaultMessageSplitter defaultMessageSplitter = new DefaultMessageSplitter();
|
||||
defaultMessageSplitter.setOutputChannelName("serviceChannel");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2020 the original author or authors.
|
||||
* Copyright 2014-2021 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.
|
||||
@@ -80,6 +80,7 @@ import org.springframework.integration.annotation.IntegrationComponentScan;
|
||||
import org.springframework.integration.annotation.MessagingGateway;
|
||||
import org.springframework.integration.annotation.Poller;
|
||||
import org.springframework.integration.annotation.Publisher;
|
||||
import org.springframework.integration.annotation.Reactive;
|
||||
import org.springframework.integration.annotation.Role;
|
||||
import org.springframework.integration.annotation.ServiceActivator;
|
||||
import org.springframework.integration.annotation.Transformer;
|
||||
@@ -105,6 +106,7 @@ import org.springframework.integration.endpoint.AbstractEndpoint;
|
||||
import org.springframework.integration.endpoint.EventDrivenConsumer;
|
||||
import org.springframework.integration.endpoint.MethodInvokingMessageSource;
|
||||
import org.springframework.integration.endpoint.PollingConsumer;
|
||||
import org.springframework.integration.endpoint.ReactiveStreamsConsumer;
|
||||
import org.springframework.integration.expression.SpelPropertyAccessorRegistrar;
|
||||
import org.springframework.integration.gateway.GatewayProxyFactoryBean;
|
||||
import org.springframework.integration.handler.ServiceActivatingHandler;
|
||||
@@ -270,6 +272,9 @@ public class EnableIntegrationTests {
|
||||
@Autowired
|
||||
private MessageChannel bridgeToInput;
|
||||
|
||||
@Autowired
|
||||
private AbstractEndpoint reactiveBridge;
|
||||
|
||||
@Autowired
|
||||
private PollableChannel bridgeToOutput;
|
||||
|
||||
@@ -655,6 +660,7 @@ public class EnableIntegrationTests {
|
||||
assertThat(testMessage).isSameAs(receive);
|
||||
assertThat(this.metaBridgeOutput.receive(10)).isNull();
|
||||
|
||||
assertThat(this.reactiveBridge).isInstanceOf(ReactiveStreamsConsumer.class);
|
||||
this.bridgeToInput.send(testMessage);
|
||||
receive = this.bridgeToOutput.receive(10_000);
|
||||
assertThat(receive).isNotNull();
|
||||
@@ -881,7 +887,7 @@ public class EnableIntegrationTests {
|
||||
@Bean
|
||||
@GlobalChannelInterceptor
|
||||
public FactoryBean<ChannelInterceptor> ciFactoryBean() {
|
||||
return new AbstractFactoryBean<ChannelInterceptor>() {
|
||||
return new AbstractFactoryBean<>() {
|
||||
|
||||
@Override
|
||||
public Class<?> getObjectType() {
|
||||
@@ -889,7 +895,7 @@ public class EnableIntegrationTests {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ChannelInterceptor createInstance() throws Exception {
|
||||
protected ChannelInterceptor createInstance() {
|
||||
return new ChannelInterceptor() {
|
||||
|
||||
@Override
|
||||
@@ -933,7 +939,8 @@ public class EnableIntegrationTests {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@BridgeTo("bridgeToOutput")
|
||||
@BridgeTo(value = "bridgeToOutput", reactive = @Reactive)
|
||||
@EndpointId("reactiveBridge")
|
||||
public MessageChannel bridgeToInput() {
|
||||
return new DirectChannel();
|
||||
}
|
||||
@@ -1320,7 +1327,7 @@ public class EnableIntegrationTests {
|
||||
assertThat(message.getHeaders().get("foo")).isEqualTo("FOO");
|
||||
assertThat(message.getHeaders()).containsKey("calledMethod");
|
||||
assertThat(message.getHeaders().get("calledMethod")).isEqualTo("echo");
|
||||
return this.handle(message.getPayload()) + Arrays.asList(new Throwable().getStackTrace()).toString();
|
||||
return handle(message.getPayload()) + Arrays.asList(new Throwable().getStackTrace()).toString();
|
||||
}
|
||||
|
||||
@Transformer(inputChannel = "gatewayChannel2")
|
||||
@@ -1330,7 +1337,7 @@ public class EnableIntegrationTests {
|
||||
assertThat(message.getHeaders().get("foo")).isEqualTo("FOO");
|
||||
assertThat(message.getHeaders()).containsKey("calledMethod");
|
||||
assertThat(message.getHeaders().get("calledMethod")).isEqualTo("echo2");
|
||||
return this.handle(message.getPayload()) + "2" + Arrays.asList(new Throwable().getStackTrace()).toString();
|
||||
return handle(message.getPayload()) + "2" + Arrays.asList(new Throwable().getStackTrace()).toString();
|
||||
}
|
||||
|
||||
@MyInboundChannelAdapter1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
* Copyright 2016-2021 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.
|
||||
@@ -46,6 +46,8 @@ import org.springframework.integration.dsl.IntegrationFlow;
|
||||
import org.springframework.integration.dsl.IntegrationFlows;
|
||||
import org.springframework.integration.dsl.MessageChannels;
|
||||
import org.springframework.integration.dsl.context.IntegrationFlowContext;
|
||||
import org.springframework.integration.endpoint.AbstractEndpoint;
|
||||
import org.springframework.integration.endpoint.ReactiveStreamsConsumer;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
@@ -53,6 +55,7 @@ import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
|
||||
/**
|
||||
@@ -73,6 +76,9 @@ public class ReactiveStreamsTests {
|
||||
@Qualifier("pollableReactiveFlow")
|
||||
private Publisher<Message<Integer>> pollablePublisher;
|
||||
|
||||
@Autowired
|
||||
private AbstractEndpoint reactiveTransformer;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("reactiveStreamsMessageSource")
|
||||
private Lifecycle messageSource;
|
||||
@@ -109,12 +115,13 @@ public class ReactiveStreamsTests {
|
||||
this.messageSource.start();
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
String[] strings = results.toArray(new String[0]);
|
||||
assertThat(strings).isEqualTo(new String[] { "A", "B", "C", "D", "E", "F" });
|
||||
assertThat(strings).isEqualTo(new String[]{ "A", "B", "C", "D", "E", "F" });
|
||||
this.messageSource.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPollableReactiveFlow() throws Exception {
|
||||
assertThat(this.reactiveTransformer).isInstanceOf(ReactiveStreamsConsumer.class);
|
||||
this.inputChannel.send(new GenericMessage<>("1,2,3,4,5"));
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(6);
|
||||
@@ -216,9 +223,7 @@ public class ReactiveStreamsTests {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Flux.from(this.singleChannelFlow)
|
||||
.map(m -> m.getPayload().toUpperCase())
|
||||
.subscribe(p -> {
|
||||
latch.countDown();
|
||||
});
|
||||
.subscribe(p -> latch.countDown());
|
||||
this.singleChannel.send(new GenericMessage<>("foo"));
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
@@ -228,9 +233,7 @@ public class ReactiveStreamsTests {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Flux.from(this.fixedSubscriberChannelFlow)
|
||||
.map(m -> m.getPayload().toUpperCase())
|
||||
.subscribe(p -> {
|
||||
latch.countDown();
|
||||
});
|
||||
.subscribe(p -> latch.countDown());
|
||||
this.fixedSubscriberChannel.send(new GenericMessage<>("bar"));
|
||||
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
@@ -258,7 +261,8 @@ public class ReactiveStreamsTests {
|
||||
return IntegrationFlows
|
||||
.from("inputChannel")
|
||||
.split(s -> s.delimiters(","))
|
||||
.<String, Integer>transform(Integer::parseInt)
|
||||
.<String, Integer>transform(Integer::parseInt,
|
||||
e -> e.reactive(flux -> flux.publishOn(Schedulers.parallel())).id("reactiveTransformer"))
|
||||
.channel(MessageChannels.queue())
|
||||
.log()
|
||||
.toReactivePublisher();
|
||||
|
||||
Reference in New Issue
Block a user