Browse Source

DATAMONGO-2012 - Polishing.

Simplify conditional flow. Replace AtomicReference construction in ChangeStreamEvent with AtomicReferenceFieldUpdater usage to reduce object allocations to streamline lazy body conversion usage. Tweak Javadoc and reference docs.

Original pull request: #576.
pull/577/merge
Mark Paluch 8 years ago
parent
commit
78c2ab290d
  1. 48
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java
  2. 19
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java
  3. 13
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
  4. 24
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java
  5. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java
  6. 8
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainerTests.java
  7. 29
      src/main/asciidoc/reference/change-streams.adoc

48
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java

@ -18,7 +18,7 @@ package org.springframework.data.mongodb.core;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.time.Instant; import java.time.Instant;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import org.bson.BsonValue; import org.bson.BsonValue;
import org.bson.Document; import org.bson.Document;
@ -41,11 +41,17 @@ import com.mongodb.client.model.changestream.OperationType;
@EqualsAndHashCode @EqualsAndHashCode
public class ChangeStreamEvent<T> { public class ChangeStreamEvent<T> {
@SuppressWarnings("rawtypes") //
private static final AtomicReferenceFieldUpdater<ChangeStreamEvent, Object> CONVERTED_UPDATER = AtomicReferenceFieldUpdater
.newUpdater(ChangeStreamEvent.class, Object.class, "converted");
private final @Nullable ChangeStreamDocument<Document> raw; private final @Nullable ChangeStreamDocument<Document> raw;
private final Class<T> targetType; private final Class<T> targetType;
private final MongoConverter converter; private final MongoConverter converter;
private final AtomicReference<T> converted = new AtomicReference<>();
// accessed through CONVERTED_UPDATER.
private volatile @Nullable T converted;
/** /**
* @param raw can be {@literal null}. * @param raw can be {@literal null}.
@ -77,7 +83,7 @@ public class ChangeStreamEvent<T> {
*/ */
@Nullable @Nullable
public Instant getTimestamp() { public Instant getTimestamp() {
return raw != null ? Instant.ofEpochMilli(raw.getClusterTime().getValue()) : null; return raw != null && raw.getClusterTime() != null ? Instant.ofEpochMilli(raw.getClusterTime().getValue()) : null;
} }
/** /**
@ -133,36 +139,48 @@ public class ChangeStreamEvent<T> {
return null; return null;
} }
if (raw.getFullDocument() == null) { Document fullDocument = raw.getFullDocument();
return targetType.cast(raw.getFullDocument());
if (fullDocument == null) {
return targetType.cast(fullDocument);
} }
return getConverted(); return getConverted(fullDocument);
} }
private T getConverted() { @SuppressWarnings("unchecked")
private T getConverted(Document fullDocument) {
return (T) doGetConverted(fullDocument);
}
private Object doGetConverted(Document fullDocument) {
Object result = CONVERTED_UPDATER.get(this);
T result = converted.get();
if (result != null) { if (result != null) {
return result; return result;
} }
if (ClassUtils.isAssignable(Document.class, raw.getFullDocument().getClass())) { if (ClassUtils.isAssignable(Document.class, fullDocument.getClass())) {
result = converter.read(targetType, raw.getFullDocument()); result = converter.read(targetType, fullDocument);
return converted.compareAndSet(null, result) ? result : converted.get(); return CONVERTED_UPDATER.compareAndSet(this, null, result) ? result : CONVERTED_UPDATER.get(this);
} }
if (converter.getConversionService().canConvert(raw.getFullDocument().getClass(), targetType)) { if (converter.getConversionService().canConvert(fullDocument.getClass(), targetType)) {
result = converter.getConversionService().convert(raw.getFullDocument(), targetType); result = converter.getConversionService().convert(fullDocument, targetType);
return converted.compareAndSet(null, result) ? result : converted.get(); return CONVERTED_UPDATER.compareAndSet(this, null, result) ? result : CONVERTED_UPDATER.get(this);
} }
throw new IllegalArgumentException(String.format("No converter found capable of converting %s to %s", throw new IllegalArgumentException(String.format("No converter found capable of converting %s to %s",
raw.getFullDocument().getClass(), targetType)); fullDocument.getClass(), targetType));
} }
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override @Override
public String toString() { public String toString() {
return "ChangeStreamEvent {" + "raw=" + raw + ", targetType=" + targetType + '}'; return "ChangeStreamEvent {" + "raw=" + raw + ", targetType=" + targetType + '}';

19
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java

@ -26,6 +26,7 @@ import org.bson.Document;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription; import org.reactivestreams.Subscription;
import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResult;
import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
@ -1356,9 +1357,9 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
<T> Flux<T> tail(Query query, Class<T> entityClass, String collectionName); <T> Flux<T> tail(Query query, Class<T> entityClass, String collectionName);
/** /**
* Subscribe to a MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Streams</a> for all events * Subscribe to a MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Stream</a> for all events in
* in the configured default database via the reactive infrastructure. Use the optional provided {@link Aggregation} * the configured default database via the reactive infrastructure. Use the optional provided {@link Aggregation} to
* to filter events. The stream will not be completed unless the {@link org.reactivestreams.Subscription} is * filter events. The stream will not be completed unless the {@link org.reactivestreams.Subscription} is
* {@link Subscription#cancel() canceled}. * {@link Subscription#cancel() canceled}.
* <p /> * <p />
* The {@link ChangeStreamEvent#getBody()} is mapped to the {@literal resultType} while the * The {@link ChangeStreamEvent#getBody()} is mapped to the {@literal resultType} while the
@ -1372,14 +1373,16 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* @param <T> * @param <T>
* @return the {@link Flux} emitting {@link ChangeStreamEvent events} as they arrive. * @return the {@link Flux} emitting {@link ChangeStreamEvent events} as they arrive.
* @since 2.1 * @since 2.1
* @see ReactiveMongoDatabaseFactory#getMongoDatabase()
* @see ChangeStreamOptions#getFilter()
*/ */
default <T> Flux<ChangeStreamEvent<T>> changeStream(ChangeStreamOptions options, Class<T> targetType) { default <T> Flux<ChangeStreamEvent<T>> changeStream(ChangeStreamOptions options, Class<T> targetType) {
return changeStream(null, options, targetType); return changeStream(null, options, targetType);
} }
/** /**
* Subscribe to a MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Streams</a> for all events * Subscribe to a MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Stream</a> for all events in
* in the given collection via the reactive infrastructure. Use the optional provided {@link Aggregation} to filter * the given collection via the reactive infrastructure. Use the optional provided {@link Aggregation} to filter
* events. The stream will not be completed unless the {@link org.reactivestreams.Subscription} is * events. The stream will not be completed unless the {@link org.reactivestreams.Subscription} is
* {@link Subscription#cancel() canceled}. * {@link Subscription#cancel() canceled}.
* <p /> * <p />
@ -1389,12 +1392,13 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* Use {@link ChangeStreamOptions} to set arguments like {@link ChangeStreamOptions#getResumeToken() the resumseToken} * Use {@link ChangeStreamOptions} to set arguments like {@link ChangeStreamOptions#getResumeToken() the resumseToken}
* for resuming change streams. * for resuming change streams.
* *
* @param collectionName the collection to watch. Can be {@literal null}, watches all collections if so. * @param collectionName the collection to watch. Can be {@literal null} to watch all collections.
* @param options must not be {@literal null}. Use {@link ChangeStreamOptions#empty()}. * @param options must not be {@literal null}. Use {@link ChangeStreamOptions#empty()}.
* @param targetType the result type to use. * @param targetType the result type to use.
* @param <T> * @param <T>
* @return the {@link Flux} emitting {@link ChangeStreamEvent events} as they arrive. * @return the {@link Flux} emitting {@link ChangeStreamEvent events} as they arrive.
* @since 2.1 * @since 2.1
* @see ChangeStreamOptions#getFilter()
*/ */
default <T> Flux<ChangeStreamEvent<T>> changeStream(@Nullable String collectionName, ChangeStreamOptions options, default <T> Flux<ChangeStreamEvent<T>> changeStream(@Nullable String collectionName, ChangeStreamOptions options,
Class<T> targetType) { Class<T> targetType) {
@ -1403,7 +1407,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
} }
/** /**
* Subscribe to a MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Streams</a> via the reactive * Subscribe to a MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Stream</a> via the reactive
* infrastructure. Use the optional provided {@link Aggregation} to filter events. The stream will not be completed * infrastructure. Use the optional provided {@link Aggregation} to filter events. The stream will not be completed
* unless the {@link org.reactivestreams.Subscription} is {@link Subscription#cancel() canceled}. * unless the {@link org.reactivestreams.Subscription} is {@link Subscription#cancel() canceled}.
* <p /> * <p />
@ -1420,6 +1424,7 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations {
* @param <T> * @param <T>
* @return the {@link Flux} emitting {@link ChangeStreamEvent events} as they arrive. * @return the {@link Flux} emitting {@link ChangeStreamEvent events} as they arrive.
* @since 2.1 * @since 2.1
* @see ChangeStreamOptions#getFilter()
*/ */
<T> Flux<ChangeStreamEvent<T>> changeStream(@Nullable String database, @Nullable String collectionName, <T> Flux<ChangeStreamEvent<T>> changeStream(@Nullable String database, @Nullable String collectionName,
ChangeStreamOptions options, Class<T> targetType); ChangeStreamOptions options, Class<T> targetType);

13
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java

@ -2028,11 +2028,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
List<Document> prepareFilter(ChangeStreamOptions options) { List<Document> prepareFilter(ChangeStreamOptions options) {
if (!options.getFilter().isPresent()) { Object filter = options.getFilter().orElse(Collections.emptyList());
return Collections.emptyList();
}
Object filter = options.getFilter().get();
if (filter instanceof Aggregation) { if (filter instanceof Aggregation) {
Aggregation agg = (Aggregation) filter; Aggregation agg = (Aggregation) filter;
AggregationOperationContext context = agg instanceof TypedAggregation AggregationOperationContext context = agg instanceof TypedAggregation
@ -2042,13 +2039,15 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return agg.toPipeline(new PrefixingDelegatingAggregationOperationContext(context, "fullDocument", return agg.toPipeline(new PrefixingDelegatingAggregationOperationContext(context, "fullDocument",
Arrays.asList("operationType", "fullDocument", "documentKey", "updateDescription", "ns"))); Arrays.asList("operationType", "fullDocument", "documentKey", "updateDescription", "ns")));
} else if (filter instanceof List) { }
if (filter instanceof List) {
return (List<Document>) filter; return (List<Document>) filter;
} else { }
throw new IllegalArgumentException( throw new IllegalArgumentException(
"ChangeStreamRequestOptions.filter mut be either an Aggregation or a plain list of Documents"); "ChangeStreamRequestOptions.filter mut be either an Aggregation or a plain list of Documents");
} }
}
/* /*
* (non-Javadoc) * (non-Javadoc)

24
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java

@ -58,6 +58,7 @@ import com.mongodb.client.model.changestream.FullDocument;
* {@link Task} implementation for obtaining {@link ChangeStreamDocument ChangeStreamDocuments} from MongoDB. * {@link Task} implementation for obtaining {@link ChangeStreamDocument ChangeStreamDocuments} from MongoDB.
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch
* @since 2.1 * @since 2.1
*/ */
class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>, Object> { class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>, Object> {
@ -114,9 +115,8 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
.orElseGet(() -> ClassUtils.isAssignable(Document.class, targetType) ? FullDocument.DEFAULT .orElseGet(() -> ClassUtils.isAssignable(Document.class, targetType) ? FullDocument.DEFAULT
: FullDocument.UPDATE_LOOKUP); : FullDocument.UPDATE_LOOKUP);
if (changeStreamOptions.getResumeTimestamp().isPresent()) { startAt = changeStreamOptions.getResumeTimestamp().map(Instant::toEpochMilli).map(BsonTimestamp::new)
startAt = new BsonTimestamp(changeStreamOptions.getResumeTimestamp().get().toEpochMilli()); .orElse(null);
}
} }
MongoDatabase db = StringUtils.hasText(options.getDatabaseName()) MongoDatabase db = StringUtils.hasText(options.getDatabaseName())
@ -149,13 +149,15 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
return iterable.iterator(); return iterable.iterator();
} }
@SuppressWarnings("unchecked")
List<Document> prepareFilter(MongoTemplate template, ChangeStreamOptions options) { List<Document> prepareFilter(MongoTemplate template, ChangeStreamOptions options) {
if (!options.getFilter().isPresent()) { if (!options.getFilter().isPresent()) {
return Collections.emptyList(); return Collections.emptyList();
} }
Object filter = options.getFilter().get(); Object filter = options.getFilter().orElse(null);
if (filter instanceof Aggregation) { if (filter instanceof Aggregation) {
Aggregation agg = (Aggregation) filter; Aggregation agg = (Aggregation) filter;
AggregationOperationContext context = agg instanceof TypedAggregation AggregationOperationContext context = agg instanceof TypedAggregation
@ -164,14 +166,20 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
: Aggregation.DEFAULT_CONTEXT; : Aggregation.DEFAULT_CONTEXT;
return agg.toPipeline(new PrefixingDelegatingAggregationOperationContext(context, "fullDocument", blacklist)); return agg.toPipeline(new PrefixingDelegatingAggregationOperationContext(context, "fullDocument", blacklist));
} else if (filter instanceof List) { }
if (filter instanceof List) {
return (List<Document>) filter; return (List<Document>) filter;
} else { }
throw new IllegalArgumentException( throw new IllegalArgumentException(
"ChangeStreamRequestOptions.filter mut be either an Aggregation or a plain list of Documents"); "ChangeStreamRequestOptions.filter mut be either an Aggregation or a plain list of Documents");
} }
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.messaging.CursorReadingTask#createMessage(java.lang.Object, java.lang.Class, org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions)
*/
@Override @Override
protected Message<ChangeStreamDocument<Document>, Object> createMessage(ChangeStreamDocument<Document> source, protected Message<ChangeStreamDocument<Document>, Object> createMessage(ChangeStreamDocument<Document> source,
Class<Object> targetType, RequestOptions options) { Class<Object> targetType, RequestOptions options) {
@ -232,7 +240,7 @@ class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>,
} }
/** /**
* @return the resume token or {@litearl null} if not set. * @return the resume token or {@literal null} if not set.
* @see ChangeStreamEvent#getResumeToken() * @see ChangeStreamEvent#getResumeToken()
*/ */
@Nullable @Nullable

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java

@ -57,7 +57,7 @@ public interface SubscriptionRequest<S, T, O extends RequestOptions> {
* Get the database name of the db. * Get the database name of the db.
* *
* @return the name of the database to subscribe to. Can be {@literal null} in which case the default * @return the name of the database to subscribe to. Can be {@literal null} in which case the default
* {@link MongoDbFactory#getDb()} is used. * {@link MongoDbFactory#getDb() database} is used.
*/ */
@Nullable @Nullable
default String getDatabaseName() { default String getDatabaseName() {

8
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainerTests.java

@ -56,16 +56,16 @@ public class DefaultMessageListenerContainerTests {
public static final String DATABASE_NAME = "change-stream-events"; public static final String DATABASE_NAME = "change-stream-events";
public static final String COLLECTION_NAME = "collection-1"; public static final String COLLECTION_NAME = "collection-1";
public static final String COLLECTION_2_NAME = "collection-2"; public static final String COLLECTION_2_NAME = "collection-2";
MongoDbFactory dbFactory;
public @Rule TestRule replSet = ReplicaSet.none();
MongoDbFactory dbFactory;
MongoCollection<Document> collection; MongoCollection<Document> collection;
private MongoCollection<Document> collection2; MongoCollection<Document> collection2;
private CollectingMessageListener<Object, Object> messageListener; private CollectingMessageListener<Object, Object> messageListener;
private MongoTemplate template; private MongoTemplate template;
public @Rule TestRule replSet = ReplicaSet.none();
@Before @Before
public void setUp() { public void setUp() {

29
src/main/asciidoc/reference/change-streams.adoc

@ -5,16 +5,16 @@ As of MongoDB 3.6, https://docs.mongodb.com/manual/changeStreams/[Change Streams
NOTE: Change Stream support is only possible for replica sets or for a sharded cluster. NOTE: Change Stream support is only possible for replica sets or for a sharded cluster.
Change Streams can be subscribed to with both the imperative and the reactive MongoDB Java driver. It is highly recommended to use the reactive variant, as it is less resource-intensive. However if you cannot use the reactive API, you can still obtain the change events by using the messaging concept that is already prevalent in the Spring ecosystem. Change Streams can be consumed with both, the imperative and the reactive MongoDB Java driver. It is highly recommended to use the reactive variant, as it is less resource-intensive. However, if you cannot use the reactive API, you can still obtain change events by using the messaging concept that is already prevalent in the Spring ecosystem.
It is possible to watch both on a collection as well as database level, whereas the database level variant publishes It is possible to watch both on a collection as well as database level, whereas the database level variant publishes
changes from all collections within the database. So when subscribing to a database change stream, make sure to use a changes from all collections within the database. When subscribing to a database change stream, make sure to use a
suitable type for the event type in use as conversion might not apply correctly when set to specificly. In doubt use suitable type for the event type as conversion might not apply correctly across different entity types.
`Document`. In doubt, use `Document`.
=== Change Streams with `MessageListener` === Change Streams with `MessageListener`
Listening to a https://docs.mongodb.com/manual/tutorial/change-streams-example/[Change Stream by using a Sync Driver] is a long running, blocking task that needs to be delegated to a separate component. Listening to a https://docs.mongodb.com/manual/tutorial/change-streams-example/[Change Stream by using a Sync Driver] creates a long running, blocking task that needs to be delegated to a separate component.
In this case, we need to first create a `MessageListenerContainer`, which will be the main entry point for running the specific `SubscriptionRequest` tasks. In this case, we need to first create a `MessageListenerContainer`, which will be the main entry point for running the specific `SubscriptionRequest` tasks.
Spring Data MongoDB already ships with a default implementation that operates on `MongoTemplate` and is capable of creating and executing `Task` instances for a `ChangeStreamRequest`. Spring Data MongoDB already ships with a default implementation that operates on `MongoTemplate` and is capable of creating and executing `Task` instances for a `ChangeStreamRequest`.
@ -36,18 +36,18 @@ Subscription subscription = container.register(new ChangeStreamRequest<>(listene
container.stop(); <5> container.stop(); <5>
---- ----
<1> Starting the container intializes the resources and starts the `Task` instances for the already registered `SubscriptionRequest` instances. Requests added after the startup are run immediately. <1> Starting the container intializes the resources and starts `Task` instances for already registered `SubscriptionRequest` instances. Requests added after startup are ran immediately.
<2> Define the listener called when a `Message` is received. The `Message#getBody()` is converted to the requested domain type. Use `Document` to receive raw results without conversion. <2> Define the listener called when a `Message` is received. The `Message#getBody()` is converted to the requested domain type. Use `Document` to receive raw results without conversion.
<3> Set the collection to listen to and provide additional options through `ChangeStreamOptions`. <3> Set the collection to listen to and provide additional options through `ChangeStreamOptions`.
<4> Register the request. The returned `Subscription` can be used to check the current `Task` state and cancel its execution to free resources. <4> Register the request. The returned `Subscription` can be used to check the current `Task` state and cancel its execution to free resources.
<5> Do not forget to stop the container once you are sure you no longer need it. Doing so stops all running `Task` instances within the container. <5> Do not forget to stop the container once you are sure you no longer need it. Doing so stops all running `Task` instances within the container.
==== ====
=== Change Streams - Reactive === Reactive Change Streams
Subscribing to Change Stream with the reactive API is more straightforward. Still the essential building blocks, such as `ChangeStreamOptions`, remain the same. The following example shows how to use Change Streams with reactive `MessageListeners`: Subscribing to Change Streams with the reactive API is a more natural approach to work with streams. Still, the essential building blocks, such as `ChangeStreamOptions`, remain the same. The following example shows how to use Change Streams emitting ``ChangeStreamEvent``s:
.Change Streams with `MessageListeners` .Change Streams emitting `ChangeStreamEvent`
==== ====
[source,java] [source,java]
---- ----
@ -63,9 +63,10 @@ Flux<ChangeStreamEvent<User>> flux = reactiveTemplate.changeStream("user", optio
=== Resuming Change Streams === Resuming Change Streams
Change Streams can be resumed and will pick up emitting events where you left. To resume the stream either a resume Change Streams can be resumed and resume emitting events where you left. To resume the stream, you need to supply either a resume
token or the server time in UTC from where to resume is required. Use `ChangeStreamOptions` to set the value token or the last known server time (in UTC). Use `ChangeStreamOptions` to set the value accordingly.
accordingly.
The following example shows how to set the resume offset using server time:
.Resume a Change Stream .Resume a Change Stream
==== ====
@ -77,6 +78,6 @@ ChangeStreamOptions = ChangeStreamOptions.builder()
Flux<ChangeStreamEvent<Person>> resumed = template.changeStream("person", options, User.class) Flux<ChangeStreamEvent<Person>> resumed = template.changeStream("person", options, User.class)
---- ----
<1> You may obtain the server time of an `ChangeStreamEvent` via the `getTimestamp` method or use the `resumeToken` <1> You may obtain the server time of an `ChangeStreamEvent` through the `getTimestamp` method or use the `resumeToken`
exposed via `getResumeToken`. exposed through `getResumeToken`.
==== ====

Loading…
Cancel
Save