From d37eba786ec06ef88dac456538159ce3be36bc8c Mon Sep 17 00:00:00 2001 From: Scott Murphy Heiberg Date: Thu, 4 Jun 2026 20:36:00 -0700 Subject: [PATCH] Add MongoClientSettingsBuilderCustomizer hook for the Mongo client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow application beans to customize the MongoClientSettings.Builder before the MongoClient is created for the default connection source. This is the supported extension point for settings with no grails.mongodb.* equivalent — e.g. registering a driver CommandListener for metrics/tracing, tuning the connection pool, or configuring read/write concerns programmatically. Any MongoClientSettingsBuilderCustomizer beans in the context are injected into MongoConnectionSourceFactory (registered with autowire) and applied, in order, in create(). Mirrors Spring Boot's interface of the same name. --- ...ongoClientSettingsBuilderCustomizer.groovy | 57 +++++++++++++++++++ .../MongoConnectionSourceFactory.groovy | 12 ++++ .../MongoConnectionSourceFactorySpec.groovy | 46 +++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/connections/MongoClientSettingsBuilderCustomizer.groovy diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/connections/MongoClientSettingsBuilderCustomizer.groovy b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/connections/MongoClientSettingsBuilderCustomizer.groovy new file mode 100644 index 00000000000..fd3ff8500be --- /dev/null +++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/connections/MongoClientSettingsBuilderCustomizer.groovy @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.grails.datastore.mapping.mongo.connections + +import com.mongodb.MongoClientSettings + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link MongoClientSettings.Builder} used to construct the underlying + * {@link com.mongodb.client.MongoClient} before it is created. + * + *

Any beans of this type found in the application context are applied, in order, + * to the builder for the default connection source. This is the supported extension + * point for settings that cannot be expressed through {@code grails.mongodb.*} + * configuration — for example registering a driver + * {@link com.mongodb.event.CommandListener} for metrics/tracing, tuning the connection + * pool, or configuring read/write concerns programmatically.

+ * + *
+ * @Bean
+ * MongoClientSettingsBuilderCustomizer mongoMetrics(MeterRegistry registry) {
+ *     return { builder -> builder.addCommandListener(new MongoMetricsCommandListener(registry)) }
+ * }
+ * 
+ * + *

Mirrors the semantics of Spring Boot's + * {@code MongoClientSettingsBuilderCustomizer}.

+ * + * @since 7.2 + */ +@FunctionalInterface +interface MongoClientSettingsBuilderCustomizer { + + /** + * Customize the {@link MongoClientSettings.Builder}. + * + * @param builder the builder to customize, never {@code null} + */ + void customize(MongoClientSettings.Builder builder) +} diff --git a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/connections/MongoConnectionSourceFactory.groovy b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/connections/MongoConnectionSourceFactory.groovy index 4db4ddce546..df1de862614 100644 --- a/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/connections/MongoConnectionSourceFactory.groovy +++ b/grails-data-mongodb/core/src/main/groovy/org/grails/datastore/mapping/mongo/connections/MongoConnectionSourceFactory.groovy @@ -63,6 +63,15 @@ class MongoConnectionSourceFactory extends AbstractConnectionSourceFactory codecs = [] + /** + * Optional customizers applied to the {@link MongoClientSettings.Builder} of the + * default connection source before the {@link MongoClient} is created. This is the + * supported hook for settings that have no {@code grails.mongodb.*} equivalent — e.g. + * registering a driver {@link com.mongodb.event.CommandListener} for metrics/tracing. + */ + @Autowired(required = false) + List clientSettingsCustomizers = [] + @Override Serializable getConnectionSourcesConfigurationKey() { return MongoSettings.SETTING_CONNECTIONS @@ -98,6 +107,9 @@ class MongoConnectionSourceFactory extends AbstractConnectionSourceFactory(name, client, settings) } diff --git a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/mapping/mongo/config/MongoConnectionSourceFactorySpec.groovy b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/mapping/mongo/config/MongoConnectionSourceFactorySpec.groovy index 18cc8e669ab..8a5ade99b97 100644 --- a/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/mapping/mongo/config/MongoConnectionSourceFactorySpec.groovy +++ b/grails-data-mongodb/core/src/test/groovy/org/grails/datastore/mapping/mongo/config/MongoConnectionSourceFactorySpec.groovy @@ -20,10 +20,13 @@ package org.grails.datastore.mapping.mongo.config import com.mongodb.client.MongoClient import com.mongodb.ReadPreference +import com.mongodb.MongoClientSettings +import com.mongodb.event.CommandListener import org.grails.datastore.mapping.core.DatastoreUtils import org.grails.datastore.mapping.core.connections.ConnectionSource import org.grails.datastore.mapping.core.connections.ConnectionSources import org.grails.datastore.mapping.core.connections.ConnectionSourcesInitializer +import org.grails.datastore.mapping.mongo.connections.MongoClientSettingsBuilderCustomizer import org.grails.datastore.mapping.mongo.connections.MongoConnectionSourceFactory import org.grails.datastore.mapping.mongo.connections.MongoConnectionSourceSettings import spock.lang.Specification @@ -82,4 +85,47 @@ class MongoConnectionSourceFactorySpec extends Specification { sources?.close() } + void "test a registered MongoClientSettingsBuilderCustomizer is applied to the default connection source"() { + given: "a customizer that records its invocation and registers a command listener" + CommandListener listener = new CommandListener() {} + MongoClientSettings.Builder captured = null + MongoClientSettingsBuilderCustomizer customizer = { MongoClientSettings.Builder builder -> + captured = builder + builder.addCommandListener(listener) + } + + when: "the factory builds connection sources with the customizer registered" + MongoConnectionSourceFactory factory = new MongoConnectionSourceFactory(clientSettingsCustomizers: [customizer]) + ConnectionSources sources = ConnectionSourcesInitializer.create(factory, DatastoreUtils.createPropertyResolver( + (MongoSettings.SETTING_URL): "mongodb://localhost/myDb" + )) + + then: "the customizer was invoked and its command listener applied to the built client settings" + sources.defaultConnectionSource != null + captured != null + captured.build().commandListeners.contains(listener) + + cleanup: + sources?.close() + } + + void "test multiple MongoClientSettingsBuilderCustomizers are applied in registration order"() { + given: "two customizers that append to a shared list" + List order = [] + MongoClientSettingsBuilderCustomizer first = { MongoClientSettings.Builder builder -> order << 'first' } + MongoClientSettingsBuilderCustomizer second = { MongoClientSettings.Builder builder -> order << 'second' } + + when: "the factory builds connection sources with both customizers registered" + MongoConnectionSourceFactory factory = new MongoConnectionSourceFactory(clientSettingsCustomizers: [first, second]) + ConnectionSources sources = ConnectionSourcesInitializer.create(factory, DatastoreUtils.createPropertyResolver( + (MongoSettings.SETTING_URL): "mongodb://localhost/myDb" + )) + + then: "both ran, in order" + order == ['first', 'second'] + + cleanup: + sources?.close() + } + }