#337 - Add sample for MongoDB 3.6 Change Streams.

This commit is contained in:
Christoph Strobl
2018-02-08 19:24:16 +01:00
committed by Mark Paluch
parent e1cc43e1ca
commit 2474846281
8 changed files with 473 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
# Spring Data MongoDB - Change Streams Example
This project contains usage samples for consuming MongoDB 3.6 [Change Streams](https://docs.mongodb.com/manual/changeStreams/) using the imperative as well as the reactive MongoDB Java drivers.
### Imperative Style
Change stream events can be consumed using a `MessageListener` registered within a `MessageListenerContainer`. The container takes care of running the task in a separate `Thread` pushing events to the `MessageListener`.
```java
@Configuration
class Config {
@Bean
MessageListenerContainer messageListenerContainer(MongoTemplate template) {
return new DefaultMessageListenerContainer(template);
}
}
```
Once the `MessageListenerContainer` is in place `MessageListeners` can be registered.
```java
MessageListener<ChangeStreamDocument<Document>, Person> messageListener = (message) -> {
System.out.println("Hello " + message.getBody().getFirstname());
};
ChangeStreamRequest<Person> request = ChangeStreamRequest.builder()
.collection("person")
.filter(newAggregation(match(where("operationType").is("insert"))))
.publishTo(messageListener)
.build();
Subscription subscription = messageListenerContainer.register(request, Person.class);
// ...
```
### Reactive Style
Change stream events be directly consumed via a `Flux` connected to the change stream.
```java
Flux changeStream = reactiveTemplate
.changeStream(newAggregation(match(where("operationType").is("insert"))),
Person.class, ChangeStreamOptions.empty(), "person");
changeStream.doOnNext(event -> System.out.println("Hello " + event.getBody().getFirstname()))
.subscribe();
```

View File

@@ -0,0 +1,65 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.data.examples</groupId>
<artifactId>spring-data-mongodb-examples</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-data-mongodb-change-streams</artifactId>
<name>Spring Data MongoDB - Change Streams</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
<exclusions>
<exclusion>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
<version>3.6.2</version>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-async</artifactId>
<version>3.6.2</version>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-reactivestreams</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<version>2.1.0.M1</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2018 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
*
* http://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 example.springdata.mongodb;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import org.springframework.data.mongodb.core.messaging.Message;
import org.springframework.data.mongodb.core.messaging.MessageListener;
/**
* Just a simple {@link MessageListener} implementation collecting {@link Message messages} in a {@link BlockingDeque}
* and printing the message itself to the console.
*
* @author Christoph Strobl
*/
class CollectingMessageListener<S, T> implements MessageListener<S, T> {
private final BlockingDeque<Message<S, T>> messages = new LinkedBlockingDeque<>();
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.messaging.MessageListener#onMessage(org.springframework.data.mongodb.core.messaging.Message)
*/
@Override
public void onMessage(Message<S, T> message) {
System.out.println(String.format("Received Message in collection %s.\n\trawsource: %s\n\tconverted: %s",
message.getProperties().getCollectionName(), message.getRaw(), message.getBody()));
messages.add(message);
}
int messageCount() {
return messages.size();
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2018 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
*
* http://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 example.springdata.mongodb;
import lombok.Data;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Field;
/**
* The domain type of choice.
* <p />
* This time <a href="http://www.cbs.com/shows/star-trek-discovery/">Star Trek: Discovery</a> <br />
* <i>Just a trekkie - no relation to CBS in any way other than watching.</i>
*
* @author Christoph Strobl
*/
@Data
class Person {
@Id ObjectId id;
@Field("first_name") private String firstname;
@Field("last_name") private String lastname;
private int age;
Person(String firstname, String lastname, int age) {
this.firstname = firstname;
this.lastname = lastname;
this.age = age;
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2018 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
*
* http://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 example.springdata.mongodb;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import static org.springframework.data.mongodb.core.query.Update.*;
import reactor.core.Disposable;
import reactor.test.StepVerifier;
import java.time.Duration;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.bson.Document;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.ChangeStreamEvent;
import org.springframework.data.mongodb.core.ChangeStreamOptions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.messaging.ChangeStreamRequest;
import org.springframework.data.mongodb.core.messaging.DefaultMessageListenerContainer;
import org.springframework.data.mongodb.core.messaging.Message;
import org.springframework.data.mongodb.core.messaging.MessageListenerContainer;
import org.springframework.data.mongodb.core.messaging.Subscription;
import org.springframework.test.context.junit4.SpringRunner;
import com.mongodb.client.model.changestream.ChangeStreamDocument;
/**
* A simple Test demonstrating required {@link Configuration} for consumption of MongoDB
* <a href="https://docs.mongodb.com/manual/changeStreams/">Change Streams</a> using the sync and reactivestreams java
* driver.
* <p />
* We currently need to have both tests in one class due to some shutdown error in flapdoodle which lets us start the
* process only once.
*
* @author Christoph Strobl
*/
@RunWith(SpringRunner.class)
@DataMongoTest
public class ChangeStreamsTests extends ReplicaSetInitiatingTest {
@Autowired MessageListenerContainer container; // for imperative style
@Autowired ReactiveMongoOperations reactiveTemplate; // for reactive style
Person gabriel = new Person("Gabriel", "Lorca", 30);
Person michael = new Person("Michael", "Burnham", 30);
Person ash = new Person("Ash", "Tyler", 35);
/**
* Configuration? Yes we need a bit of it - Do not worry, it won't be much!
*/
@Configuration
@EnableAutoConfiguration
static class Config {
/**
* Since listening to a <a href="https://docs.mongodb.com/manual/changeStreams/">Change Stream</a> using the sync
* MongoDB Java Driver is a blocking class, we need to move load to another {@link Thread} by simply using a
* {@link MessageListenerContainer}.
* <p />
* As this is a {@link org.springframework.context.SmartLifecycle smart lifecycle component} we do actually not need
* to worry about its lifecycle, the resource allocation and freeing.
*
* @param template must not be {@literal null}.
* @return a new {@link MessageListenerContainer}
*/
@Bean
MessageListenerContainer messageListenerContainer(MongoTemplate template) {
return new DefaultMessageListenerContainer(template) {
/* auto startup will be changed for M2, so this should no longer be required. */
@Override
public boolean isAutoStartup() {
return true;
}
};
}
}
/**
* Use the {@link MessageListenerContainer} registered within the
* {@link org.springframework.context.ApplicationContext} to subscribe to a MongoDB Change Stream. Events published
* via {@link com.mongodb.client.ChangeStreamIterable} are passed to the
* {@link org.springframework.data.mongodb.core.messaging.MessageListener#onMessage(Message) MessageListener}.
*/
@Test
public void imperativeChangeEvents() throws InterruptedException {
CollectingMessageListener<ChangeStreamDocument<Document>, Person> messageListener = new CollectingMessageListener();
ChangeStreamRequest<Person> request = ChangeStreamRequest.builder() //
.collection("person") //
.filter(newAggregation(match(where("operationType").is("insert")))) // we are only interested in inserts
.publishTo(messageListener) //
.build();
Subscription subscription = container.register(request, Person.class);
subscription.await(Duration.ofMillis(200)); // wait till the subscription becomes active
template.save(gabriel);
template.save(ash);
Thread.sleep(200);
assertThat(messageListener.messageCount()).isEqualTo(2); // first two insert events, so far so good
template.update(Person.class) //
.matching(query(where("id").is(ash.getId()))) //
.apply(update("age", 40)) //
.first();
Thread.sleep(200);
assertThat(messageListener.messageCount()).isEqualTo(2); // updates are skipped
template.save(michael);
Thread.sleep(200);
assertThat(messageListener.messageCount()).isEqualTo(3); // there we go, all events received.
}
/**
* Use the {@link reactor.core.publisher.Flux} to subscribe to a MongoDB Change Stream.
*/
@Test
public void reactiveChangeEvents() throws InterruptedException {
BlockingQueue<ChangeStreamEvent<Person>> documents = new LinkedBlockingQueue<>(100);
Disposable disposable = reactiveTemplate.changeStream(newAggregation(match(where("operationType").is("insert"))),
Person.class, ChangeStreamOptions.empty(), "person").doOnNext(documents::add).subscribe();
Thread.sleep(200); // wait till the subscription becomes active
StepVerifier.create(reactiveTemplate.save(gabriel)).expectNextCount(1).verifyComplete();
StepVerifier.create(reactiveTemplate.save(ash)).expectNextCount(1).verifyComplete();
Thread.sleep(200);
assertThat(documents.size()).isEqualTo(2); // first two insert events, so far so good
StepVerifier.create(reactiveTemplate.update(Person.class) //
.matching(query(where("id").is(ash.getId()))) //
.apply(update("age", 40)) //
.first()).expectNextCount(1).verifyComplete();
Thread.sleep(200);
assertThat(documents.size()).isEqualTo(2); // updates are skipped
StepVerifier.create(reactiveTemplate.save(michael)).expectNextCount(1).verifyComplete();
Thread.sleep(200); // just give it some time to link receive all events
assertThat(documents.size()).isEqualTo(3); // there we go, all events received.
disposable.dispose();
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2018 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
*
* http://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 example.springdata.mongodb;
import java.util.Arrays;
import org.bson.Document;
import org.hamcrest.number.IsCloseTo;
import org.junit.Assume;
import org.junit.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.mongodb.core.MongoTemplate;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientOptions;
import com.mongodb.ServerAddress;
import com.mongodb.client.MongoDatabase;
/**
* Just a simple base Class initiating the required replica set.
*
* @author Christoph Strobl
*/
abstract class ReplicaSetInitiatingTest {
@Autowired MongoTemplate template;
@Value("${spring.data.mongodb.port}") Integer port;
/**
* Before we can actually continue we need to make sure that the Server works as a single node replica set which is
* required for Change Streams.
*/
@Before
public void setUp() {
MongoClient mongo = new MongoClient(new ServerAddress("localhost:" + port),
MongoClientOptions.builder().connectTimeout(10).build());
MongoDatabase admin = mongo.getDatabase("admin");
Document status = admin.runCommand(new Document("serverStatus", "1"));
if (!status.get("repl", Document.class).get("ismaster", Boolean.class)) {
Document result = admin.runCommand(new Document("replSetInitiate", new Document("_id", "spring-data-examples")
.append("members", Arrays.asList(new Document("_id", 0).append("host", "localhost:" + port)))));
Assume.assumeThat(result.getDouble("ok"), IsCloseTo.closeTo(1.0D, 0D));
}
mongo.close();
template.dropCollection(Person.class);
template.createCollection(Person.class);
}
}

View File

@@ -0,0 +1,4 @@
spring.mongodb.embedded.version=3.6.0
spring.mongodb.embedded.features=ONLY_WITH_SSL,ONLY_64BIT,NO_HTTP_INTERFACE_ARG
spring.mongodb.embedded.storage.repl-set-name=spring-data-examples
spring.data.mongodb.port=57143

View File

@@ -18,6 +18,7 @@
<modules>
<module>aggregation</module>
<module>change-streams</module>
<module>example</module>
<module>fluent-api</module>
<module>geo-json</module>