Allow disabling entity lifecycle events.

We now support disabling lifecycle events through the Template API to reduce the framework overhead when events are not needed.

Closes #4107
This commit is contained in:
Mark Paluch
2022-07-20 16:05:48 +02:00
parent f5378bf825
commit d130984bdc
6 changed files with 135 additions and 12 deletions

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2022 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.data.mongodb.core;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.lang.Nullable;
/**
* Delegate class to encapsulate lifecycle event configuration and publishing.
*
* @author Mark Paluch
* @since 4.0
* @see ApplicationEventPublisher
*/
class EntityLifecycleEventDelegate {
private @Nullable ApplicationEventPublisher publisher;
private boolean eventsEnabled = true;
public void setPublisher(@Nullable ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public boolean isEventsEnabled() {
return eventsEnabled;
}
public void setEventsEnabled(boolean eventsEnabled) {
this.eventsEnabled = eventsEnabled;
}
/**
* Publish an application event if event publishing is enabled.
*
* @param event the application event.
*/
public void publishEvent(Object event) {
if (canPublishEvent()) {
publisher.publishEvent(event);
}
}
private boolean canPublishEvent() {
return publisher != null && eventsEnabled;
}
}

View File

@@ -23,6 +23,7 @@ import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiPredicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -174,6 +175,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
private final EntityOperations operations;
private final PropertyOperations propertyOperations;
private final QueryOperations queryOperations;
private final EntityLifecycleEventDelegate eventDelegate;
private @Nullable WriteConcern writeConcern;
private WriteConcernResolver writeConcernResolver = DefaultWriteConcernResolver.INSTANCE;
@@ -228,6 +230,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext());
this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations,
mongoDbFactory);
this.eventDelegate = new EntityLifecycleEventDelegate();
// We always have a mapping context in the converter, whether it's a simple one or not
mappingContext = this.mongoConverter.getMappingContext();
@@ -266,6 +269,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
this.operations = that.operations;
this.propertyOperations = that.propertyOperations;
this.queryOperations = that.queryOperations;
this.eventDelegate = that.eventDelegate;
}
/**
@@ -308,12 +312,25 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
this.readPreference = readPreference;
}
/**
* Configure whether lifecycle events such as {@link AfterLoadEvent}, {@link BeforeSaveEvent}, etc. should be
* published or whether emission should be suppressed. Enabled by default.
*
* @param enabled {@code true} to enable entity lifecycle events; {@code false} to disable entity lifecycle events.
* @since 4.0
* @see MongoMappingEvent
*/
public void setEntityLifecycleEventsEnabled(boolean enabled) {
this.eventDelegate.setEventsEnabled(enabled);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
prepareIndexCreator(applicationContext);
eventPublisher = applicationContext;
eventDelegate.setPublisher(eventPublisher);
if (entityCallbacks == null) {
setEntityCallbacks(EntityCallbacks.create(applicationContext));
@@ -2168,11 +2185,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
protected <E extends MongoMappingEvent<T>, T> E maybeEmitEvent(E event) {
if (eventPublisher != null) {
eventPublisher.publishEvent(event);
}
eventDelegate.publishEvent(event);
return event;
}

View File

@@ -185,6 +185,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private final EntityOperations operations;
private final PropertyOperations propertyOperations;
private final QueryOperations queryOperations;
private final EntityLifecycleEventDelegate eventDelegate;
private @Nullable WriteConcern writeConcern;
private WriteConcernResolver writeConcernResolver = DefaultWriteConcernResolver.INSTANCE;
@@ -256,6 +257,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.propertyOperations = new PropertyOperations(this.mongoConverter.getMappingContext());
this.queryOperations = new QueryOperations(queryMapper, updateMapper, operations, propertyOperations,
mongoDatabaseFactory);
this.eventDelegate = new EntityLifecycleEventDelegate();
// We create indexes based on mapping events
if (this.mappingContext instanceof MongoMappingContext) {
@@ -287,6 +289,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.propertyOperations = that.propertyOperations;
this.sessionSynchronization = that.sessionSynchronization;
this.queryOperations = that.queryOperations;
this.eventDelegate = that.eventDelegate;
}
private void onCheckForIndexes(MongoPersistentEntity<?> entity, Consumer<Throwable> subscriptionExceptionHandler) {
@@ -339,12 +342,25 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.readPreference = readPreference;
}
/**
* Configure whether lifecycle events such as {@link AfterLoadEvent}, {@link BeforeSaveEvent}, etc. should be
* published or whether emission should be suppressed. Enabled by default.
*
* @param enabled {@code true} to enable entity lifecycle events; {@code false} to disable entity lifecycle events.
* @since 4.0
* @see MongoMappingEvent
*/
public void setEntityLifecycleEventsEnabled(boolean enabled) {
this.eventDelegate.setEventsEnabled(enabled);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
prepareIndexCreator(applicationContext);
eventPublisher = applicationContext;
eventDelegate.setPublisher(eventPublisher);
if (entityCallbacks == null) {
setEntityCallbacks(ReactiveEntityCallbacks.create(applicationContext));
@@ -2324,11 +2340,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
}
protected <E extends MongoMappingEvent<T>, T> E maybeEmitEvent(E event) {
if (eventPublisher != null) {
eventPublisher.publishEvent(event);
}
eventDelegate.publishEvent(event);
return event;
}

View File

@@ -1616,6 +1616,21 @@ public class ReactiveMongoTemplateTests {
assertThat(saved.get().id).isNotNull();
}
@Test // GH-4107
void afterSaveEventCanBeDisabled() {
AtomicReference<ImmutableVersioned> saved = createAfterSaveReference();
ImmutableVersioned source = new ImmutableVersioned();
template.setEntityLifecycleEventsEnabled(false);
template.insertAll(Collections.singleton(new ImmutableVersioned())) //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
assertThat(saved).hasValue(null);
}
@Test // DATAMONGO-2012
@EnableIfMongoServerVersion(isGreaterThanEqual = "4.0")
@EnableIfReplicaSetAvailable

View File

@@ -47,6 +47,7 @@ import org.springframework.data.mongodb.repository.support.MongoRepositoryFactor
import org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor;
import org.springframework.data.mongodb.test.util.Client;
import org.springframework.data.mongodb.test.util.MongoClientExtension;
import org.springframework.test.annotation.DirtiesContext;
import com.mongodb.WriteConcern;
import com.mongodb.client.MongoClient;
@@ -161,6 +162,22 @@ public class ApplicationContextEventTests {
assertThat(listener.onAfterConvertEvents.get(0).getCollectionName()).isEqualTo(COLLECTION_NAME);
}
@Test // GH-4107
@DirtiesContext
public void configurationShouldDisableLifecycleEvents() {
template.setEntityLifecycleEventsEnabled(false);
PersonPojoStringId entity = new PersonPojoStringId("1", "Text");
template.insert(entity);
template.findOne(query(where("id").is(entity.getId())), PersonPojoStringId.class);
assertThat(listener.onAfterLoadEvents).isEmpty();
assertThat(listener.onBeforeConvertEvents).isEmpty();
assertThat(listener.onAfterConvertEvents).isEmpty();
}
@Test // DATAMONGO-1256
public void loadEventsOnAggregation() {

View File

@@ -2537,9 +2537,15 @@ You can get at the MongoDB driver's `MongoDatabase.runCommand( )` method by usin
[[mongodb.mapping-usage.events]]
== Lifecycle Events
The MongoDB mapping framework includes several `org.springframework.context.ApplicationEvent` events that your application can respond to by registering special beans in the `ApplicationContext`. Being based on Spring's `ApplicationContext` event infrastructure enables other products, such as Spring Integration, to easily receive these events, as they are a well known eventing mechanism in Spring-based applications.
The MongoDB mapping framework includes several `org.springframework.context.ApplicationEvent` events that your application can respond to by registering special beans in the `ApplicationContext`.
Being based on Spring's `ApplicationContext` event infrastructure enables other products, such as Spring Integration, to easily receive these events, as they are a well known eventing mechanism in Spring-based applications.
To intercept an object before it goes through the conversion process (which turns your domain object into a `org.bson.Document`), you can register a subclass of `AbstractMongoEventListener` that overrides the `onBeforeConvert` method. When the event is dispatched, your listener is called and passed the domain object before it goes into the converter. The following example shows how to do so:
Entity lifecycle events can be costly and you may notice a change in the performance profile when loading large result sets.
You can disable lifecycle events on the link:https://docs.spring.io/spring-data/mongodb/docs/{version}/api/org/springframework/data/mongodb/core/MongoTemplate.html#setEntityLifecycleEventsEnabled(boolean)[Template API].
To intercept an object before it goes through the conversion process (which turns your domain object into a `org.bson.Document`), you can register a subclass of `AbstractMongoEventListener` that overrides the `onBeforeConvert` method.
When the event is dispatched, your listener is called and passed the domain object before it goes into the converter.
The following example shows how to do so:
====
[source,java]