Infrastructure for ReactiveMessageHandler (#3137)

* Infrastructure for ReactiveMessageHandler

We have now a `ReactiveMongoDbStoringMessageHandler` which implements
a `ReactiveMessageHandler`, but not a `MessageHandler` for possible
deferred subscriptions to the returned Reactor type

We don't have a proper application context processing for this
new type of message handlers

* Change a  `ConsumerEndpointFactoryBean` to apply an `MH` and `RMH`
as possible types for handler
* Introduce a `ReactiveMessageHandlerAdapter` to wrap an `RMH`
into a `MH` for synchronous calls in the regular consumer endpoints
* Wrap an `RMH` into a `ReactiveMessageHandlerAdapter` for regular
endpoints and unwrap for `ReactiveStreamsConsumer`
* Add `RMH`-based ctor into `ReactiveStreamsConsumer` for target
reactive streams composition (`flatMap()` on the `RMH`)
* Remove a `DelegatingSubscriber` from the `ReactiveStreamsConsumer`
in favor of direct calls from the `doOnSubscribe()`, `doOnComplete()`
& `doOnNext()`
* Add an `onErrorContinue()` to handle per-message errors, but don't
cancel the whole source `Publisher`
* Use `Disposable` from the `subscribe()` to cancel in the `stop()`
- recommended way in Reactor
* Use `onErrorContinue()` in the `FluxMessageChannel` instead of
`try..catch` in the `doOnNext()` - for possible `onErrorStop()`
in the provided upstream `Publisher`
* Handle `RMH` in the `ServiceActivatorFactoryBean` as a direct handler
as well with wrapping into `ReactiveMessageHandlerAdapter` for return.
The `ConsumerEndpointFactoryBean` extracts an `RMH` from the adapter
for the `ReactiveStreamsConsumer` anyway
* Add XML parsing test for `ReactiveMongoDbStoringMessageHandler`
* Add `log4j-slf4j-impl` for all the test runtime since `slf4j-api`
comes as a transitive dependency from many places

* * Fix conflicts after rebasing to master

* * Fix typo in warn message
* Change `Assert.state()` to `Assert.isTrue()`
for `ConsumerEndpointFactoryBean.setHandler()`

* * Fix `ConsumerEndpointFactoryBean` when reactive and no advice-chain
* Fix race condition in the
`ReactiveMongoDbStoringMessageHandlerTests.testReactiveMongoMessageHandlerFromApplicationContext()`

* * Handle `ReactiveMessageHandler` in Java DSL.
Essentially request a wrapping into `ReactiveMessageHandlerAdapter`.
Describe such a requirements in the `ReactiveMessageHandlerAdapter` JavaDocs
* Some Java DSL test polishing
* Add Java DSL for `ReactiveMongoDbStoringMessageHandler`
* Propagate missed `ApplicationContext` population into an internally
created `ReactiveMongoTemplate` in the `ReactiveMongoDbStoringMessageHandler`
This commit is contained in:
Artem Bilan
2020-01-13 08:41:41 -05:00
committed by Gary Russell
parent e0509cc339
commit d13752b40b
18 changed files with 501 additions and 131 deletions

View File

@@ -17,7 +17,9 @@
package org.springframework.integration.mongodb.dsl;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.convert.MongoConverter;
/**
@@ -52,6 +54,30 @@ public final class MongoDb {
return new MongoDbOutboundGatewaySpec(mongoTemplate);
}
/**
* Create a {@link ReactiveMongoDbMessageHandlerSpec} builder instance
* based on the provided {@link ReactiveMongoDatabaseFactory}.
* @param mongoDbFactory the {@link ReactiveMongoDatabaseFactory} to use.
* @return the {@link MongoDbOutboundGatewaySpec} instance
* @since 5.3
*/
public static ReactiveMongoDbMessageHandlerSpec reactiveOutboundChannelAdapter(
ReactiveMongoDatabaseFactory mongoDbFactory) {
return new ReactiveMongoDbMessageHandlerSpec(mongoDbFactory);
}
/**
* Create a {@link ReactiveMongoDbMessageHandlerSpec} builder instance
* based on the provided {@link ReactiveMongoOperations}.
* @param mongoTemplate the {@link ReactiveMongoOperations} to use.
* @return the {@link ReactiveMongoDbMessageHandlerSpec} instance
* @since 5.3
*/
public static ReactiveMongoDbMessageHandlerSpec reactiveOutboundChannelAdapter(ReactiveMongoOperations mongoTemplate) {
return new ReactiveMongoDbMessageHandlerSpec(mongoTemplate);
}
private MongoDb() {
}

View File

@@ -32,7 +32,7 @@ import org.springframework.messaging.Message;
/**
* A {@link MessageHandlerSpec} extension for the MongoDb Outbound endpoint {@link MongoDbOutboundGateway}
*
* @author Xavier Padr?
* @author Xavier Padro
* @author Artem Bilan
*
* @since 5.0

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.mongodb.dsl;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.dsl.ComponentsRegistration;
import org.springframework.integration.dsl.MessageHandlerSpec;
import org.springframework.integration.expression.FunctionExpression;
import org.springframework.integration.handler.ReactiveMessageHandlerAdapter;
import org.springframework.integration.mongodb.outbound.ReactiveMongoDbStoringMessageHandler;
import org.springframework.messaging.Message;
/**
* A {@link MessageHandlerSpec} extension for the Reactive MongoDb Outbound endpoint
* {@link ReactiveMongoDbStoringMessageHandler}.
*
* @author Artem Bilan
*
* @since 5.3
*/
public class ReactiveMongoDbMessageHandlerSpec
extends MessageHandlerSpec<ReactiveMongoDbMessageHandlerSpec, ReactiveMessageHandlerAdapter>
implements ComponentsRegistration {
private final ReactiveMongoDbStoringMessageHandler messageHandler;
ReactiveMongoDbMessageHandlerSpec(ReactiveMongoDatabaseFactory mongoDbFactory) {
this(new ReactiveMongoDbStoringMessageHandler(mongoDbFactory));
}
ReactiveMongoDbMessageHandlerSpec(ReactiveMongoOperations reactiveMongoOperations) {
this(new ReactiveMongoDbStoringMessageHandler(reactiveMongoOperations));
}
private ReactiveMongoDbMessageHandlerSpec(ReactiveMongoDbStoringMessageHandler messageHandler) {
this.messageHandler = messageHandler;
this.target = new ReactiveMessageHandlerAdapter(this.messageHandler);
}
/**
* Configure a {@link MongoConverter}.
* @param mongoConverter the {@link MongoConverter} to use.
* @return the spec
*/
public ReactiveMongoDbMessageHandlerSpec mongoConverter(MongoConverter mongoConverter) {
this.messageHandler.setMongoConverter(mongoConverter);
return this;
}
/**
* Configure a collection name to store data.
* @param collectionName the explicit collection name to use.
* @return the spec
*/
public ReactiveMongoDbMessageHandlerSpec collectionName(String collectionName) {
return collectionNameExpression(new LiteralExpression(collectionName));
}
/**
* Configure a {@link Function} for evaluation a collection against request message.
* @param collectionNameFunction the {@link Function} to determine a collection name at runtime.
* @param <P> an expected payload type
* @return the spec
*/
public <P> ReactiveMongoDbMessageHandlerSpec collectionNameFunction(
Function<Message<P>, String> collectionNameFunction) {
return collectionNameExpression(new FunctionExpression<>(collectionNameFunction));
}
/**
* Configure a SpEL expression to evaluate a collection name against a request message.
* @param collectionNameExpression the SpEL expression to use.
* @return the spec
*/
public ReactiveMongoDbMessageHandlerSpec collectionNameExpression(Expression collectionNameExpression) {
this.messageHandler.setCollectionNameExpression(collectionNameExpression);
return this;
}
@Override
public Map<Object, String> getComponentsToRegister() {
return Collections.singletonMap(this.messageHandler, null);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2019 the original author or authors.
* Copyright 2019-2020 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.
@@ -36,6 +36,7 @@ import reactor.core.publisher.Mono;
* collection is identified by evaluation of the {@link #collectionNameExpression}.
*
* @author David Turanski
* @author Artme Bilan
*
* @since 5.3
*/
@@ -104,7 +105,9 @@ public class ReactiveMongoDbStoringMessageHandler extends AbstractReactiveMessag
super.onInit();
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
if (this.mongoTemplate == null) {
this.mongoTemplate = new ReactiveMongoTemplate(this.mongoDbFactory, this.mongoConverter);
ReactiveMongoTemplate mongoTemplate = new ReactiveMongoTemplate(this.mongoDbFactory, this.mongoConverter);
mongoTemplate.setApplicationContext(getApplicationContext());
this.mongoTemplate = mongoTemplate;
}
this.initialized = true;
}

View File

@@ -17,7 +17,9 @@
package org.springframework.integration.mongodb.dsl;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
@@ -34,8 +36,10 @@ import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.BulkOperations;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.integration.config.EnableIntegration;
@@ -255,6 +259,25 @@ public class MongoDbTests extends MongoDbAvailableTests {
bulkOperations.execute();
}
@Autowired
@Qualifier("reactiveStore.input")
private MessageChannel reactiveStoreInput;
@Test
@MongoDbAvailable
public void testReactiveMongoDbMessageHandler() {
this.reactiveStoreInput.send(MessageBuilder.withPayload(createPerson("Bob")).build());
ReactiveMongoTemplate reactiveMongoTemplate = new ReactiveMongoTemplate(REACTIVE_MONGO_DATABASE_FACTORY);
await().untilAsserted(() ->
assertThat(
reactiveMongoTemplate.findOne(new BasicQuery("{'name' : 'Bob'}"), Person.class, "data")
.block(Duration.ofSeconds(10)))
.isNotNull()
.extracting("name", "address.state").contains("Bob", "PA"));
}
@Configuration
@EnableIntegration
public static class ContextConfiguration {
@@ -395,6 +418,14 @@ public class MongoDbTests extends MongoDbAvailableTests {
.entityClass(Person.class);
}
@Bean
public IntegrationFlow reactiveStore() {
return f -> f
.channel(MessageChannels.flux())
.handle(MongoDb.reactiveOutboundChannelAdapter(REACTIVE_MONGO_DATABASE_FACTORY));
}
}
}

View File

@@ -39,7 +39,9 @@ import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @author Xavier Padr?
* @author Xavier Padro
* @author Artem Bilan
*
* @since 5.0
*/
@RunWith(SpringRunner.class)
@@ -52,7 +54,7 @@ public class MongoDbOutboundGatewayXmlTests extends MongoDbAvailableTests {
private ApplicationContext context;
@Before
public void setUp() throws Exception {
public void setUp() {
MongoDbFactory mongoDbFactory = this.prepareMongoFactory();
MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory);
@@ -63,7 +65,7 @@ public class MongoDbOutboundGatewayXmlTests extends MongoDbAvailableTests {
}
@After
public void cleanUp() throws Exception {
public void cleanUp() {
MongoDbFactory mongoDbFactory = this.prepareMongoFactory();
MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory);
@@ -73,7 +75,7 @@ public class MongoDbOutboundGatewayXmlTests extends MongoDbAvailableTests {
@Test
@MongoDbAvailable
public void testSingleQuery() throws Exception {
public void testSingleQuery() {
EventDrivenConsumer consumer = context.getBean("gatewaySingleQuery", EventDrivenConsumer.class);
PollableChannel outChannel = context.getBean("out", PollableChannel.class);
@@ -87,7 +89,7 @@ public class MongoDbOutboundGatewayXmlTests extends MongoDbAvailableTests {
@Test
@MongoDbAvailable
public void testSingleQueryWithTemplate() throws Exception {
public void testSingleQueryWithTemplate() {
EventDrivenConsumer consumer = context.getBean("gatewayWithTemplate", EventDrivenConsumer.class);
PollableChannel outChannel = context.getBean("out", PollableChannel.class);
@@ -101,7 +103,7 @@ public class MongoDbOutboundGatewayXmlTests extends MongoDbAvailableTests {
@Test
@MongoDbAvailable
public void testSingleQueryExpression() throws Exception {
public void testSingleQueryExpression() {
EventDrivenConsumer consumer = context.getBean("gatewaySingleQueryExpression", EventDrivenConsumer.class);
PollableChannel outChannel = context.getBean("out", PollableChannel.class);
@@ -120,7 +122,7 @@ public class MongoDbOutboundGatewayXmlTests extends MongoDbAvailableTests {
@Test
@MongoDbAvailable
public void testQueryExpression() throws Exception {
public void testQueryExpression() {
EventDrivenConsumer consumer = context.getBean("gatewayQueryExpression", EventDrivenConsumer.class);
PollableChannel outChannel = context.getBean("out", PollableChannel.class);
@@ -139,7 +141,7 @@ public class MongoDbOutboundGatewayXmlTests extends MongoDbAvailableTests {
@Test
@MongoDbAvailable
public void testQueryExpressionWithLimit() throws Exception {
public void testQueryExpressionWithLimit() {
EventDrivenConsumer consumer = context.getBean("gatewayQueryExpressionLimit", EventDrivenConsumer.class);
PollableChannel outChannel = context.getBean("out", PollableChannel.class);
@@ -157,7 +159,7 @@ public class MongoDbOutboundGatewayXmlTests extends MongoDbAvailableTests {
@Test
@MongoDbAvailable
public void testCollectionCallback() throws Exception {
public void testCollectionCallback() {
EventDrivenConsumer consumer = context.getBean("gatewayCollectionCallback", EventDrivenConsumer.class);
PollableChannel outChannel = context.getBean("out", PollableChannel.class);

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation="http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="input" class="org.springframework.integration.channel.FluxMessageChannel"/>
<int:service-activator input-channel="input">
<bean class="org.springframework.integration.mongodb.outbound.ReactiveMongoDbStoringMessageHandler">
<constructor-arg
value="#{T (org.springframework.integration.mongodb.outbound.ReactiveMongoDbStoringMessageHandlerTests).REACTIVE_MONGO_DATABASE_FACTORY}"/>
</bean>
</int:service-activator>
</beans>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2019 the original author or authors.
* Copyright 2019-2020 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,6 +18,7 @@ package org.springframework.integration.mongodb.outbound;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
@@ -26,8 +27,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
@@ -40,6 +45,9 @@ import org.springframework.integration.mongodb.rules.MongoDbAvailable;
import org.springframework.integration.mongodb.rules.MongoDbAvailableTests;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import reactor.core.publisher.Mono;
@@ -48,19 +56,25 @@ import reactor.core.publisher.Mono;
* @author Oleg Zhurakousky
* @author Gary Russell
* @author David Turanski
* @author Artem Bilan
*
* @since 5.3
*/
@RunWith(SpringRunner.class)
@DirtiesContext
public class ReactiveMongoDbStoringMessageHandlerTests extends MongoDbAvailableTests {
private ReactiveMongoTemplate template;
private ReactiveMongoDatabaseFactory mongoDbFactory;
@Autowired
private MessageChannel input;
@Before
public void setUp() {
mongoDbFactory = this.prepareReactiveMongoFactory("foo");
template = new ReactiveMongoTemplate(mongoDbFactory);
this.mongoDbFactory = prepareReactiveMongoFactory("foo");
this.template = new ReactiveMongoTemplate(this.mongoDbFactory);
}
@Test
@@ -82,6 +96,7 @@ public class ReactiveMongoDbStoringMessageHandlerTests extends MongoDbAvailableT
public void validateMessageHandlingWithDefaultCollection() {
ReactiveMongoDbStoringMessageHandler handler = new ReactiveMongoDbStoringMessageHandler(this.mongoDbFactory);
handler.setBeanFactory(mock(BeanFactory.class));
handler.setApplicationContext(mock(ApplicationContext.class, Answers.RETURNS_MOCKS));
handler.afterPropertiesSet();
Message<Person> message = MessageBuilder.withPayload(this.createPerson("Bob")).build();
waitFor(handler.handleMessage(message));
@@ -99,6 +114,7 @@ public class ReactiveMongoDbStoringMessageHandlerTests extends MongoDbAvailableT
ReactiveMongoDbStoringMessageHandler handler = new ReactiveMongoDbStoringMessageHandler(this.mongoDbFactory);
handler.setCollectionNameExpression(new LiteralExpression("foo"));
handler.setBeanFactory(mock(BeanFactory.class));
handler.setApplicationContext(mock(ApplicationContext.class, Answers.RETURNS_MOCKS));
handler.afterPropertiesSet();
Message<Person> message = MessageBuilder.withPayload(this.createPerson("Bob")).build();
@@ -119,6 +135,7 @@ public class ReactiveMongoDbStoringMessageHandlerTests extends MongoDbAvailableT
ReactiveMongoDbStoringMessageHandler handler = new ReactiveMongoDbStoringMessageHandler(this.mongoDbFactory);
handler.setCollectionNameExpression(new LiteralExpression(null));
handler.setBeanFactory(mock(BeanFactory.class));
handler.setApplicationContext(mock(ApplicationContext.class, Answers.RETURNS_MOCKS));
handler.afterPropertiesSet();
Message<Person> message = MessageBuilder.withPayload(createPerson("Bob")).build();
@@ -139,6 +156,7 @@ public class ReactiveMongoDbStoringMessageHandlerTests extends MongoDbAvailableT
converter = spy(converter);
handler.setMongoConverter(converter);
handler.setBeanFactory(mock(BeanFactory.class));
handler.setApplicationContext(mock(ApplicationContext.class, Answers.RETURNS_MOCKS));
handler.afterPropertiesSet();
Message<Person> message = MessageBuilder.withPayload(this.createPerson("Bob")).build();
waitFor(handler.handleMessage(message));
@@ -161,6 +179,7 @@ public class ReactiveMongoDbStoringMessageHandlerTests extends MongoDbAvailableT
ReactiveMongoDbStoringMessageHandler handler = new ReactiveMongoDbStoringMessageHandler(writingTemplate);
handler.setCollectionNameExpression(new LiteralExpression("foo"));
handler.setBeanFactory(mock(BeanFactory.class));
handler.setApplicationContext(mock(ApplicationContext.class, Answers.RETURNS_MOCKS));
handler.afterPropertiesSet();
Message<Person> message = MessageBuilder.withPayload(this.createPerson("Bob")).build();
waitFor(handler.handleMessage(message));
@@ -172,8 +191,22 @@ public class ReactiveMongoDbStoringMessageHandlerTests extends MongoDbAvailableT
assertThat(person.getAddress().getState()).isEqualTo("PA");
}
@Test
@MongoDbAvailable
public void testReactiveMongoMessageHandlerFromApplicationContext() {
Message<Person> message = MessageBuilder.withPayload(createPerson("Bob")).build();
this.input.send(message);
Query query = new BasicQuery("{'name' : 'Bob'}");
await().untilAsserted(() ->
assertThat(waitFor(this.template.findOne(query, Person.class, "data")))
.isNotNull()
.extracting("name", "address.state").contains("Bob", "PA"));
}
private static <T> T waitFor(Mono<T> mono) {
return mono.block(Duration.ofSeconds(3));
return mono.block(Duration.ofSeconds(10));
}
}

View File

@@ -65,16 +65,20 @@ public abstract class MongoDbAvailableTests {
MongoClientSettings.builder().build()),
"test");
public static final ReactiveMongoDatabaseFactory REACTIVE_MONGO_DATABASE_FACTORY =
new SimpleReactiveMongoDatabaseFactory(
com.mongodb.reactivestreams.client.MongoClients.create(
MongoClientSettings.builder().build()),
"test");
protected MongoDbFactory prepareMongoFactory(String... additionalCollectionsToDrop) {
cleanupCollections(MONGO_DATABASE_FACTORY, additionalCollectionsToDrop);
return MONGO_DATABASE_FACTORY;
}
protected ReactiveMongoDatabaseFactory prepareReactiveMongoFactory(String... additionalCollectionsToDrop) {
ReactiveMongoDatabaseFactory mongoDbFactory = new SimpleReactiveMongoDatabaseFactory(
com.mongodb.reactivestreams.client.MongoClients.create(), "test");
cleanupCollections(mongoDbFactory, additionalCollectionsToDrop);
return mongoDbFactory;
cleanupCollections(REACTIVE_MONGO_DATABASE_FACTORY, additionalCollectionsToDrop);
return REACTIVE_MONGO_DATABASE_FACTORY;
}
protected void cleanupCollections(ReactiveMongoDatabaseFactory mongoDbFactory,