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:
Artem Bilan
2021-03-08 12:02:31 -05:00
committed by GitHub
parent f154088935
commit e9f234683e
23 changed files with 449 additions and 99 deletions

View File

@@ -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);
}
}

View File

@@ -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");

View File

@@ -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

View File

@@ -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();