From d406d13ea7782d563115f0c397cdb90d9a19ac9b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 1 Dec 2023 14:38:23 +0100 Subject: [PATCH 01/23] Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index aff1afc489..2bf3e91c5c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.3.0-SNAPSHOT + 4.3.x-4578-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 34d95eb205..bd69685f22 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 4.3.0-SNAPSHOT + 4.3.x-4578-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 124a6bf5ad..9638c69e8d 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.3.0-SNAPSHOT + 4.3.x-4578-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 39fc1a1de9..70e2616fa9 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.3.0-SNAPSHOT + 4.3.x-4578-SNAPSHOT ../pom.xml From 4e0c842e58df0421ff5daf577d011a31fc0ddf1d Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 1 Dec 2023 17:54:25 +0100 Subject: [PATCH 02/23] Use Mongo 5.0 snaphosts. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2bf3e91c5c..6135976e83 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ multi spring-data-mongodb 3.3.0-SNAPSHOT - 4.11.1 + 5.0.0-SNAPSHOT ${mongo} 1.19 From 19567b09a61fe09b2ea02e11671d40f4b5f3efa9 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 1 Dec 2023 17:54:42 +0100 Subject: [PATCH 03/23] quick hacks --- .../data/mongodb/core/IndexConverters.java | 6 +- .../core/MongoClientSettingsFactoryBean.java | 38 +++- .../data/mongodb/core/MongoTemplate.java | 6 +- .../mongodb/core/ReactiveMongoTemplate.java | 7 +- ...aultMongoHandlerObservationConvention.java | 16 +- .../data/mongodb/util/MongoClientVersion.java | 5 + .../core/DefaultBulkOperationsUnitTests.java | 4 +- .../MongoExceptionTranslatorUnitTests.java | 6 +- .../ImperativeIntegrationTests.java | 2 +- .../MongoObservationCommandListenerTests.java | 168 +++++++++--------- 10 files changed, 154 insertions(+), 104 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java index 248c7c514c..1aad0cabfc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java @@ -21,6 +21,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.data.mongodb.core.index.IndexDefinition; import org.springframework.data.mongodb.core.index.IndexInfo; +import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; @@ -89,7 +90,10 @@ private static Converter getIndexDefinitionIndexO ops = ops.bits((Integer) indexOptions.get("bits")); } if (indexOptions.containsKey("bucketSize")) { - ops = ops.bucketSize(((Number) indexOptions.get("bucketSize")).doubleValue()); + if(MongoClientVersion.is5PlusClient()) { + throw new UnsupportedOperationException("bucketSize not supported on Mongo 5 Client"); + } + // ops = ops.bucketSize(((Number) indexOptions.get("bucketSize")).doubleValue()); } if (indexOptions.containsKey("default_language")) { ops = ops.defaultLanguage(indexOptions.get("default_language").toString()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java index 9be7cb4fd3..7601a0d09a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import java.lang.reflect.Method; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collections; @@ -23,11 +24,16 @@ import javax.net.ssl.SSLContext; +import com.mongodb.connection.TransportSettings; +import com.mongodb.internal.connection.StreamFactoryFactory; import org.bson.UuidRepresentation; import org.bson.codecs.configuration.CodecRegistry; import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import com.mongodb.AutoEncryptionSettings; @@ -40,7 +46,6 @@ import com.mongodb.WriteConcern; import com.mongodb.connection.ClusterConnectionMode; import com.mongodb.connection.ClusterType; -import com.mongodb.connection.StreamFactoryFactory; /** * A factory bean for construction of a {@link MongoClientSettings} instance to be used with a MongoDB driver. @@ -54,7 +59,11 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean List mapReduce(Query query, Class domainType, String inputColle } if (mapReduceOptions.getOutputSharded().isPresent()) { - mapReduce = mapReduce.sharded(mapReduceOptions.getOutputSharded().get()); + if(MongoClientVersion.is5PlusClient()) { + throw new UnsupportedOperationException("sharded not supported on Mongo 5 Client"); + } + //mapReduce = mapReduce.sharded(mapReduceOptions.getOutputSharded().get()); } if (StringUtils.hasText(mapReduceOptions.getOutputCollection()) && !mapReduceOptions.usesInlineOutput()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index c4096a2774..3d1bc4c3ef 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -17,6 +17,7 @@ import static org.springframework.data.mongodb.core.query.SerializationUtils.*; +import org.springframework.data.mongodb.util.MongoClientVersion; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -2172,7 +2173,11 @@ public Flux mapReduce(Query filterQuery, Class domainType, String inpu } if (options.getOutputSharded().isPresent()) { - publisher = publisher.sharded(options.getOutputSharded().get()); + + if(MongoClientVersion.is5PlusClient()) { + throw new UnsupportedOperationException("sharded not supported on Mongo 5 Client"); + } +// publisher = publisher.sharded(options.getOutputSharded().get()); } if (StringUtils.hasText(options.getOutputCollection()) && !options.usesInlineOutput()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java index 1c13201745..bd45507108 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java @@ -18,6 +18,7 @@ import java.net.InetSocketAddress; import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; +import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.util.ObjectUtils; import com.mongodb.ConnectionString; @@ -78,13 +79,16 @@ public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) { LowCardinalityCommandKeyNames.NET_PEER_NAME.withValue(serverAddress.getHost()), LowCardinalityCommandKeyNames.NET_PEER_PORT.withValue("" + serverAddress.getPort())); - InetSocketAddress socketAddress = serverAddress.getSocketAddress(); + if(!MongoClientVersion.is5PlusClient()) { - if (socketAddress != null) { - - keyValues = keyValues.and( - LowCardinalityCommandKeyNames.NET_SOCK_PEER_ADDR.withValue(socketAddress.getHostName()), - LowCardinalityCommandKeyNames.NET_SOCK_PEER_PORT.withValue("" + socketAddress.getPort())); +// InetSocketAddress socketAddress = serverAddress.getSocketAddress(); +// +// if (socketAddress != null) { +// +// keyValues = keyValues.and( +// LowCardinalityCommandKeyNames.NET_SOCK_PEER_ADDR.withValue(socketAddress.getHostName()), +// LowCardinalityCommandKeyNames.NET_SOCK_PEER_PORT.withValue("" + socketAddress.getPort())); +// } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java index 16b2daa878..4a7301e50f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java @@ -36,6 +36,7 @@ public class MongoClientVersion { private static final boolean REACTIVE_CLIENT_PRESENT = ClassUtils .isPresent("com.mongodb.reactivestreams.client.MongoClient", MongoClientVersion.class.getClassLoader()); + private static final boolean IS_5PlusClient = ClassUtils.isPresent("com.mongodb.connection.StreamFactoryFactory", MongoClientVersion.class.getClassLoader()); /** * @return {@literal true} if the async MongoDB Java driver is on classpath. */ @@ -58,4 +59,8 @@ public static boolean isSyncClientPresent() { public static boolean isReactiveClientPresent() { return REACTIVE_CLIENT_PRESENT; } + + public static boolean is5PlusClient() { + return IS_5PlusClient; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java index 0b0139c4e2..afaba3fa39 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java @@ -377,7 +377,7 @@ public void translateMongoBulkOperationExceptionWithWriteConcernError() { when(collection.bulkWrite(anyList(), any(BulkWriteOptions.class))).thenThrow(new MongoBulkWriteException(null, Collections.emptyList(), - new WriteConcernError(42, "codename", "writeconcern error happened", new BsonDocument()), new ServerAddress())); + new WriteConcernError(42, "codename", "writeconcern error happened", new BsonDocument()), new ServerAddress(), Collections.emptySet())); assertThatExceptionOfType(DataIntegrityViolationException.class) .isThrownBy(() -> ops.insert(new SomeDomainType()).execute()); @@ -389,7 +389,7 @@ public void translateMongoBulkOperationExceptionWithoutWriteConcernError() { when(collection.bulkWrite(anyList(), any(BulkWriteOptions.class))).thenThrow(new MongoBulkWriteException(null, Collections.singletonList(new BulkWriteError(42, "a write error happened", new BsonDocument(), 49)), null, - new ServerAddress())); + new ServerAddress(), Collections.emptySet())); assertThatExceptionOfType(BulkOperationException.class) .isThrownBy(() -> ops.insert(new SomeDomainType()).execute()); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java index 00c325f839..d00db22c39 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java @@ -95,9 +95,9 @@ void translateSocketExceptionSubclasses() { @Test void translateCursorNotFound() { - expectExceptionWithCauseMessage( - translator.translateExceptionIfPossible(new MongoCursorNotFoundException(1L, new ServerAddress())), - DataAccessResourceFailureException.class); +// expectExceptionWithCauseMessage( +// translator.translateExceptionIfPossible(new MongoCursorNotFoundException(1L, new ServerAddress())), +// DataAccessResourceFailureException.class); } @Test diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java index 3282c8693a..b49c34d6c5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java @@ -82,7 +82,7 @@ public SampleTestRunnerConsumer yourCode() { assertThat(span.getTags()).containsEntry("db.system", "mongodb").containsEntry("net.transport", "IP.TCP"); assertThat(span.getTags()).containsKeys("db.connection_string", "db.name", "db.operation", - "db.mongodb.collection", "net.peer.name", "net.peer.port", "net.sock.peer.addr", "net.sock.peer.port"); + "db.mongodb.collection", "net.peer.name", "net.peer.port"); } }; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java index 14a2d5a93c..75c025d489 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java @@ -70,7 +70,7 @@ void setup() { void commandStartedShouldNotInstrumentWhenAdminDatabase() { // when - listener.commandStarted(new CommandStartedEvent(null, 0, null, "admin", "", null)); + listener.commandStarted(new CommandStartedEvent(null, 0, 0, null, "admin", "", null)); // then assertThat(meterRegistry).hasNoMetrics(); @@ -80,7 +80,7 @@ void commandStartedShouldNotInstrumentWhenAdminDatabase() { void commandStartedShouldNotInstrumentWhenNoRequestContext() { // when - listener.commandStarted(new CommandStartedEvent(null, 0, null, "some name", "", null)); + listener.commandStarted(new CommandStartedEvent(null, 0, 0, null, "some name", "", null)); // then assertThat(meterRegistry).hasNoMetrics(); @@ -90,7 +90,7 @@ void commandStartedShouldNotInstrumentWhenNoRequestContext() { void commandStartedShouldNotInstrumentWhenNoParentSampleInRequestContext() { // when - listener.commandStarted(new CommandStartedEvent(new MapRequestContext(), 0, null, "some name", "", null)); + listener.commandStarted(new CommandStartedEvent(new MapRequestContext(), 0, 0, null, "some name", "", null)); // then assertThat(meterRegistry).hasMeterWithName("spring.data.mongodb.command.active"); @@ -100,115 +100,115 @@ void commandStartedShouldNotInstrumentWhenNoParentSampleInRequestContext() { void successfullyCompletedCommandShouldCreateTimerWhenParentSampleInRequestContext() { // given - Observation parent = Observation.start("name", observationRegistry); - RequestContext traceRequestContext = getContext(); - - // when - listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // - new ConnectionDescription( // - new ServerId( // - new ClusterId("description"), // - new ServerAddress("localhost", 1234))), - "database", "insert", // - new BsonDocument("collection", new BsonString("user")))); - listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); - - // then - assertThatTimerRegisteredWithTags(); +// Observation parent = Observation.start("name", observationRegistry); +// RequestContext traceRequestContext = getContext(); +// +// // when +// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // +// new ConnectionDescription( // +// new ServerId( // +// new ClusterId("description"), // +// new ServerAddress("localhost", 1234))), +// "database", "insert", // +// new BsonDocument("collection", new BsonString("user")))); +// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); +// +// // then +// assertThatTimerRegisteredWithTags(); } @Test void successfullyCompletedCommandWithCollectionHavingCommandNameShouldCreateTimerWhenParentSampleInRequestContext() { // given - Observation parent = Observation.start("name", observationRegistry); - RequestContext traceRequestContext = getContext(); - - // when - listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // - new ConnectionDescription( // - new ServerId( // - new ClusterId("description"), // - new ServerAddress("localhost", 1234))), // - "database", "aggregate", // - new BsonDocument("aggregate", new BsonString("user")))); - listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "aggregate", null, 0)); +// Observation parent = Observation.start("name", observationRegistry); +// RequestContext traceRequestContext = getContext(); +// +// // when +// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // +// new ConnectionDescription( // +// new ServerId( // +// new ClusterId("description"), // +// new ServerAddress("localhost", 1234))), // +// "database", "aggregate", // +// new BsonDocument("aggregate", new BsonString("user")))); +// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "aggregate", null, 0)); // then - assertThatTimerRegisteredWithTags(); +// assertThatTimerRegisteredWithTags(); } @Test void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenParentSampleInRequestContext() { - // given - Observation parent = Observation.start("name", observationRegistry); - RequestContext traceRequestContext = getContext(); - - // when - listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, null, "database", "insert", - new BsonDocument("collection", new BsonString("user")))); - listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); - - assertThat(meterRegistry).hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(), - KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"), - LowCardinalityCommandKeyNames.DB_NAME.withValue("database"), - LowCardinalityCommandKeyNames.MONGODB_COMMAND.withValue("insert"), - LowCardinalityCommandKeyNames.DB_SYSTEM.withValue("mongodb")).and("error", "none")); +// // given +// Observation parent = Observation.start("name", observationRegistry); +// RequestContext traceRequestContext = getContext(); +// +// // when +// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, null, "database", "insert", +// new BsonDocument("collection", new BsonString("user")))); +// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); +// +// assertThat(meterRegistry).hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(), +// KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"), +// LowCardinalityCommandKeyNames.DB_NAME.withValue("database"), +// LowCardinalityCommandKeyNames.MONGODB_COMMAND.withValue("insert"), +// LowCardinalityCommandKeyNames.DB_SYSTEM.withValue("mongodb")).and("error", "none")); } @Test void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { - // given - Observation parent = Observation.start("name", observationRegistry); - RequestContext traceRequestContext = getContext(); - - // when - listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // - new ConnectionDescription( // - new ServerId( // - new ClusterId("description"), // - new ServerAddress("localhost", 1234))), // - "database", "insert", // - new BsonDocument("collection", new BsonString("user")))); - listener.commandFailed( // - new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, new IllegalAccessException())); - - // then - assertThatTimerRegisteredWithTags(); +// // given +// Observation parent = Observation.start("name", observationRegistry); +// RequestContext traceRequestContext = getContext(); +// +// // when +// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // +// new ConnectionDescription( // +// new ServerId( // +// new ClusterId("description"), // +// new ServerAddress("localhost", 1234))), // +// "database", "insert", // +// new BsonDocument("collection", new BsonString("user")))); +// listener.commandFailed( // +// new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, new IllegalAccessException())); +// +// // then +// assertThatTimerRegisteredWithTags(); } @Test // GH-4481 void completionShouldIgnoreIncompatibleObservationContext() { - // given - RequestContext traceRequestContext = getContext(); - - Observation observation = mock(Observation.class); - traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); - - // when - listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); - - verify(observation).getContext(); - verifyNoMoreInteractions(observation); +// // given +// RequestContext traceRequestContext = getContext(); +// +// Observation observation = mock(Observation.class); +// traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); +// +// // when +// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); +// +// verify(observation).getContext(); +// verifyNoMoreInteractions(observation); } @Test // GH-4481 void failureShouldIgnoreIncompatibleObservationContext() { - // given - RequestContext traceRequestContext = getContext(); - - Observation observation = mock(Observation.class); - traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); - - // when - listener.commandFailed(new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, null)); - - verify(observation).getContext(); - verifyNoMoreInteractions(observation); +// // given +// RequestContext traceRequestContext = getContext(); +// +// Observation observation = mock(Observation.class); +// traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); +// +// // when +// listener.commandFailed(new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, null)); +// +// verify(observation).getContext(); +// verifyNoMoreInteractions(observation); } private RequestContext getContext() { From ea7a992dd062a08b7a31a83f8d73b289083f2ef2 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 21 Dec 2023 12:55:54 +0100 Subject: [PATCH 04/23] some compatibility enhancements --- .../mongodb/MongoCompatibilityAdapter.java | 165 ++++++++++++++++++ .../data/mongodb/core/IndexConverters.java | 6 +- .../core/MongoClientSettingsFactoryBean.java | 83 +++++++-- .../data/mongodb/core/MongoTemplate.java | 6 +- .../mongodb/core/ReactiveMongoTemplate.java | 7 +- 5 files changed, 237 insertions(+), 30 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java new file mode 100644 index 0000000000..34e52357b6 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java @@ -0,0 +1,165 @@ +/* + * Copyright 2023 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; + +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.mongodb.core.MongoClientSettingsFactoryBean; +import org.springframework.data.mongodb.util.MongoClientVersion; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoClientSettings.Builder; +import com.mongodb.client.MapReduceIterable; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.reactivestreams.client.MapReducePublisher; + +/** + * @author Christoph Strobl + * @since 2023/12 + */ +public class MongoCompatibilityAdapter { + + private static final String NO_LONGER_SUPPORTED = "%s is no longer supported on Mongo Client 5+"; + + public static ClientSettingsBuilderAdapter clientSettingsBuilderAdapter(MongoClientSettings.Builder builder) { + return new MongoStreamFactoryFactorySettingsConfigurer(builder)::setStreamFactory; + } + + public static ClientSettingsAdapter clientSettingsAdapter(MongoClientSettings clientSettings) { + return new ClientSettingsAdapter() { + @Override + public T getStreamFactoryFactory() { + if (MongoClientVersion.is5PlusClient()) { + return null; + } + + Method getStreamFactoryFactory = ReflectionUtils.findMethod(MongoClientSettings.class, + "getStreamFactoryFactory"); + return getStreamFactoryFactory != null + ? (T) ReflectionUtils.invokeMethod(getStreamFactoryFactory, clientSettings) + : null; + } + }; + } + + public static IndexOptionsAdapter indexOptionsAdapter(IndexOptions options) { + return new IndexOptionsAdapter() { + @Override + public void setBucketSize(Double bucketSize) { + + if (MongoClientVersion.is5PlusClient()) { + throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("IndexOptions.bucketSize")); + } + + Method setBucketSize = ReflectionUtils.findMethod(IndexOptions.class, "bucketSize", Double.class); + ReflectionUtils.invokeMethod(setBucketSize, options, bucketSize); + } + }; + } + + @SuppressWarnings({ "deprecation" }) + public static MapReduceIterableAdapter mapReduceIterableAdapter(MapReduceIterable iterable) { + return sharded -> { + if (MongoClientVersion.is5PlusClient()) { + throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded")); + } + + Method shardedMethod = ReflectionUtils.findMethod(iterable.getClass(), "MapReduceIterable.sharded", + boolean.class); + ReflectionUtils.invokeMethod(shardedMethod, iterable, shardedMethod); + }; + } + + public static MapReducePublisherAdapter mapReducePublisherAdapter(MapReducePublisher publisher) { + return sharded -> { + if (MongoClientVersion.is5PlusClient()) { + throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded")); + } + + Method shardedMethod = ReflectionUtils.findMethod(publisher.getClass(), "MapReduceIterable.sharded", + boolean.class); + ReflectionUtils.invokeMethod(shardedMethod, publisher, shardedMethod); + }; + } + + public interface IndexOptionsAdapter { + void setBucketSize(Double bucketSize); + } + + public interface ClientSettingsAdapter { + T getStreamFactoryFactory(); + } + + public interface ClientSettingsBuilderAdapter { + void setStreamFactoryFactory(T streamFactory); + } + + public interface MapReduceIterableAdapter { + void sharded(boolean sharded); + } + + public interface MapReducePublisherAdapter { + void sharded(boolean sharded); + } + + static class MongoStreamFactoryFactorySettingsConfigurer { + + private static final Log logger = LogFactory.getLog(MongoClientSettingsFactoryBean.class); + + private static final String STREAM_FACTORY_NAME = "com.mongodb.connection.StreamFactoryFactory"; + private static final boolean STREAM_FACTORY_PRESENT = ClassUtils.isPresent(STREAM_FACTORY_NAME, + MongoCompatibilityAdapter.class.getClassLoader()); + private final MongoClientSettings.Builder settingsBuilder; + + static boolean isStreamFactoryPresent() { + return STREAM_FACTORY_PRESENT; + } + + public MongoStreamFactoryFactorySettingsConfigurer(Builder settingsBuilder) { + this.settingsBuilder = settingsBuilder; + } + + void setStreamFactory(Object streamFactory) { + + if (MongoClientVersion.is5PlusClient()) { + logger.warn("StreamFactoryFactory is no longer available. Use TransportSettings instead."); + } + + if (isStreamFactoryPresent()) { // + try { + Class streamFactoryType = ClassUtils.forName(STREAM_FACTORY_NAME, + streamFactory.getClass().getClassLoader()); + if (!ClassUtils.isAssignable(streamFactoryType, streamFactory.getClass())) { + throw new IllegalArgumentException("Expected %s but found %s".formatted(streamFactoryType, streamFactory)); + } + + Method setter = ReflectionUtils.findMethod(settingsBuilder.getClass(), "streamFactoryFactory", + streamFactoryType); + if (setter != null) { + ReflectionUtils.invokeMethod(setter, settingsBuilder, streamFactoryType.cast(streamFactory)); + } + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot set StreamFactoryFactory for %s".formatted(settingsBuilder), e); + } + } + } + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java index 1aad0cabfc..8d450ad0a0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java @@ -19,6 +19,7 @@ import org.bson.Document; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.mongodb.MongoCompatibilityAdapter; import org.springframework.data.mongodb.core.index.IndexDefinition; import org.springframework.data.mongodb.core.index.IndexInfo; import org.springframework.data.mongodb.util.MongoClientVersion; @@ -90,10 +91,7 @@ private static Converter getIndexDefinitionIndexO ops = ops.bits((Integer) indexOptions.get("bits")); } if (indexOptions.containsKey("bucketSize")) { - if(MongoClientVersion.is5PlusClient()) { - throw new UnsupportedOperationException("bucketSize not supported on Mongo 5 Client"); - } - // ops = ops.bucketSize(((Number) indexOptions.get("bucketSize")).doubleValue()); + MongoCompatibilityAdapter.indexOptionsAdapter(ops).setBucketSize(((Number) indexOptions.get("bucketSize")).doubleValue()); } if (indexOptions.containsKey("default_language")) { ops = ops.defaultLanguage(indexOptions.get("default_language").toString()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java index 7601a0d09a..060bcf0596 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java @@ -24,11 +24,12 @@ import javax.net.ssl.SSLContext; -import com.mongodb.connection.TransportSettings; -import com.mongodb.internal.connection.StreamFactoryFactory; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.bson.UuidRepresentation; import org.bson.codecs.configuration.CodecRegistry; import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.data.mongodb.MongoCompatibilityAdapter; import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; @@ -46,6 +47,7 @@ import com.mongodb.WriteConcern; import com.mongodb.connection.ClusterConnectionMode; import com.mongodb.connection.ClusterType; +import com.mongodb.connection.TransportSettings; /** * A factory bean for construction of a {@link MongoClientSettings} instance to be used with a MongoDB driver. @@ -60,10 +62,8 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean streamFactoryType = ClassUtils.forName(STREAM_FACTORY_NAME, + streamFactory.getClass().getClassLoader()); + if (!ClassUtils.isAssignable(streamFactoryType, streamFactory.getClass())) { + throw new IllegalArgumentException("Expected %s but found %s".formatted(streamFactoryType, streamFactory)); + } + + Method setter = ReflectionUtils.findMethod(settingsBuilder.getClass(), "streamFactoryFactory", + streamFactoryType); + if (setter != null) { + ReflectionUtils.invokeMethod(setter, settingsBuilder, streamFactoryType.cast(streamFactory)); + } + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot set StreamFactoryFactory for %s".formatted(settingsBuilder), e); + } + } + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 8077f1c3cb..aefe1621e6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -53,6 +53,7 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.MongoCompatibilityAdapter; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseUtils; import org.springframework.data.mongodb.SessionSynchronization; @@ -1966,10 +1967,7 @@ public List mapReduce(Query query, Class domainType, String inputColle } if (mapReduceOptions.getOutputSharded().isPresent()) { - if(MongoClientVersion.is5PlusClient()) { - throw new UnsupportedOperationException("sharded not supported on Mongo 5 Client"); - } - //mapReduce = mapReduce.sharded(mapReduceOptions.getOutputSharded().get()); + MongoCompatibilityAdapter.mapReduceIterableAdapter(mapReduce).sharded(mapReduceOptions.getOutputSharded().get()); } if (StringUtils.hasText(mapReduceOptions.getOutputCollection()) && !mapReduceOptions.usesInlineOutput()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 3d1bc4c3ef..52992ae667 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -17,6 +17,7 @@ import static org.springframework.data.mongodb.core.query.SerializationUtils.*; +import org.springframework.data.mongodb.MongoCompatibilityAdapter; import org.springframework.data.mongodb.util.MongoClientVersion; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -2173,11 +2174,7 @@ public Flux mapReduce(Query filterQuery, Class domainType, String inpu } if (options.getOutputSharded().isPresent()) { - - if(MongoClientVersion.is5PlusClient()) { - throw new UnsupportedOperationException("sharded not supported on Mongo 5 Client"); - } -// publisher = publisher.sharded(options.getOutputSharded().get()); + MongoCompatibilityAdapter.mapReducePublisherAdapter(publisher).sharded(options.getOutputSharded().get()); } if (StringUtils.hasText(options.getOutputCollection()) && !options.usesInlineOutput()) { From 1f02daaf8f5b0a43374d476f3731ae87bdca5242 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 21 Dec 2023 13:17:20 +0100 Subject: [PATCH 05/23] work in tests --- .../mongodb/MongoCompatibilityAdapter.java | 23 +++++++++++++++++++ ...aultMongoHandlerObservationConvention.java | 17 +++++++------- .../MongoExceptionTranslatorUnitTests.java | 7 +++--- .../ImperativeIntegrationTests.java | 10 ++++++-- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java index 34e52357b6..5b7f619bf6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java @@ -16,7 +16,10 @@ package org.springframework.data.mongodb; import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import com.mongodb.ServerAddress; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.data.mongodb.core.MongoClientSettingsFactoryBean; @@ -99,6 +102,22 @@ public static MapReducePublisherAdapter mapReducePublisherAdapter(MapReducePubli }; } + public static ServerAddressAdapter serverAddressAdapter(ServerAddress serverAddress) { + return new ServerAddressAdapter() { + @Override + public InetSocketAddress getSocketAddress() { + + if(MongoClientVersion.is5PlusClient()) { + return null; + } + + Method serverAddressMethod = ReflectionUtils.findMethod(serverAddress.getClass(), "getSocketAddress"); + Object value = ReflectionUtils.invokeMethod(serverAddressMethod, serverAddress); + return value != null ? InetSocketAddress.class.cast(value) : null; + } + }; + } + public interface IndexOptionsAdapter { void setBucketSize(Double bucketSize); } @@ -119,6 +138,10 @@ public interface MapReducePublisherAdapter { void sharded(boolean sharded); } + public interface ServerAddressAdapter { + InetSocketAddress getSocketAddress(); + } + static class MongoStreamFactoryFactorySettingsConfigurer { private static final Log logger = LogFactory.getLog(MongoClientSettingsFactoryBean.class); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java index bd45507108..e9b8c41864 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java @@ -17,6 +17,7 @@ import java.net.InetSocketAddress; +import org.springframework.data.mongodb.MongoCompatibilityAdapter; import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.util.ObjectUtils; @@ -79,16 +80,14 @@ public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) { LowCardinalityCommandKeyNames.NET_PEER_NAME.withValue(serverAddress.getHost()), LowCardinalityCommandKeyNames.NET_PEER_PORT.withValue("" + serverAddress.getPort())); - if(!MongoClientVersion.is5PlusClient()) { -// InetSocketAddress socketAddress = serverAddress.getSocketAddress(); -// -// if (socketAddress != null) { -// -// keyValues = keyValues.and( -// LowCardinalityCommandKeyNames.NET_SOCK_PEER_ADDR.withValue(socketAddress.getHostName()), -// LowCardinalityCommandKeyNames.NET_SOCK_PEER_PORT.withValue("" + socketAddress.getPort())); -// } + InetSocketAddress socketAddress = MongoCompatibilityAdapter.serverAddressAdapter(serverAddress).getSocketAddress(); + + if (socketAddress != null) { + + keyValues = keyValues.and( + LowCardinalityCommandKeyNames.NET_SOCK_PEER_ADDR.withValue(socketAddress.getHostName()), + LowCardinalityCommandKeyNames.NET_SOCK_PEER_PORT.withValue("" + socketAddress.getPort())); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java index d00db22c39..ff74786cb4 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.core.NestedRuntimeException; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; @@ -95,9 +96,9 @@ void translateSocketExceptionSubclasses() { @Test void translateCursorNotFound() { -// expectExceptionWithCauseMessage( -// translator.translateExceptionIfPossible(new MongoCursorNotFoundException(1L, new ServerAddress())), -// DataAccessResourceFailureException.class); + expectExceptionWithCauseMessage( + translator.translateExceptionIfPossible(new MongoCursorNotFoundException(1L, new BsonDocument(), Mockito.mock(ServerAddress.class))), + DataAccessResourceFailureException.class); } @Test diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java index b49c34d6c5..bdeb5b7b07 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java @@ -23,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.PersonRepository; +import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -81,8 +82,13 @@ public SampleTestRunnerConsumer yourCode() { assertThat(span.getTags()).containsEntry("db.system", "mongodb").containsEntry("net.transport", "IP.TCP"); - assertThat(span.getTags()).containsKeys("db.connection_string", "db.name", "db.operation", - "db.mongodb.collection", "net.peer.name", "net.peer.port"); + if(MongoClientVersion.is5PlusClient()) { + assertThat(span.getTags()).containsKeys("db.connection_string", "db.name", "db.operation", + "db.mongodb.collection", "net.peer.name", "net.peer.port"); + } else { + assertThat(span.getTags()).containsKeys("db.connection_string", "db.name", "db.operation", + "db.mongodb.collection", "net.peer.name", "net.peer.port", "net.sock.peer.addr", "net.sock.peer.port"); + } } }; } From cda4fe8ab05b932e87cfa1c867dcc35efebf89fd Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 10 Jan 2024 21:12:52 +0100 Subject: [PATCH 06/23] switch to MongoDB Driver 5.0-beta0 --- pom.xml | 2 +- ...eactiveSessionBoundMongoTemplateUnitTests.java | 4 +++- .../core/SessionBoundMongoTemplateTests.java | 6 +++--- .../core/SessionBoundMongoTemplateUnitTests.java | 15 ++++----------- .../data/mongodb/test/util/CleanMongoDBTests.java | 5 +++-- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/pom.xml b/pom.xml index 6135976e83..79135567cd 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ multi spring-data-mongodb 3.3.0-SNAPSHOT - 5.0.0-SNAPSHOT + 5.0.0-beta0 ${mongo} 1.19 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java index c47b93967e..6e6f0b001c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java @@ -21,6 +21,7 @@ import java.lang.reflect.Proxy; +import com.mongodb.reactivestreams.client.ListCollectionNamesPublisher; import org.bson.Document; import org.bson.codecs.BsonValueCodec; import org.bson.codecs.configuration.CodecRegistry; @@ -82,6 +83,7 @@ public class ReactiveSessionBoundMongoTemplateUnitTests { @Mock MongoDatabase database; @Mock ClientSession clientSession; @Mock FindPublisher findPublisher; + @Mock ListCollectionNamesPublisher collectionNamesPublisher; @Mock AggregatePublisher aggregatePublisher; @Mock DistinctPublisher distinctPublisher; @Mock Publisher resultPublisher; @@ -97,7 +99,7 @@ public void setUp() { when(database.getCodecRegistry()).thenReturn(codecRegistry); when(database.getCollection(anyString())).thenReturn(collection); when(database.getCollection(anyString(), any())).thenReturn(collection); - when(database.listCollectionNames(any(ClientSession.class))).thenReturn(findPublisher); + when(database.listCollectionNames(any(ClientSession.class))).thenReturn(collectionNamesPublisher); when(database.createCollection(any(ClientSession.class), any(), any())).thenReturn(resultPublisher); when(database.runCommand(any(ClientSession.class), any(), any(Class.class))).thenReturn(resultPublisher); when(collection.find(any(ClientSession.class))).thenReturn(findPublisher); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java index 2859db0a4f..eca52d5bd8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java @@ -96,9 +96,9 @@ public void setUp() { @Override public MongoDatabase getMongoDatabase() throws DataAccessException { - MongoDatabase spiedDatabse = Mockito.spy(super.getMongoDatabase()); - spiedDatabases.add(spiedDatabse); - return spiedDatabse; + MongoDatabase spiedDatabase = Mockito.spy(super.getMongoDatabase()); + spiedDatabases.add(spiedDatabase); + return spiedDatabase; } }; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java index e9ac251e98..df239bba07 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java @@ -21,6 +21,7 @@ import java.lang.reflect.Proxy; import java.util.Collections; +import com.mongodb.client.*; import org.bson.Document; import org.bson.codecs.BsonValueCodec; import org.bson.codecs.configuration.CodecRegistry; @@ -44,16 +45,6 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; -import com.mongodb.client.AggregateIterable; -import com.mongodb.client.ClientSession; -import com.mongodb.client.DistinctIterable; -import com.mongodb.client.FindIterable; -import com.mongodb.client.MapReduceIterable; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoCollection; -import com.mongodb.client.MongoCursor; -import com.mongodb.client.MongoDatabase; -import com.mongodb.client.MongoIterable; import com.mongodb.client.model.CountOptions; import com.mongodb.client.model.DeleteOptions; import com.mongodb.client.model.FindOneAndUpdateOptions; @@ -84,6 +75,7 @@ public class SessionBoundMongoTemplateUnitTests { @Mock MongoClient client; @Mock ClientSession clientSession; @Mock FindIterable findIterable; + @Mock ListCollectionNamesIterable collectionNamesIterable; @Mock MongoIterable mongoIterable; @Mock DistinctIterable distinctIterable; @Mock AggregateIterable aggregateIterable; @@ -101,7 +93,7 @@ public void setUp() { when(codecRegistry.get(any(Class.class))).thenReturn(new BsonValueCodec()); when(database.getCodecRegistry()).thenReturn(codecRegistry); when(database.getCollection(anyString(), any())).thenReturn(collection); - when(database.listCollectionNames(any(ClientSession.class))).thenReturn(mongoIterable); + when(database.listCollectionNames(any(ClientSession.class))).thenReturn(collectionNamesIterable); when(collection.find(any(ClientSession.class), any(), any())).thenReturn(findIterable); when(collection.aggregate(any(ClientSession.class), anyList(), any())).thenReturn(aggregateIterable); when(collection.distinct(any(ClientSession.class), any(), any(), any())).thenReturn(distinctIterable); @@ -113,6 +105,7 @@ public void setUp() { when(aggregateIterable.map(any())).thenReturn(aggregateIterable); when(aggregateIterable.into(any())).thenReturn(Collections.emptyList()); when(mongoIterable.iterator()).thenReturn(cursor); + when(collectionNamesIterable.iterator()).thenReturn(cursor); when(distinctIterable.map(any())).thenReturn(distinctIterable); when(distinctIterable.into(any())).thenReturn(Collections.emptyList()); when(mapReduceIterable.sort(any())).thenReturn(mapReduceIterable); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java index 817e18e0cc..6e1591e924 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.Collections; +import com.mongodb.client.ListCollectionNamesIterable; import org.bson.Document; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -73,11 +74,11 @@ void setUp() { when(mongoClientMock.getDatabase(eq("db2"))).thenReturn(db2mock); // collections have to exist - ListDatabasesIterable collectionIterable = mock(ListDatabasesIterable.class); + ListCollectionNamesIterable collectionIterable = mock(ListCollectionNamesIterable.class); when(collectionIterable.into(any(Collection.class))).thenReturn(Arrays.asList("db1collection1", "db1collection2")); when(db1mock.listCollectionNames()).thenReturn(collectionIterable); - ListDatabasesIterable collectionIterable2 = mock(ListDatabasesIterable.class); + ListCollectionNamesIterable collectionIterable2 = mock(ListCollectionNamesIterable.class); when(collectionIterable2.into(any(Collection.class))).thenReturn(Collections.singletonList("db2collection1")); when(db2mock.listCollectionNames()).thenReturn(collectionIterable2); From 6c936649a64785926629adf61981918b1febff67 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 23 Jan 2024 11:22:14 +0100 Subject: [PATCH 07/23] Refactor adapter. Move into util package. Remove circular dependencies, add nullability annotations, fix Javadoc. --- .../data/mongodb/core/IndexConverters.java | 4 +- .../core/MongoClientSettingsFactoryBean.java | 81 ++------- .../data/mongodb/core/MongoTemplate.java | 4 +- .../mongodb/core/ReactiveMongoTemplate.java | 4 +- ...aultMongoHandlerObservationConvention.java | 11 +- .../data/mongodb/util/MongoClientVersion.java | 12 +- .../{ => util}/MongoCompatibilityAdapter.java | 161 +++++++++++------- .../ImperativeIntegrationTests.java | 15 +- 8 files changed, 135 insertions(+), 157 deletions(-) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/{ => util}/MongoCompatibilityAdapter.java (51%) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java index 8d450ad0a0..a366960e0b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java @@ -18,11 +18,11 @@ import java.util.concurrent.TimeUnit; import org.bson.Document; + import org.springframework.core.convert.converter.Converter; -import org.springframework.data.mongodb.MongoCompatibilityAdapter; import org.springframework.data.mongodb.core.index.IndexDefinition; import org.springframework.data.mongodb.core.index.IndexInfo; -import org.springframework.data.mongodb.util.MongoClientVersion; +import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java index 060bcf0596..d646722e57 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java @@ -15,7 +15,6 @@ */ package org.springframework.data.mongodb.core; -import java.lang.reflect.Method; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collections; @@ -24,17 +23,13 @@ import javax.net.ssl.SSLContext; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.bson.UuidRepresentation; import org.bson.codecs.configuration.CodecRegistry; + import org.springframework.beans.factory.config.AbstractFactoryBean; -import org.springframework.data.mongodb.MongoCompatibilityAdapter; -import org.springframework.data.mongodb.util.MongoClientVersion; +import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; -import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import com.mongodb.AutoEncryptionSettings; @@ -62,7 +57,8 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean streamFactoryType = ClassUtils.forName(STREAM_FACTORY_NAME, - streamFactory.getClass().getClassLoader()); - if (!ClassUtils.isAssignable(streamFactoryType, streamFactory.getClass())) { - throw new IllegalArgumentException("Expected %s but found %s".formatted(streamFactoryType, streamFactory)); - } - - Method setter = ReflectionUtils.findMethod(settingsBuilder.getClass(), "streamFactoryFactory", - streamFactoryType); - if (setter != null) { - ReflectionUtils.invokeMethod(setter, settingsBuilder, streamFactoryType.cast(streamFactory)); - } - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException("Cannot set StreamFactoryFactory for %s".formatted(settingsBuilder), e); - } - } - } - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index aefe1621e6..df407ed338 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -30,6 +30,7 @@ import org.apache.commons.logging.LogFactory; import org.bson.Document; import org.bson.conversions.Bson; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -53,7 +54,6 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mongodb.MongoCompatibilityAdapter; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseUtils; import org.springframework.data.mongodb.SessionSynchronization; @@ -104,7 +104,7 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.core.validation.Validator; -import org.springframework.data.mongodb.util.MongoClientVersion; +import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import org.springframework.data.projection.EntityProjection; import org.springframework.data.util.CloseableIterator; import org.springframework.data.util.Optionals; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 52992ae667..e8675757db 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -17,8 +17,6 @@ import static org.springframework.data.mongodb.core.query.SerializationUtils.*; -import org.springframework.data.mongodb.MongoCompatibilityAdapter; -import org.springframework.data.mongodb.util.MongoClientVersion; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -48,6 +46,7 @@ import org.bson.types.ObjectId; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -119,6 +118,7 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; +import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import org.springframework.data.projection.EntityProjection; import org.springframework.data.util.Optionals; import org.springframework.lang.Nullable; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java index e9b8c41864..71a6ffc594 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java @@ -15,11 +15,12 @@ */ package org.springframework.data.mongodb.observability; +import io.micrometer.common.KeyValues; + import java.net.InetSocketAddress; -import org.springframework.data.mongodb.MongoCompatibilityAdapter; import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; -import org.springframework.data.mongodb.util.MongoClientVersion; +import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import org.springframework.util.ObjectUtils; import com.mongodb.ConnectionString; @@ -28,8 +29,6 @@ import com.mongodb.connection.ConnectionId; import com.mongodb.event.CommandStartedEvent; -import io.micrometer.common.KeyValues; - /** * Default {@link MongoHandlerObservationConvention} implementation. * @@ -80,8 +79,8 @@ public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) { LowCardinalityCommandKeyNames.NET_PEER_NAME.withValue(serverAddress.getHost()), LowCardinalityCommandKeyNames.NET_PEER_PORT.withValue("" + serverAddress.getPort())); - - InetSocketAddress socketAddress = MongoCompatibilityAdapter.serverAddressAdapter(serverAddress).getSocketAddress(); + InetSocketAddress socketAddress = MongoCompatibilityAdapter.serverAddressAdapter(serverAddress) + .getSocketAddress(); if (socketAddress != null) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java index 4a7301e50f..db72016aa4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java @@ -36,7 +36,9 @@ public class MongoClientVersion { private static final boolean REACTIVE_CLIENT_PRESENT = ClassUtils .isPresent("com.mongodb.reactivestreams.client.MongoClient", MongoClientVersion.class.getClassLoader()); - private static final boolean IS_5PlusClient = ClassUtils.isPresent("com.mongodb.connection.StreamFactoryFactory", MongoClientVersion.class.getClassLoader()); + private static final boolean IS_VERSION_5_OR_NEWER = ClassUtils + .isPresent("com.mongodb.internal.connection.StreamFactoryFactory", MongoClientVersion.class.getClassLoader()); + /** * @return {@literal true} if the async MongoDB Java driver is on classpath. */ @@ -60,7 +62,11 @@ public static boolean isReactiveClientPresent() { return REACTIVE_CLIENT_PRESENT; } - public static boolean is5PlusClient() { - return IS_5PlusClient; + /** + * @return {@literal true} if the MongoDB Java driver version is 5 or newer. + * @since 4.3 + */ + public static boolean isVersion5OrNewer() { + return IS_VERSION_5_OR_NEWER; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java similarity index 51% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java index 5b7f619bf6..bca668f5cd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2024 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. @@ -13,116 +13,149 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb; +package org.springframework.data.mongodb.util; import java.lang.reflect.Method; -import java.net.InetAddress; import java.net.InetSocketAddress; -import com.mongodb.ServerAddress; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.data.mongodb.core.MongoClientSettingsFactoryBean; -import org.springframework.data.mongodb.util.MongoClientVersion; + +import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import com.mongodb.MongoClientSettings; import com.mongodb.MongoClientSettings.Builder; -import com.mongodb.client.MapReduceIterable; +import com.mongodb.ServerAddress; import com.mongodb.client.model.IndexOptions; -import com.mongodb.reactivestreams.client.MapReducePublisher; /** + * Compatibility adapter to bridge functionality across different MongoDB driver versions. + *

+ * This class is for internal use within the framework and should not be used by applications. + * * @author Christoph Strobl - * @since 2023/12 + * @since 4.3 */ public class MongoCompatibilityAdapter { - private static final String NO_LONGER_SUPPORTED = "%s is no longer supported on Mongo Client 5+"; + private static final String NO_LONGER_SUPPORTED = "%s is no longer supported on Mongo Client 5 or newer"; + + private static final @Nullable Method getStreamFactoryFactory = ReflectionUtils.findMethod(MongoClientSettings.class, + "getStreamFactoryFactory"); + + private static final @Nullable Method setBucketSize = ReflectionUtils.findMethod(IndexOptions.class, "bucketSize", + Double.class); + /** + * Return a compatibility adapter for {@link MongoClientSettings.Builder}. + * + * @param builder + * @return + */ public static ClientSettingsBuilderAdapter clientSettingsBuilderAdapter(MongoClientSettings.Builder builder) { return new MongoStreamFactoryFactorySettingsConfigurer(builder)::setStreamFactory; } + /** + * Return a compatibility adapter for {@link MongoClientSettings}. + * + * @param clientSettings + * @return + */ public static ClientSettingsAdapter clientSettingsAdapter(MongoClientSettings clientSettings) { return new ClientSettingsAdapter() { @Override public T getStreamFactoryFactory() { - if (MongoClientVersion.is5PlusClient()) { + + if (MongoClientVersion.isVersion5OrNewer() || getStreamFactoryFactory == null) { return null; } - Method getStreamFactoryFactory = ReflectionUtils.findMethod(MongoClientSettings.class, - "getStreamFactoryFactory"); - return getStreamFactoryFactory != null - ? (T) ReflectionUtils.invokeMethod(getStreamFactoryFactory, clientSettings) - : null; + return (T) ReflectionUtils.invokeMethod(getStreamFactoryFactory, clientSettings); } }; } + /** + * Return a compatibility adapter for {@link IndexOptions}. + * + * @param options + * @return + */ public static IndexOptionsAdapter indexOptionsAdapter(IndexOptions options) { - return new IndexOptionsAdapter() { - @Override - public void setBucketSize(Double bucketSize) { + return bucketSize -> { - if (MongoClientVersion.is5PlusClient()) { - throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("IndexOptions.bucketSize")); - } - - Method setBucketSize = ReflectionUtils.findMethod(IndexOptions.class, "bucketSize", Double.class); - ReflectionUtils.invokeMethod(setBucketSize, options, bucketSize); + if (MongoClientVersion.isVersion5OrNewer() || setBucketSize == null) { + throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("IndexOptions.bucketSize")); } + + ReflectionUtils.invokeMethod(setBucketSize, options, bucketSize); }; } - @SuppressWarnings({ "deprecation" }) - public static MapReduceIterableAdapter mapReduceIterableAdapter(MapReduceIterable iterable) { + /** + * Return a compatibility adapter for {@code MapReduceIterable}. + * + * @param iterable + * @return + */ + public static MapReduceIterableAdapter mapReduceIterableAdapter(Object iterable) { return sharded -> { - if (MongoClientVersion.is5PlusClient()) { + + if (MongoClientVersion.isVersion5OrNewer()) { throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded")); } - Method shardedMethod = ReflectionUtils.findMethod(iterable.getClass(), "MapReduceIterable.sharded", - boolean.class); - ReflectionUtils.invokeMethod(shardedMethod, iterable, shardedMethod); + Method shardedMethod = ReflectionUtils.findMethod(iterable.getClass(), "sharded", boolean.class); + ReflectionUtils.invokeMethod(shardedMethod, iterable, sharded); }; } - public static MapReducePublisherAdapter mapReducePublisherAdapter(MapReducePublisher publisher) { + /** + * Return a compatibility adapter for {@code MapReducePublisher}. + * + * @param publisher + * @return + */ + public static MapReducePublisherAdapter mapReducePublisherAdapter(Object publisher) { return sharded -> { - if (MongoClientVersion.is5PlusClient()) { + + if (MongoClientVersion.isVersion5OrNewer()) { throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded")); } - Method shardedMethod = ReflectionUtils.findMethod(publisher.getClass(), "MapReduceIterable.sharded", - boolean.class); - ReflectionUtils.invokeMethod(shardedMethod, publisher, shardedMethod); + Method shardedMethod = ReflectionUtils.findMethod(publisher.getClass(), "sharded", boolean.class); + ReflectionUtils.invokeMethod(shardedMethod, publisher, sharded); }; } + /** + * Return a compatibility adapter for {@link ServerAddress}. + * + * @param serverAddress + * @return + */ public static ServerAddressAdapter serverAddressAdapter(ServerAddress serverAddress) { - return new ServerAddressAdapter() { - @Override - public InetSocketAddress getSocketAddress() { - - if(MongoClientVersion.is5PlusClient()) { - return null; - } + return () -> { - Method serverAddressMethod = ReflectionUtils.findMethod(serverAddress.getClass(), "getSocketAddress"); - Object value = ReflectionUtils.invokeMethod(serverAddressMethod, serverAddress); - return value != null ? InetSocketAddress.class.cast(value) : null; + if (MongoClientVersion.isVersion5OrNewer()) { + return null; } + + Method serverAddressMethod = ReflectionUtils.findMethod(serverAddress.getClass(), "getSocketAddress"); + Object value = ReflectionUtils.invokeMethod(serverAddressMethod, serverAddress); + return value != null ? InetSocketAddress.class.cast(value) : null; }; } public interface IndexOptionsAdapter { - void setBucketSize(Double bucketSize); + void setBucketSize(double bucketSize); } public interface ClientSettingsAdapter { + @Nullable T getStreamFactoryFactory(); } @@ -139,12 +172,13 @@ public interface MapReducePublisherAdapter { } public interface ServerAddressAdapter { + @Nullable InetSocketAddress getSocketAddress(); } static class MongoStreamFactoryFactorySettingsConfigurer { - private static final Log logger = LogFactory.getLog(MongoClientSettingsFactoryBean.class); + private static final Log logger = LogFactory.getLog(MongoStreamFactoryFactorySettingsConfigurer.class); private static final String STREAM_FACTORY_NAME = "com.mongodb.connection.StreamFactoryFactory"; private static final boolean STREAM_FACTORY_PRESENT = ClassUtils.isPresent(STREAM_FACTORY_NAME, @@ -161,26 +195,25 @@ public MongoStreamFactoryFactorySettingsConfigurer(Builder settingsBuilder) { void setStreamFactory(Object streamFactory) { - if (MongoClientVersion.is5PlusClient()) { + if (MongoClientVersion.isVersion5OrNewer() && isStreamFactoryPresent()) { logger.warn("StreamFactoryFactory is no longer available. Use TransportSettings instead."); + return; } - if (isStreamFactoryPresent()) { // - try { - Class streamFactoryType = ClassUtils.forName(STREAM_FACTORY_NAME, - streamFactory.getClass().getClassLoader()); - if (!ClassUtils.isAssignable(streamFactoryType, streamFactory.getClass())) { - throw new IllegalArgumentException("Expected %s but found %s".formatted(streamFactoryType, streamFactory)); - } - - Method setter = ReflectionUtils.findMethod(settingsBuilder.getClass(), "streamFactoryFactory", - streamFactoryType); - if (setter != null) { - ReflectionUtils.invokeMethod(setter, settingsBuilder, streamFactoryType.cast(streamFactory)); - } - } catch (ClassNotFoundException e) { - throw new IllegalArgumentException("Cannot set StreamFactoryFactory for %s".formatted(settingsBuilder), e); + try { + Class streamFactoryType = ClassUtils.forName(STREAM_FACTORY_NAME, streamFactory.getClass().getClassLoader()); + + if (!ClassUtils.isAssignable(streamFactoryType, streamFactory.getClass())) { + throw new IllegalArgumentException("Expected %s but found %s".formatted(streamFactoryType, streamFactory)); + } + + Method setter = ReflectionUtils.findMethod(settingsBuilder.getClass(), "streamFactoryFactory", + streamFactoryType); + if (setter != null) { + ReflectionUtils.invokeMethod(setter, settingsBuilder, streamFactoryType.cast(streamFactory)); } + } catch (ReflectiveOperationException e) { + throw new IllegalArgumentException("Cannot set StreamFactoryFactory for %s".formatted(settingsBuilder), e); } } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java index bdeb5b7b07..6f3607ca98 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java @@ -17,9 +17,16 @@ import static org.springframework.data.mongodb.test.util.Assertions.*; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.test.SampleTestRunner; + import java.util.List; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.PersonRepository; @@ -27,12 +34,6 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import io.micrometer.observation.ObservationRegistry; -import io.micrometer.tracing.exporter.FinishedSpan; -import io.micrometer.tracing.test.SampleTestRunner; - /** * Collection of tests that log metrics and tracing with an external tracing tool. * @@ -82,7 +83,7 @@ public SampleTestRunnerConsumer yourCode() { assertThat(span.getTags()).containsEntry("db.system", "mongodb").containsEntry("net.transport", "IP.TCP"); - if(MongoClientVersion.is5PlusClient()) { + if (MongoClientVersion.isVersion5OrNewer()) { assertThat(span.getTags()).containsKeys("db.connection_string", "db.name", "db.operation", "db.mongodb.collection", "net.peer.name", "net.peer.port"); } else { From 69a5022ba55396127a301c56794b9b1b6444bc71 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 23 Jan 2024 14:27:28 +0100 Subject: [PATCH 08/23] Reinstate observability tests. --- .../MongoObservationCommandListenerTests.java | 163 +++++++++--------- 1 file changed, 82 insertions(+), 81 deletions(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java index 75c025d489..b89384a0db 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java @@ -30,6 +30,7 @@ import org.bson.BsonString; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; import com.mongodb.RequestContext; @@ -100,115 +101,115 @@ void commandStartedShouldNotInstrumentWhenNoParentSampleInRequestContext() { void successfullyCompletedCommandShouldCreateTimerWhenParentSampleInRequestContext() { // given -// Observation parent = Observation.start("name", observationRegistry); -// RequestContext traceRequestContext = getContext(); -// -// // when -// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // -// new ConnectionDescription( // -// new ServerId( // -// new ClusterId("description"), // -// new ServerAddress("localhost", 1234))), -// "database", "insert", // -// new BsonDocument("collection", new BsonString("user")))); -// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); -// -// // then -// assertThatTimerRegisteredWithTags(); + Observation parent = Observation.start("name", observationRegistry); + RequestContext traceRequestContext = getContext(); + + // when + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, 0, // + new ConnectionDescription( // + new ServerId( // + new ClusterId("description"), // + new ServerAddress("localhost", 1234))), + "database", "insert", // + new BsonDocument("collection", new BsonString("user")))); + listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, 0, null, "insert", null, null, 0)); + + // then + assertThatTimerRegisteredWithTags(); } @Test void successfullyCompletedCommandWithCollectionHavingCommandNameShouldCreateTimerWhenParentSampleInRequestContext() { // given -// Observation parent = Observation.start("name", observationRegistry); -// RequestContext traceRequestContext = getContext(); -// -// // when -// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // -// new ConnectionDescription( // -// new ServerId( // -// new ClusterId("description"), // -// new ServerAddress("localhost", 1234))), // -// "database", "aggregate", // -// new BsonDocument("aggregate", new BsonString("user")))); -// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "aggregate", null, 0)); + Observation parent = Observation.start("name", observationRegistry); + RequestContext traceRequestContext = getContext(); + + // when + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, 0, // + new ConnectionDescription( // + new ServerId( // + new ClusterId("description"), // + new ServerAddress("localhost", 1234))), // + "database", "aggregate", // + new BsonDocument("aggregate", new BsonString("user")))); + listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, 0, null, "aggregate", null, null, 0)); // then -// assertThatTimerRegisteredWithTags(); + assertThatTimerRegisteredWithTags(); } @Test void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenParentSampleInRequestContext() { -// // given -// Observation parent = Observation.start("name", observationRegistry); -// RequestContext traceRequestContext = getContext(); -// -// // when -// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, null, "database", "insert", -// new BsonDocument("collection", new BsonString("user")))); -// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); -// -// assertThat(meterRegistry).hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(), -// KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"), -// LowCardinalityCommandKeyNames.DB_NAME.withValue("database"), -// LowCardinalityCommandKeyNames.MONGODB_COMMAND.withValue("insert"), -// LowCardinalityCommandKeyNames.DB_SYSTEM.withValue("mongodb")).and("error", "none")); + // given + Observation parent = Observation.start("name", observationRegistry); + RequestContext traceRequestContext = getContext(); + + // when + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, 0, null, "database", "insert", + new BsonDocument("collection", new BsonString("user")))); + listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, 0, null, "insert", null, null, 0)); + + assertThat(meterRegistry).hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(), + KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"), + LowCardinalityCommandKeyNames.DB_NAME.withValue("database"), + LowCardinalityCommandKeyNames.MONGODB_COMMAND.withValue("insert"), + LowCardinalityCommandKeyNames.DB_SYSTEM.withValue("mongodb")).and("error", "none")); } @Test void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { -// // given -// Observation parent = Observation.start("name", observationRegistry); -// RequestContext traceRequestContext = getContext(); -// -// // when -// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // -// new ConnectionDescription( // -// new ServerId( // -// new ClusterId("description"), // -// new ServerAddress("localhost", 1234))), // -// "database", "insert", // -// new BsonDocument("collection", new BsonString("user")))); -// listener.commandFailed( // -// new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, new IllegalAccessException())); -// -// // then -// assertThatTimerRegisteredWithTags(); + // given + Observation parent = Observation.start("name", observationRegistry); + RequestContext traceRequestContext = getContext(); + + // when + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, 0, // + new ConnectionDescription( // + new ServerId( // + new ClusterId("description"), // + new ServerAddress("localhost", 1234))), // + "database", "insert", // + new BsonDocument("collection", new BsonString("user")))); + listener.commandFailed( // + new CommandFailedEvent(traceRequestContext, 0, 0, null, "db", "insert", 0, new IllegalAccessException())); + + // then + assertThatTimerRegisteredWithTags(); } @Test // GH-4481 void completionShouldIgnoreIncompatibleObservationContext() { -// // given -// RequestContext traceRequestContext = getContext(); -// -// Observation observation = mock(Observation.class); -// traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); -// -// // when -// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); -// -// verify(observation).getContext(); -// verifyNoMoreInteractions(observation); + // given + RequestContext traceRequestContext = getContext(); + + Observation observation = mock(Observation.class); + traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); + + // when + listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, 0, null, "insert", null, null, 0)); + + verify(observation).getContext(); + verifyNoMoreInteractions(observation); } @Test // GH-4481 void failureShouldIgnoreIncompatibleObservationContext() { -// // given -// RequestContext traceRequestContext = getContext(); -// -// Observation observation = mock(Observation.class); -// traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); -// -// // when -// listener.commandFailed(new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, null)); -// -// verify(observation).getContext(); -// verifyNoMoreInteractions(observation); + // given + RequestContext traceRequestContext = getContext(); + + Observation observation = mock(Observation.class); + traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); + + // when + listener.commandFailed(new CommandFailedEvent(traceRequestContext, 0, 0, null, "db", "insert", 0, null)); + + verify(observation).getContext(); + verifyNoMoreInteractions(observation); } private RequestContext getContext() { From dd7f1f68c1dff081039d75ea087d32e207f55eef Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 23 Jan 2024 14:50:07 +0100 Subject: [PATCH 09/23] Polishing. Add missing Override annotations. --- .../data/mongodb/core/DefaultIndexOperations.java | 1 + .../data/mongodb/core/DefaultReactiveIndexOperations.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java index 583453c7a1..4772e6ab09 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java @@ -114,6 +114,7 @@ public DefaultIndexOperations(MongoOperations mongoOperations, String collection this.type = type; } + @Override public String ensureIndex(final IndexDefinition indexDefinition) { return execute(collection -> { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java index 5776619399..548d3e0862 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java @@ -88,7 +88,8 @@ private DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, this.type = type; } - public Mono ensureIndex(final IndexDefinition indexDefinition) { + @Override + public Mono ensureIndex(IndexDefinition indexDefinition) { return mongoOperations.execute(collectionName, collection -> { From bdd3fd459b8bb428b0808bdab789a6ee3285f684 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 23 Jan 2024 14:50:36 +0100 Subject: [PATCH 10/23] Use interfaces for reflection lookup to avoid access violations to package-protected types. --- .../data/mongodb/util/MongoCompatibilityAdapter.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java index bca668f5cd..7c3fc11eb5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java @@ -28,7 +28,9 @@ import com.mongodb.MongoClientSettings; import com.mongodb.MongoClientSettings.Builder; import com.mongodb.ServerAddress; +import com.mongodb.client.MapReduceIterable; import com.mongodb.client.model.IndexOptions; +import com.mongodb.reactivestreams.client.MapReducePublisher; /** * Compatibility adapter to bridge functionality across different MongoDB driver versions. @@ -101,6 +103,7 @@ public static IndexOptionsAdapter indexOptionsAdapter(IndexOptions options) { * @param iterable * @return */ + @SuppressWarnings("deprecation") public static MapReduceIterableAdapter mapReduceIterableAdapter(Object iterable) { return sharded -> { @@ -108,7 +111,9 @@ public static MapReduceIterableAdapter mapReduceIterableAdapter(Object iterable) throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded")); } - Method shardedMethod = ReflectionUtils.findMethod(iterable.getClass(), "sharded", boolean.class); + // Use MapReduceIterable to avoid package-protected access violations to + // com.mongodb.client.internal.MapReduceIterableImpl + Method shardedMethod = ReflectionUtils.findMethod(MapReduceIterable.class, "sharded", boolean.class); ReflectionUtils.invokeMethod(shardedMethod, iterable, sharded); }; } @@ -119,6 +124,7 @@ public static MapReduceIterableAdapter mapReduceIterableAdapter(Object iterable) * @param publisher * @return */ + @SuppressWarnings("deprecation") public static MapReducePublisherAdapter mapReducePublisherAdapter(Object publisher) { return sharded -> { @@ -126,7 +132,8 @@ public static MapReducePublisherAdapter mapReducePublisherAdapter(Object publish throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded")); } - Method shardedMethod = ReflectionUtils.findMethod(publisher.getClass(), "sharded", boolean.class); + // Use MapReducePublisher to avoid package-protected access violations to MapReducePublisherImpl + Method shardedMethod = ReflectionUtils.findMethod(MapReducePublisher.class, "sharded", boolean.class); ReflectionUtils.invokeMethod(shardedMethod, publisher, sharded); }; } From e0bad13ed9ebe75f4c6461433d6796b89bcd164b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 25 Jan 2024 10:37:31 +0100 Subject: [PATCH 11/23] Register runtime hints for native image. Register hints during AOT computation required for reflective access on MongoDB driver API via the ComptatibilityAdapter. Along the way also introduce JUnit extensions allowing declarative class path exclusions for tests. --- spring-data-mongodb/pom.xml | 6 + .../data/mongodb/aot/MongoAotPredicates.java | 9 ++ .../data/mongodb/aot/MongoRuntimeHints.java | 33 ++++ .../aot/MongoRuntimeHintsUnitTests.java | 128 ++++++++++++++++ .../test/util/ClassPathExclusions.java | 45 ++++++ .../util/ClassPathExclusionsExtension.java | 129 ++++++++++++++++ .../util/PackageExcludingClassLoader.java | 142 ++++++++++++++++++ 7 files changed, 492 insertions(+) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/MongoRuntimeHintsUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusions.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusionsExtension.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/PackageExcludingClassLoader.java diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 70e2616fa9..0d1a169fbe 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -260,6 +260,12 @@ test + + org.junit.platform + junit-platform-launcher + test + + jakarta.transaction jakarta.transaction-api diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java index 3fcded33ed..71cdbb5679 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java @@ -33,6 +33,7 @@ public class MongoAotPredicates { public static final Predicate> IS_SIMPLE_TYPE = (type) -> MongoSimpleTypes.HOLDER.isSimpleType(type) || TypeUtils.type(type).isPartOf("org.bson"); public static final Predicate IS_REACTIVE_LIBARARY_AVAILABLE = ReactiveWrappers::isAvailable; public static final Predicate IS_SYNC_CLIENT_PRESENT = (classLoader) -> ClassUtils.isPresent("com.mongodb.client.MongoClient", classLoader); + public static final Predicate IS_REACTIVE_CLIENT_PRESENT = (classLoader) -> ClassUtils.isPresent("com.mongodb.reactivestreams.client.MongoClient", classLoader); public static boolean isReactorPresent() { return IS_REACTIVE_LIBARARY_AVAILABLE.test(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR); @@ -42,4 +43,12 @@ public static boolean isSyncClientPresent(@Nullable ClassLoader classLoader) { return IS_SYNC_CLIENT_PRESENT.test(classLoader); } + /** + * @param classLoader can be {@literal null}. + * @return {@literal true} if the {@link com.mongodb.reactivestreams.client.MongoClient} is present. + * @since 4.3 + */ + public static boolean isReactiveClientPresent(@Nullable ClassLoader classLoader) { + return IS_REACTIVE_CLIENT_PRESENT.test(classLoader); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java index a49d0bece1..ff0b4aa5ea 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java @@ -19,6 +19,12 @@ import java.util.Arrays; +import com.mongodb.MongoClientSettings; +import com.mongodb.ServerAddress; +import com.mongodb.UnixServerAddress; +import com.mongodb.client.MapReduceIterable; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.reactivestreams.client.MapReducePublisher; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -31,6 +37,7 @@ import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterSaveCallback; import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback; import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCallback; +import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; @@ -53,6 +60,7 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) MemberCategory.INVOKE_PUBLIC_METHODS)); registerTransactionProxyHints(hints, classLoader); + registerMongoCompatibilityAdapterHints(hints, classLoader); if (isReactorPresent()) { @@ -80,4 +88,29 @@ private static void registerTransactionProxyHints(RuntimeHints hints, @Nullable } } + private static void registerMongoCompatibilityAdapterHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + + hints.reflection() // + .registerType(MongoClientSettings.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(MongoClientSettings.Builder.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(IndexOptions.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(ServerAddress.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(UnixServerAddress.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(TypeReference.of("com.mongodb.connection.StreamFactoryFactory"), MemberCategory.INTROSPECT_PUBLIC_METHODS); + + if(MongoAotPredicates.isSyncClientPresent(classLoader)) { + + hints.reflection() // + .registerType(MapReduceIterable.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(TypeReference.of("com.mongodb.client.internal.MapReduceIterableImpl"), MemberCategory.INVOKE_PUBLIC_METHODS); + } + + if(MongoAotPredicates.isReactiveClientPresent(classLoader)) { + + hints.reflection() // + .registerType(MapReducePublisher.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MapReducePublisherImpl"), MemberCategory.INVOKE_PUBLIC_METHODS); + } + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/MongoRuntimeHintsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/MongoRuntimeHintsUnitTests.java new file mode 100644 index 0000000000..526ab39b1e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/MongoRuntimeHintsUnitTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2024 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.aot; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.aot.hint.MemberCategory.*; +import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.*; + +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.data.mongodb.test.util.ClassPathExclusions; + +import com.mongodb.MongoClientSettings; +import com.mongodb.ServerAddress; +import com.mongodb.UnixServerAddress; +import com.mongodb.client.MapReduceIterable; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.reactivestreams.client.MapReducePublisher; + +/** + * Unit Tests for {@link MongoRuntimeHints}. + * + * @author Christoph Strobl + */ +class MongoRuntimeHintsUnitTests { + + @Test // GH-4578 + @ClassPathExclusions(packages = { "com.mongodb.client", "com.mongodb.reactivestreams.client" }) + void shouldRegisterGeneralCompatibilityHints() { + + RuntimeHints runtimeHints = new RuntimeHints(); + + new MongoRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader()); + + Predicate expected = reflection().onType(MongoClientSettings.class) + .withMemberCategory(INVOKE_PUBLIC_METHODS) + .and(reflection().onType(MongoClientSettings.Builder.class).withMemberCategory(INVOKE_PUBLIC_METHODS)) + .and(reflection().onType(IndexOptions.class).withMemberCategory(INVOKE_PUBLIC_METHODS)) + .and(reflection().onType(ServerAddress.class).withMemberCategory(INVOKE_PUBLIC_METHODS)) + .and(reflection().onType(UnixServerAddress.class).withMemberCategory(INVOKE_PUBLIC_METHODS)) + .and(reflection().onType(TypeReference.of("com.mongodb.connection.StreamFactoryFactory")) + .withMemberCategory(INTROSPECT_PUBLIC_METHODS)); + + assertThat(runtimeHints).matches(expected); + } + + @Test // GH-4578 + @ClassPathExclusions(packages = { "com.mongodb.reactivestreams.client" }) + void shouldRegisterSyncCompatibilityHintsIfPresent() { + + RuntimeHints runtimeHints = new RuntimeHints(); + + new MongoRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader()); + + Predicate expected = reflection().onType(MapReduceIterable.class) + .withMemberCategory(INVOKE_PUBLIC_METHODS) + .and(reflection().onType(TypeReference.of("com.mongodb.client.internal.MapReduceIterableImpl")) + .withMemberCategory(INVOKE_PUBLIC_METHODS)); + + assertThat(runtimeHints).matches(expected); + } + + @Test // GH-4578 + @ClassPathExclusions(packages = { "com.mongodb.client" }) + void shouldNotRegisterSyncCompatibilityHintsIfClientNotPresent() { + + RuntimeHints runtimeHints = new RuntimeHints(); + + new MongoRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader()); + + Predicate expected = reflection().onType(TypeReference.of("com.mongodb.client.MapReduceIterable")) + .withMemberCategory(INVOKE_PUBLIC_METHODS).negate() + .and(reflection().onType(TypeReference.of("com.mongodb.client.internal.MapReduceIterableImpl")) + .withMemberCategory(INVOKE_PUBLIC_METHODS).negate()); + + assertThat(runtimeHints).matches(expected); + } + + @Test // GH-4578 + @ClassPathExclusions(packages = { "com.mongodb.client" }) + void shouldRegisterReactiveCompatibilityHintsIfPresent() { + + RuntimeHints runtimeHints = new RuntimeHints(); + + new MongoRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader()); + + Predicate expected = reflection().onType(MapReducePublisher.class) + .withMemberCategory(INVOKE_PUBLIC_METHODS) + .and(reflection().onType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MapReducePublisherImpl")) + .withMemberCategory(INVOKE_PUBLIC_METHODS)); + + assertThat(runtimeHints).matches(expected); + } + + @Test // GH-4578 + @ClassPathExclusions(packages = { "com.mongodb.reactivestreams.client" }) + void shouldNotRegisterReactiveCompatibilityHintsIfClientNotPresent() { + + RuntimeHints runtimeHints = new RuntimeHints(); + + new MongoRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader()); + + Predicate expected = reflection() + .onType(TypeReference.of("com.mongodb.reactivestreams.client.MapReducePublisher")) + .withMemberCategory(INVOKE_PUBLIC_METHODS).negate() + .and(reflection().onType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MapReducePublisherImpl")) + .withMemberCategory(INVOKE_PUBLIC_METHODS).negate()); + + assertThat(runtimeHints).matches(expected); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusions.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusions.java new file mode 100644 index 0000000000..264c5bfa03 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusions.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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.test.util; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Annotation used to exclude entries from the classpath. + * Simplified version of ClassPathExclusions. + * + * @author Christoph Strobl + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ExtendWith(ClassPathExclusionsExtension.class) +public @interface ClassPathExclusions { + + /** + * One or more packages that should be excluded from the classpath. + * + * @return the excluded packages + */ + String[] packages(); + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusionsExtension.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusionsExtension.java new file mode 100644 index 0000000000..9d4454a26f --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusionsExtension.java @@ -0,0 +1,129 @@ +/* + * Copyright 2024 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.test.util; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; +import org.junit.platform.launcher.listeners.SummaryGeneratingListener; +import org.junit.platform.launcher.listeners.TestExecutionSummary; +import org.springframework.util.CollectionUtils; + +/** + * Simplified version of ModifiedClassPathExtension. + * + * @author Christoph Strobl + */ +class ClassPathExclusionsExtension implements InvocationInterceptor { + + @Override + public void interceptBeforeAllMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept(invocation, extensionContext); + } + + @Override + public void interceptBeforeEachMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept(invocation, extensionContext); + } + + @Override + public void interceptAfterEachMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept(invocation, extensionContext); + } + + @Override + public void interceptAfterAllMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept(invocation, extensionContext); + } + + @Override + public void interceptTestMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + interceptMethod(invocation, invocationContext, extensionContext); + } + + @Override + public void interceptTestTemplateMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + interceptMethod(invocation, invocationContext, extensionContext); + } + + private void interceptMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + + if (isModifiedClassPathClassLoader(extensionContext)) { + invocation.proceed(); + return; + } + + Class testClass = extensionContext.getRequiredTestClass(); + Method testMethod = invocationContext.getExecutable(); + PackageExcludingClassLoader modifiedClassLoader = PackageExcludingClassLoader.get(testClass, testMethod); + if (modifiedClassLoader == null) { + invocation.proceed(); + return; + } + invocation.skip(); + ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(modifiedClassLoader); + try { + runTest(extensionContext.getUniqueId()); + } finally { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + } + + private void runTest(String testId) throws Throwable { + + LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectUniqueId(testId)).build(); + Launcher launcher = LauncherFactory.create(); + TestPlan testPlan = launcher.discover(request); + SummaryGeneratingListener listener = new SummaryGeneratingListener(); + launcher.registerTestExecutionListeners(listener); + launcher.execute(testPlan); + TestExecutionSummary summary = listener.getSummary(); + if (!CollectionUtils.isEmpty(summary.getFailures())) { + throw summary.getFailures().get(0).getException(); + } + } + + private void intercept(Invocation invocation, ExtensionContext extensionContext) throws Throwable { + if (isModifiedClassPathClassLoader(extensionContext)) { + invocation.proceed(); + return; + } + invocation.skip(); + } + + private boolean isModifiedClassPathClassLoader(ExtensionContext extensionContext) { + Class testClass = extensionContext.getRequiredTestClass(); + ClassLoader classLoader = testClass.getClassLoader(); + return classLoader.getClass().getName().equals(PackageExcludingClassLoader.class.getName()); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/PackageExcludingClassLoader.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/PackageExcludingClassLoader.java new file mode 100644 index 0000000000..ba8ed8b296 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/PackageExcludingClassLoader.java @@ -0,0 +1,142 @@ +/* + * Copyright 2024 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.test.util; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.Stream; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.ClassUtils; + +/** + * Simplified version of ModifiedClassPathClassLoader. + * + * @author Christoph Strobl + */ +class PackageExcludingClassLoader extends URLClassLoader { + + private final Set excludedPackages; + private final ClassLoader junitLoader; + + PackageExcludingClassLoader(URL[] urls, ClassLoader parent, Collection excludedPackages, + ClassLoader junitClassLoader) { + + super(urls, parent); + this.excludedPackages = Set.copyOf(excludedPackages); + this.junitLoader = junitClassLoader; + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + + if (name.startsWith("org.junit") || name.startsWith("org.hamcrest")) { + return Class.forName(name, false, this.junitLoader); + } + + String packageName = ClassUtils.getPackageName(name); + if (this.excludedPackages.contains(packageName)) { + throw new ClassNotFoundException(name); + } + return super.loadClass(name); + } + + static PackageExcludingClassLoader get(Class testClass, Method testMethod) { + + List excludedPackages = readExcludedPackages(testClass, testMethod); + + if (excludedPackages.isEmpty()) { + return null; + } + + ClassLoader testClassClassLoader = testClass.getClassLoader(); + Stream urls = null; + if (testClassClassLoader instanceof URLClassLoader urlClassLoader) { + urls = Stream.of(urlClassLoader.getURLs()); + } else { + urls = Stream.of(ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator)) + .map(PackageExcludingClassLoader::toURL); + } + + return new PackageExcludingClassLoader(urls.toArray(URL[]::new), testClassClassLoader.getParent(), excludedPackages, + testClassClassLoader); + } + + private static List readExcludedPackages(Class testClass, Method testMethod) { + + return Stream.of( // + AnnotatedElementUtils.findMergedAnnotation(testClass, ClassPathExclusions.class), + AnnotatedElementUtils.findMergedAnnotation(testMethod, ClassPathExclusions.class) // + ).filter(Objects::nonNull) // + .map(ClassPathExclusions::packages) // + .collect(new CombingArrayCollector()); + } + + private static URL toURL(String entry) { + try { + return new File(entry).toURI().toURL(); + } catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } + + private static class CombingArrayCollector implements Collector, List> { + + @Override + public Supplier> supplier() { + return ArrayList::new; + } + + @Override + public BiConsumer, T[]> accumulator() { + return (target, values) -> target.addAll(Arrays.asList(values)); + } + + @Override + public BinaryOperator> combiner() { + return (r1, r2) -> { + r1.addAll(r2); + return r1; + }; + } + + @Override + public Function, List> finisher() { + return i -> (List) i; + } + + @Override + public Set characteristics() { + return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH)); + } + } +} From ecf0f415e9e5c38b5ad51b4927545e0211d9f1d1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 25 Jan 2024 10:37:58 +0100 Subject: [PATCH 12/23] Polishing. --- .../data/mongodb/aot/MongoAotPredicates.java | 8 ++++++++ .../data/mongodb/util/MongoCompatibilityAdapter.java | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java index 71cdbb5679..40e4a7466a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java @@ -25,6 +25,9 @@ import org.springframework.util.ClassUtils; /** + * Collection of {@link Predicate predicates} to determine dynamic library aspects during AOT computation. + * Intended for internal usage only. + * * @author Christoph Strobl * @since 4.0 */ @@ -39,6 +42,11 @@ public static boolean isReactorPresent() { return IS_REACTIVE_LIBARARY_AVAILABLE.test(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR); } + /** + * @param classLoader can be {@literal null}. + * @return {@literal true} if the {@link com.mongodb.client.MongoClient} is present. + * @since 4.0 + */ public static boolean isSyncClientPresent(@Nullable ClassLoader classLoader) { return IS_SYNC_CLIENT_PRESENT.test(classLoader); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java index 7c3fc11eb5..b029e44b8f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java @@ -151,7 +151,7 @@ public static ServerAddressAdapter serverAddressAdapter(ServerAddress serverAddr return null; } - Method serverAddressMethod = ReflectionUtils.findMethod(serverAddress.getClass(), "getSocketAddress"); + Method serverAddressMethod = ReflectionUtils.findMethod(ServerAddress.class, "getSocketAddress"); Object value = ReflectionUtils.invokeMethod(serverAddressMethod, serverAddress); return value != null ? InetSocketAddress.class.cast(value) : null; }; From 49085d405e4ec53e9c12d26264920dd93c2e63db Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 25 Jan 2024 11:38:33 +0100 Subject: [PATCH 13/23] Protect MongoDB 5 client from failing for annotation default bucketSize GeospatialIndexed defaults the bucketSize to 1.0 which is no longer supported for MongoDB 5 clients. In case the default is used with the new driver the index resolver will log an info message and proceed without setting the value. --- .../MongoPersistentEntityIndexResolver.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index d9494249cc..9a9cee26f9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -25,6 +25,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @@ -53,6 +54,7 @@ import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.mongodb.util.DotPath; +import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.data.mongodb.util.spel.ExpressionUtils; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.util.TypeInformation; @@ -708,7 +710,21 @@ protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath, .named(pathAwareIndexName(index.name(), dotPath, persistentProperty.getOwner(), persistentProperty)); } - indexDefinition.typed(index.type()).withBucketSize(index.bucketSize()).withAdditionalField(index.additionalField()); + if(MongoClientVersion.isVersion5OrNewer()) { + + Optional defaultBucketSize = MergedAnnotation.of(GeoSpatialIndexed.class).getDefaultValue("bucketSize", Double.class); + if (!defaultBucketSize.isPresent() || index.bucketSize() != defaultBucketSize.get()) { + indexDefinition.withBucketSize(index.bucketSize()); + } else { + if(LOGGER.isInfoEnabled()) { + LOGGER.info("Ignoring no longer supported default GeoSpatialIndexed.bucketSize on %s for Mongo Client 5 or newer.".formatted(dotPath)); + } + } + } else { + indexDefinition.withBucketSize(index.bucketSize()); + } + + indexDefinition.typed(index.type()).withAdditionalField(index.additionalField()); return new IndexDefinitionHolder(dotPath, indexDefinition, collection); } From e8cddfe98bc729f195ddcb4287170426f4239ffe Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 25 Jan 2024 11:50:11 +0100 Subject: [PATCH 14/23] Update documentation. --- src/main/antora/modules/ROOT/nav.adoc | 1 + .../antora/modules/ROOT/pages/preface.adoc | 72 +++---------------- 2 files changed, 11 insertions(+), 62 deletions(-) diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 559aee6b2a..f308084bf8 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -5,6 +5,7 @@ *** xref:migration-guide/migration-guide-3.x-to-4.x.adoc[] * xref:mongodb.adoc[] +** xref:preface.adoc[] ** xref:mongodb/getting-started.adoc[] ** xref:mongodb/configuration.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/preface.adoc b/src/main/antora/modules/ROOT/pages/preface.adoc index d21df39cac..793f7b803a 100644 --- a/src/main/antora/modules/ROOT/pages/preface.adoc +++ b/src/main/antora/modules/ROOT/pages/preface.adoc @@ -1,50 +1,12 @@ -[[preface]] -= Preface - -The Spring Data MongoDB project applies core Spring concepts to the development of solutions that use the MongoDB document style data store. We provide a "`template`" as a high-level abstraction for storing and querying documents. You may notice similarities to the JDBC support provided by the Spring Framework. - -This document is the reference guide for Spring Data - MongoDB Support. It explains MongoDB module concepts and semantics and syntax for various store namespaces. - -This section provides some basic introduction to Spring and Document databases. The rest of the document refers only to Spring Data MongoDB features and assumes the user is familiar with MongoDB and Spring concepts. - -[[get-started:first-steps:spring]] -== Learning Spring - -Spring Data uses Spring framework's link:{springDocsUrl}/core.html[core] functionality, including: - -* link:{springDocsUrl}/core.html#beans[IoC] container -* link:{springDocsUrl}/core.html#validation[type conversion system] -* link:{springDocsUrl}/core.html#expressions[expression language] -* link:{springDocsUrl}/integration.html#jmx[JMX integration] -* link:{springDocsUrl}/data-access.html#dao-exceptions[DAO exception hierarchy]. - -While you need not know the Spring APIs, understanding the concepts behind them is important. At a minimum, the idea behind Inversion of Control (IoC) should be familiar, and you should be familiar with whatever IoC container you choose to use. - -The core functionality of the MongoDB support can be used directly, with no need to invoke the IoC services of the Spring Container. This is much like `JdbcTemplate`, which can be used "'standalone'" without any other services of the Spring container. To leverage all the features of Spring Data MongoDB, such as the repository support, you need to configure some parts of the library to use Spring. - -To learn more about Spring, you can refer to the comprehensive documentation that explains the Spring Framework in detail. There are a lot of articles, blog entries, and books on the subject. See the Spring framework https://spring.io/docs[home page] for more information. - -[[get-started:first-steps:nosql]] -== Learning NoSQL and Document databases -NoSQL stores have taken the storage world by storm. It is a vast domain with a plethora of solutions, terms, and patterns (to make things worse, even the term itself has multiple https://www.google.com/search?q=nosoql+acronym[meanings]). While some of the principles are common, you must be familiar with MongoDB to some degree. The best way to get acquainted is to read the documentation and follow the examples. It usually does not take more then 5-10 minutes to go through them and, especially if you are coming from an RDMBS-only background, these exercises can be an eye opener. - -The starting point for learning about MongoDB is https://www.mongodb.org/[www.mongodb.org]. Here is a list of other useful resources: - -* The https://docs.mongodb.org/manual/[manual] introduces MongoDB and contains links to getting started guides, reference documentation, and tutorials. -* Visit https://learn.mongodb.com/[MongoDB University] for free training material and online courses. -* MongoDB https://docs.mongodb.org/ecosystem/drivers/java/[Java Language Center]. -* Several https://www.mongodb.org/books[books] you can purchase. -* Karl Seguin's online book: https://openmymind.net/mongodb.pdf[The Little MongoDB Book]. - [[requirements]] -== Requirements += Requirements The Spring Data MongoDB 4.x binaries require JDK level 17 and above and https://spring.io/docs[Spring Framework] {springVersion} and above. -In terms of document stores, you need at least version 3.6 of https://www.mongodb.org/[MongoDB], though we recommend a more recent version. +In terms of database and driver, you need at least version 4.x of https://www.mongodb.org/[MongoDB] and a compatible MongoDB Java Driver (4.x or 5.x). [[compatibility.matrix]] -=== Compatibility Matrix +== Compatibility Matrix The following compatibility matrix summarizes Spring Data versions to MongoDB driver/database versions. Database versions show the highest supported server version that pass the Spring Data test suite. @@ -59,6 +21,11 @@ See also the https://www.mongodb.com/docs/drivers/java/sync/current/compatibilit |Driver Version |Server Version +|2024.0 +|4.3.x +|4.11.x & 5.x +|6.x + |2023.0 |4.1.x |4.9.x @@ -108,32 +75,13 @@ See also the https://www.mongodb.com/docs/drivers/java/sync/current/compatibilit [[compatibility.changes]] [[compatibility.changes-4.4]] -==== Relevant Changes in MongoDB 4.4 +=== Relevant Changes in MongoDB 4.4 * Fields list must not contain text search score property when no `$text` criteria present. See also https://docs.mongodb.com/manual/reference/operator/query/text/[`$text` operator] * Sort must not be an empty document when running map reduce. [[compatibility.changes-4.2]] -==== Relevant Changes in MongoDB 4.2 +=== Relevant Changes in MongoDB 4.2 * Removal of `geoNear` command. See also https://docs.mongodb.com/manual/release-notes/4.2-compatibility/#remove-support-for-the-geonear-command[Removal of `geoNear`] * Removal of `eval` command. See also https://docs.mongodb.com/manual/release-notes/4.2-compatibility/#remove-support-for-the-eval-command[Removal of `eval`] - -[[get-started:help]] -== Additional Help Resources - -Learning a new framework is not always straightforward. -In this section, we try to provide what we think is an easy-to-follow guide for starting with the Spring Data MongoDB module. -However, if you encounter issues or you need advice, feel free to use one of the following links: - -[[get-started:help:community]] -Community Forum :: Spring Data on https://stackoverflow.com/questions/tagged/spring-data[Stack Overflow] is a tag for all Spring Data (not just Document) users to share information and help each other. -Note that registration is needed only for posting. - -[[get-started:help:professional]] -Professional Support :: Professional, from-the-source support, with guaranteed response time, is available from https://pivotal.io/[Pivotal Software, Inc.], the company behind Spring Data and Spring. - -[[get-started:up-to-date]] -== Following Development - -For information on the Spring Data Mongo source code repository, nightly builds, and snapshot artifacts, see the Spring Data Mongo https://spring.io/projects/spring-data-mongodb/[homepage]. You can help make Spring Data best serve the needs of the Spring community by interacting with developers through the Community on https://stackoverflow.com/questions/tagged/spring-data[Stack Overflow]. To follow developer activity, look for the mailing list information on the Spring Data Mongo https://spring.io/projects/spring-data-mongodb/[homepage]. If you encounter a bug or want to suggest an improvement, please create a ticket on the Spring Data https://github.com/spring-projects/spring-data-mongodb/issues[issue tracker]. To stay up to date with the latest news and announcements in the Spring eco system, subscribe to the Spring Community https://spring.io[Portal]. You can also follow the Spring https://spring.io/blog[blog] or the project team on Twitter (https://twitter.com/SpringData[SpringData]). From fae7f925d929fb8aa3664e025c206f70c5c4f8f0 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 25 Jan 2024 11:59:51 +0100 Subject: [PATCH 15/23] Do not default bucket size for 5.0 Mongo driver --- .../data/mongodb/core/index/GeospatialIndex.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java index 8492ad3bc5..822ade040e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java @@ -19,6 +19,7 @@ import org.bson.Document; import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -40,7 +41,7 @@ public class GeospatialIndex implements IndexDefinition { private @Nullable Integer max; private @Nullable Integer bits; private GeoSpatialIndexType type = GeoSpatialIndexType.GEO_2D; - private Double bucketSize = 1.0; + private Double bucketSize = MongoClientVersion.isVersion5OrNewer() ? null : 1.0; private @Nullable String additionalField; private Optional filter = Optional.empty(); private Optional collation = Optional.empty(); @@ -207,7 +208,9 @@ public Document getIndexOptions() { case GEO_HAYSTACK: - document.put("bucketSize", bucketSize); + if (bucketSize != null) { + document.put("bucketSize", bucketSize); + } break; } From e07513be26a8b1d50ab5937e1600a89ff8c1c5c6 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 25 Jan 2024 13:56:50 +0100 Subject: [PATCH 16/23] guard tests only runnable for MongoDB Server < 5.0 and Driver < 5.0 --- .../data/mongodb/core/geo/GeoSpatialIndexTests.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java index e20a934aaa..0257ab3395 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; +import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -40,6 +41,7 @@ import com.mongodb.MongoException; import com.mongodb.WriteConcern; import com.mongodb.client.MongoCollection; +import org.springframework.data.mongodb.util.MongoClientVersion; /** * Integration tests for geo-spatial indexing. @@ -86,6 +88,8 @@ public void test2dSphereIndex() { @EnableIfMongoServerVersion(isLessThan = "5.0") public void testHaystackIndex() { + Assumptions.assumeThat(MongoClientVersion.isVersion5OrNewer()).isFalse(); + try { template.save(new GeoSpatialEntityHaystack(45.2, 4.6, "Paris")); assertThat(hasIndexOfType(GeoSpatialEntityHaystack.class, "geoHaystack")).isTrue(); From 4a266b33bf22a3345173a4d49efe8cbc9219a010 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 26 Jan 2024 09:13:08 +0100 Subject: [PATCH 17/23] Downgrade to 4.11 driver again --- pom.xml | 2 +- .../data/mongodb/aot/MongoRuntimeHints.java | 3 + .../data/mongodb/core/MongoTemplate.java | 4 +- .../mongodb/core/ReactiveMongoTemplate.java | 4 +- .../data/mongodb/util/MongoClientVersion.java | 1 + .../util/MongoCompatibilityAdapter.java | 152 +++++++++++++++++- ...iveSessionBoundMongoTemplateUnitTests.java | 7 +- .../SessionBoundMongoTemplateUnitTests.java | 6 +- .../mongodb/test/util/CleanMongoDBTests.java | 14 +- .../ExcludeReactiveClientFromClassPath.java | 34 ++++ .../util/ExcludeSyncClientFromClassPath.java | 34 ++++ .../mongodb/test/util/MongoTestTemplate.java | 3 +- .../MongoCompatibilityAdapterUnitTests.java | 49 ++++++ 13 files changed, 294 insertions(+), 19 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java diff --git a/pom.xml b/pom.xml index 79135567cd..2bf3e91c5c 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ multi spring-data-mongodb 3.3.0-SNAPSHOT - 5.0.0-beta0 + 4.11.1 ${mongo} 1.19 diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java index ff0b4aa5ea..c9016e1d31 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java @@ -23,6 +23,7 @@ import com.mongodb.ServerAddress; import com.mongodb.UnixServerAddress; import com.mongodb.client.MapReduceIterable; +import com.mongodb.client.MongoDatabase; import com.mongodb.client.model.IndexOptions; import com.mongodb.reactivestreams.client.MapReducePublisher; import org.springframework.aot.hint.MemberCategory; @@ -101,6 +102,7 @@ private static void registerMongoCompatibilityAdapterHints(RuntimeHints hints, @ if(MongoAotPredicates.isSyncClientPresent(classLoader)) { hints.reflection() // + .registerType(MongoDatabase.class, MemberCategory.INVOKE_PUBLIC_METHODS) .registerType(MapReduceIterable.class, MemberCategory.INVOKE_PUBLIC_METHODS) .registerType(TypeReference.of("com.mongodb.client.internal.MapReduceIterableImpl"), MemberCategory.INVOKE_PUBLIC_METHODS); } @@ -108,6 +110,7 @@ private static void registerMongoCompatibilityAdapterHints(RuntimeHints hints, @ if(MongoAotPredicates.isReactiveClientPresent(classLoader)) { hints.reflection() // + .registerType(com.mongodb.reactivestreams.client.MongoDatabase.class, MemberCategory.INVOKE_PUBLIC_METHODS) .registerType(MapReducePublisher.class, MemberCategory.INVOKE_PUBLIC_METHODS) .registerType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MapReducePublisherImpl"), MemberCategory.INVOKE_PUBLIC_METHODS); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index df407ed338..1125b516c2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -724,7 +724,7 @@ public boolean collectionExists(String collectionName) { return execute(db -> { - for (String name : db.listCollectionNames()) { + for (String name : MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db).listCollectionNames()) { if (name.equals(collectionName)) { return true; } @@ -2342,7 +2342,7 @@ protected String replaceWithResourceIfNecessary(String function) { public Set getCollectionNames() { return execute(db -> { Set result = new LinkedHashSet<>(); - for (String name : db.listCollectionNames()) { + for (String name : MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db).listCollectionNames()) { result.add(name); } return result; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index e8675757db..0f1b8905a2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -738,7 +738,7 @@ public Mono collectionExists(Class entityClass) { @Override public Mono collectionExists(String collectionName) { - return createMono(db -> Flux.from(db.listCollectionNames()) // + return createMono(db -> Flux.from(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(db).listCollectionNames()) // .filter(s -> s.equals(collectionName)) // .map(s -> true) // .single(false)); @@ -786,7 +786,7 @@ public ReactiveBulkOperations bulkOps(BulkMode mode, @Nullable Class entityTy @Override public Flux getCollectionNames() { - return createFlux(MongoDatabase::listCollectionNames); + return createFlux(db -> MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(db).listCollectionNames()); } public Mono getMongoDatabase() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java index db72016aa4..f2978b5af4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java @@ -28,6 +28,7 @@ public class MongoClientVersion { private static final boolean SYNC_CLIENT_PRESENT = ClassUtils.isPresent("com.mongodb.MongoClient", + MongoClientVersion.class.getClassLoader()) || ClassUtils.isPresent("com.mongodb.client.MongoClient", MongoClientVersion.class.getClassLoader()); private static final boolean ASYNC_CLIENT_PRESENT = ClassUtils.isPresent("com.mongodb.async.client.MongoClient", diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java index b029e44b8f..aa1eddabd1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java @@ -20,15 +20,19 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - +import org.reactivestreams.Publisher; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import com.mongodb.MongoClientSettings; import com.mongodb.MongoClientSettings.Builder; import com.mongodb.ServerAddress; +import com.mongodb.client.ClientSession; import com.mongodb.client.MapReduceIterable; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.MongoIterable; import com.mongodb.client.model.IndexOptions; import com.mongodb.reactivestreams.client.MapReducePublisher; @@ -157,6 +161,14 @@ public static ServerAddressAdapter serverAddressAdapter(ServerAddress serverAddr }; } + public static MongoDatabaseAdapterBuilder mongoDatabaseAdapter() { + return MongoDatabaseAdapter::new; + } + + public static ReactiveMongoDatabaseAdapterBuilder reactiveMongoDatabaseAdapter() { + return ReactiveMongoDatabaseAdapter::new; + } + public interface IndexOptionsAdapter { void setBucketSize(double bucketSize); } @@ -183,6 +195,144 @@ public interface ServerAddressAdapter { InetSocketAddress getSocketAddress(); } + public interface MongoDatabaseAdapterBuilder { + MongoDatabaseAdapter forDb(com.mongodb.client.MongoDatabase db); + } + + public static class MongoDatabaseAdapter { + + @Nullable // + private static final Method LIST_COLLECTION_NAMES_METHOD; + + @Nullable // + private static final Method LIST_COLLECTION_NAMES_METHOD_SESSION; + + private static final Class collectionNamesReturnType; + + private final MongoDatabase db; + + static { + + if (MongoClientVersion.isSyncClientPresent()) { + + LIST_COLLECTION_NAMES_METHOD = ReflectionUtils.findMethod(MongoDatabase.class, "listCollectionNames"); + LIST_COLLECTION_NAMES_METHOD_SESSION = ReflectionUtils.findMethod(MongoDatabase.class, "listCollectionNames", + ClientSession.class); + + if (MongoClientVersion.isVersion5OrNewer()) { + try { + collectionNamesReturnType = ClassUtils.forName("com.mongodb.client.ListCollectionNamesIterable", + MongoDatabaseAdapter.class.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Unable to load com.mongodb.client.ListCollectionNamesIterable", e); + } + } else { + try { + collectionNamesReturnType = ClassUtils.forName("com.mongodb.client.MongoIterable", + MongoDatabaseAdapter.class.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Unable to load com.mongodb.client.ListCollectionNamesIterable", e); + } + } + } else { + LIST_COLLECTION_NAMES_METHOD = null; + LIST_COLLECTION_NAMES_METHOD_SESSION = null; + collectionNamesReturnType = Object.class; + } + } + + public MongoDatabaseAdapter(MongoDatabase db) { + this.db = db; + } + + public Class> collectionNameIterableType() { + return (Class>) collectionNamesReturnType; + } + + public MongoIterable listCollectionNames() { + + Assert.state(LIST_COLLECTION_NAMES_METHOD != null, "No method listCollectionNames present for %s".formatted(db)); + return (MongoIterable) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD, db); + } + + public MongoIterable listCollectionNames(ClientSession clientSession) { + Assert.state(LIST_COLLECTION_NAMES_METHOD != null, + "No method listCollectionNames(ClientSession) present for %s".formatted(db)); + return (MongoIterable) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD_SESSION, db, + clientSession); + } + } + + public interface ReactiveMongoDatabaseAdapterBuilder { + ReactiveMongoDatabaseAdapter forDb(com.mongodb.reactivestreams.client.MongoDatabase db); + } + + public static class ReactiveMongoDatabaseAdapter { + + @Nullable // + private static final Method LIST_COLLECTION_NAMES_METHOD; + + @Nullable // + private static final Method LIST_COLLECTION_NAMES_METHOD_SESSION; + + private static final Class collectionNamesReturnType; + + private final com.mongodb.reactivestreams.client.MongoDatabase db; + + static { + + if (MongoClientVersion.isReactiveClientPresent()) { + + LIST_COLLECTION_NAMES_METHOD = ReflectionUtils + .findMethod(com.mongodb.reactivestreams.client.MongoDatabase.class, "listCollectionNames"); + LIST_COLLECTION_NAMES_METHOD_SESSION = ReflectionUtils.findMethod( + com.mongodb.reactivestreams.client.MongoDatabase.class, "listCollectionNames", + com.mongodb.reactivestreams.client.ClientSession.class); + + if (MongoClientVersion.isVersion5OrNewer()) { + try { + collectionNamesReturnType = ClassUtils.forName( + "com.mongodb.reactivestreams.client.ListCollectionNamesPublisher", + ReactiveMongoDatabaseAdapter.class.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("com.mongodb.reactivestreams.client.ListCollectionNamesPublisher", e); + } + } else { + try { + collectionNamesReturnType = ClassUtils.forName("org.reactivestreams.Publisher", + ReactiveMongoDatabaseAdapter.class.getClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("org.reactivestreams.Publisher", e); + } + } + } else { + LIST_COLLECTION_NAMES_METHOD = null; + LIST_COLLECTION_NAMES_METHOD_SESSION = null; + collectionNamesReturnType = Object.class; + } + } + + ReactiveMongoDatabaseAdapter(com.mongodb.reactivestreams.client.MongoDatabase db) { + this.db = db; + } + + public Class> collectionNamePublisherType() { + return (Class>) collectionNamesReturnType; + + } + + public Publisher listCollectionNames() { + Assert.state(LIST_COLLECTION_NAMES_METHOD != null, "No method listCollectionNames present for %s".formatted(db)); + return (Publisher) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD, db); + } + + public Publisher listCollectionNames(com.mongodb.reactivestreams.client.ClientSession clientSession) { + Assert.state(LIST_COLLECTION_NAMES_METHOD != null, + "No method listCollectionNames(ClientSession) present for %s".formatted(db)); + return (Publisher) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD_SESSION, db, clientSession); + } + } + static class MongoStreamFactoryFactorySettingsConfigurer { private static final Log logger = LogFactory.getLog(MongoStreamFactoryFactorySettingsConfigurer.class); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java index 6e6f0b001c..8acc112418 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java @@ -21,7 +21,6 @@ import java.lang.reflect.Proxy; -import com.mongodb.reactivestreams.client.ListCollectionNamesPublisher; import org.bson.Document; import org.bson.codecs.BsonValueCodec; import org.bson.codecs.configuration.CodecRegistry; @@ -59,6 +58,7 @@ import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.MongoCollection; import com.mongodb.reactivestreams.client.MongoDatabase; +import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; /** * Unit tests for {@link ReactiveSessionBoundMongoTemplate}. @@ -83,7 +83,7 @@ public class ReactiveSessionBoundMongoTemplateUnitTests { @Mock MongoDatabase database; @Mock ClientSession clientSession; @Mock FindPublisher findPublisher; - @Mock ListCollectionNamesPublisher collectionNamesPublisher; + Publisher collectionNamesPublisher; @Mock AggregatePublisher aggregatePublisher; @Mock DistinctPublisher distinctPublisher; @Mock Publisher resultPublisher; @@ -94,12 +94,13 @@ public class ReactiveSessionBoundMongoTemplateUnitTests { @Before public void setUp() { + mock(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(database).collectionNamePublisherType()); when(client.getDatabase(anyString())).thenReturn(database); when(codecRegistry.get(any(Class.class))).thenReturn(new BsonValueCodec()); when(database.getCodecRegistry()).thenReturn(codecRegistry); when(database.getCollection(anyString())).thenReturn(collection); when(database.getCollection(anyString(), any())).thenReturn(collection); - when(database.listCollectionNames(any(ClientSession.class))).thenReturn(collectionNamesPublisher); + doReturn(collectionNamesPublisher).when(database).listCollectionNames(any(ClientSession.class)); when(database.createCollection(any(ClientSession.class), any(), any())).thenReturn(resultPublisher); when(database.runCommand(any(ClientSession.class), any(), any(Class.class))).thenReturn(resultPublisher); when(collection.find(any(ClientSession.class))).thenReturn(findPublisher); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java index df239bba07..bec7a7a057 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java @@ -49,6 +49,7 @@ import com.mongodb.client.model.DeleteOptions; import com.mongodb.client.model.FindOneAndUpdateOptions; import com.mongodb.client.model.UpdateOptions; +import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; /** * Unit test for {@link SessionBoundMongoTemplate} making sure a proxied {@link MongoCollection} and @@ -75,7 +76,7 @@ public class SessionBoundMongoTemplateUnitTests { @Mock MongoClient client; @Mock ClientSession clientSession; @Mock FindIterable findIterable; - @Mock ListCollectionNamesIterable collectionNamesIterable; + MongoIterable collectionNamesIterable; @Mock MongoIterable mongoIterable; @Mock DistinctIterable distinctIterable; @Mock AggregateIterable aggregateIterable; @@ -89,11 +90,12 @@ public class SessionBoundMongoTemplateUnitTests { @Before public void setUp() { + collectionNamesIterable = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(database).collectionNameIterableType()); when(client.getDatabase(anyString())).thenReturn(database); when(codecRegistry.get(any(Class.class))).thenReturn(new BsonValueCodec()); when(database.getCodecRegistry()).thenReturn(codecRegistry); when(database.getCollection(anyString(), any())).thenReturn(collection); - when(database.listCollectionNames(any(ClientSession.class))).thenReturn(collectionNamesIterable); + doReturn(collectionNamesIterable).when(database).listCollectionNames(any(ClientSession.class)); when(collection.find(any(ClientSession.class), any(), any())).thenReturn(findIterable); when(collection.aggregate(any(ClientSession.class), anyList(), any())).thenReturn(aggregateIterable); when(collection.distinct(any(ClientSession.class), any(), any(), any())).thenReturn(distinctIterable); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java index 6e1591e924..fb7f774e94 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java @@ -21,7 +21,6 @@ import java.util.Collection; import java.util.Collections; -import com.mongodb.client.ListCollectionNamesIterable; import org.bson.Document; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,13 +31,14 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; - import org.springframework.data.mongodb.test.util.CleanMongoDB.Struct; +import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import com.mongodb.client.ListDatabasesIterable; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; +import com.mongodb.client.MongoIterable; /** * @author Christoph Strobl @@ -63,7 +63,7 @@ class CleanMongoDBTests { @SuppressWarnings({ "serial", "unchecked" }) @BeforeEach - void setUp() { + void setUp() throws ClassNotFoundException { // DB setup @@ -74,13 +74,13 @@ void setUp() { when(mongoClientMock.getDatabase(eq("db2"))).thenReturn(db2mock); // collections have to exist - ListCollectionNamesIterable collectionIterable = mock(ListCollectionNamesIterable.class); + MongoIterable collectionIterable = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db1mock).collectionNameIterableType()); when(collectionIterable.into(any(Collection.class))).thenReturn(Arrays.asList("db1collection1", "db1collection2")); - when(db1mock.listCollectionNames()).thenReturn(collectionIterable); + doReturn(collectionIterable).when(db1mock).listCollectionNames(); - ListCollectionNamesIterable collectionIterable2 = mock(ListCollectionNamesIterable.class); + MongoIterable collectionIterable2 = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db2mock).collectionNameIterableType()); when(collectionIterable2.into(any(Collection.class))).thenReturn(Collections.singletonList("db2collection1")); - when(db2mock.listCollectionNames()).thenReturn(collectionIterable2); + doReturn(collectionIterable2).when(db2mock).listCollectionNames(); // return collections according to names when(db1mock.getCollection(eq("db1collection1"))).thenReturn(db1collection1mock); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java new file mode 100644 index 0000000000..74397900c6 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 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 org.springframework.data.mongodb.test.util; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Christoph Strobl + * @see ClassPathExclusions + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ClassPathExclusions(packages = { "com.mongodb.reactivestreams.client" }) +public @interface ExcludeReactiveClientFromClassPath { + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java new file mode 100644 index 0000000000..555ef76864 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java @@ -0,0 +1,34 @@ +/* + * Copyright 2024 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 org.springframework.data.mongodb.test.util; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Christoph Strobl + * @see ClassPathExclusions + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ClassPathExclusions(packages = { "com.mongodb.client" }) +public @interface ExcludeSyncClientFromClassPath { + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java index d561966dd3..bab535cb56 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java @@ -29,6 +29,7 @@ import com.mongodb.MongoWriteException; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; +import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; /** * A {@link MongoTemplate} with configuration hooks and extension suitable for tests. @@ -94,7 +95,7 @@ public void flush() { } public void flushDatabase() { - flush(getDb().listCollectionNames()); + flush(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(getDb()).listCollectionNames()); } public void flush(Iterable collections) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java new file mode 100644 index 0000000000..2e9b8c4f58 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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.util; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.test.util.ExcludeReactiveClientFromClassPath; +import org.springframework.data.mongodb.test.util.ExcludeSyncClientFromClassPath; +import org.springframework.util.ClassUtils; + +/** + * @author Christoph Strobl + */ +class MongoCompatibilityAdapterUnitTests { + + @Test // GH-4578 + @ExcludeReactiveClientFromClassPath + void returnsListCollectionNameIterableTypeCorrectly() { + + String expectedType = MongoClientVersion.isVersion5OrNewer() ? "ListCollectionNamesIterable" : "MongoIterable"; + assertThat(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(null).collectionNameIterableType()) + .satisfies(type -> assertThat(ClassUtils.getShortName(type)).isEqualTo(expectedType)); + + } + + @Test // GH-4578 + @ExcludeSyncClientFromClassPath + void returnsListCollectionNamePublisherTypeCorrectly() { + + String expectedType = MongoClientVersion.isVersion5OrNewer() ? "ListCollectionNamesPublisher" : "Publisher"; + assertThat(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(null).collectionNamePublisherType()) + .satisfies(type -> assertThat(ClassUtils.getShortName(type)).isEqualTo(expectedType)); + + } +} From 4fd58dc88f312baeee1d407a2881e6f586dc5caa Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 26 Jan 2024 10:05:40 +0100 Subject: [PATCH 18/23] Add Build profile for Mongo 5.0 driver --- Jenkinsfile | 28 ++++++++++++++++++++++++++++ pom.xml | 7 +++++++ 2 files changed, 35 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index f122dfb0c5..7ada9e6892 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -265,6 +265,34 @@ pipeline { } } + stage("test: MongoDB 7.0 (driver-next)") { + agent { + label 'data' + } + options { timeout(time: 30, unit: 'MINUTES') } + environment { + ARTIFACTORY = credentials("${p['artifactory.credentials']}") + DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") + } + steps { + script { + docker.image("harbor-repo.vmware.com/dockerhub-proxy-cache/springci/spring-data-with-mongodb-7.0:${p['java.main.tag']}").inside(p['docker.java.inside.basic']) { + sh 'mkdir -p /tmp/mongodb/db /tmp/mongodb/log' + sh 'mongod --setParameter transactionLifetimeLimitSeconds=90 --setParameter maxTransactionLockRequestTimeoutMillis=10000 --dbpath /tmp/mongodb/db --replSet rs0 --fork --logpath /tmp/mongodb/log/mongod.log &' + sh 'sleep 10' + sh 'mongosh --eval "rs.initiate({_id: \'rs0\', members:[{_id: 0, host: \'127.0.0.1:27017\'}]});"' + sh 'sleep 15' + sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' + + "DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR} " + + "DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW} " + + "GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY} " + + "./mvnw -s settings.xml -Pmongo-5.0 clean dependency:list test -Dsort -U -B" + } + } + } + } + stage("test: MongoDB 7.0 (next)") { agent { label 'data' diff --git a/pom.xml b/pom.xml index 2bf3e91c5c..8bae08443b 100644 --- a/pom.xml +++ b/pom.xml @@ -132,6 +132,13 @@ spring-data-mongodb-benchmarks + + mongo-5.0 + + 5.0.0-beta0 + + + From f38cac66d03b0a82f31abe012738b1629fa25aa1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 26 Jan 2024 10:15:08 +0100 Subject: [PATCH 19/23] fix nohttp --- .../mongodb/test/util/ExcludeReactiveClientFromClassPath.java | 2 +- .../data/mongodb/test/util/ExcludeSyncClientFromClassPath.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java index 74397900c6..617bc5991e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java @@ -5,7 +5,7 @@ * 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 + * 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, diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java index 555ef76864..ff8915386f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java @@ -5,7 +5,7 @@ * 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 + * 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, From 05505039b4ddbf3345dd8cf290f040052fbb6570 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 26 Jan 2024 10:20:18 +0100 Subject: [PATCH 20/23] remove links rendered invalid to satisfy the doc checks --- .../data/mongodb/core/MongoClientSettingsFactoryBean.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java index d646722e57..a08fad5972 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java @@ -123,7 +123,7 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean Date: Fri, 26 Jan 2024 10:35:13 +0100 Subject: [PATCH 21/23] Get rid of the gradle cache that ignores changed dependencies. Yeah, changed dependencies, no need to test that - right gradle? --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 7ada9e6892..da92e1b68b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -287,7 +287,7 @@ pipeline { "DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR} " + "DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW} " + "GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY} " + - "./mvnw -s settings.xml -Pmongo-5.0 clean dependency:list test -Dsort -U -B" + "./mvnw -s settings.xml -Pmongo-5.0 clean dependency:list test -Dsort -U -B -Dgradle.cache.local.enabled=false -Dgradle.cache.remote.enabled=false" } } } From f1a532383d09a76ea421827a37396eff6f03bdb1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 26 Jan 2024 11:20:36 +0100 Subject: [PATCH 22/23] add runtime hints for driver internal classes --- .../org/springframework/data/mongodb/aot/MongoRuntimeHints.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java index c9016e1d31..226a102290 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java @@ -103,6 +103,7 @@ private static void registerMongoCompatibilityAdapterHints(RuntimeHints hints, @ hints.reflection() // .registerType(MongoDatabase.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(TypeReference.of("com.mongodb.client.internal.MongoDatabaseImpl"), MemberCategory.INVOKE_PUBLIC_METHODS) .registerType(MapReduceIterable.class, MemberCategory.INVOKE_PUBLIC_METHODS) .registerType(TypeReference.of("com.mongodb.client.internal.MapReduceIterableImpl"), MemberCategory.INVOKE_PUBLIC_METHODS); } @@ -111,6 +112,7 @@ private static void registerMongoCompatibilityAdapterHints(RuntimeHints hints, @ hints.reflection() // .registerType(com.mongodb.reactivestreams.client.MongoDatabase.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MongoDatabaseImpl"), MemberCategory.INVOKE_PUBLIC_METHODS) .registerType(MapReducePublisher.class, MemberCategory.INVOKE_PUBLIC_METHODS) .registerType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MapReducePublisherImpl"), MemberCategory.INVOKE_PUBLIC_METHODS); } From 08e1fa6ce4599634342171d44adf8f4f5f145766 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 26 Jan 2024 12:14:47 +0100 Subject: [PATCH 23/23] try to read driver version from internal MongoDriverVersion type --- .../data/mongodb/util/MongoClientVersion.java | 46 +++++++++++++++++-- .../util/MongoClientVersionUnitTests.java | 44 ++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoClientVersionUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java index f2978b5af4..bf02047625 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java @@ -15,8 +15,12 @@ */ package org.springframework.data.mongodb.util; +import org.springframework.data.util.Version; +import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import com.mongodb.internal.build.MongoDriverVersion; + /** * {@link MongoClientVersion} holds information about the used mongo-java client and is used to distinguish between * different versions. @@ -28,8 +32,8 @@ public class MongoClientVersion { private static final boolean SYNC_CLIENT_PRESENT = ClassUtils.isPresent("com.mongodb.MongoClient", - MongoClientVersion.class.getClassLoader()) || ClassUtils.isPresent("com.mongodb.client.MongoClient", - MongoClientVersion.class.getClassLoader()); + MongoClientVersion.class.getClassLoader()) + || ClassUtils.isPresent("com.mongodb.client.MongoClient", MongoClientVersion.class.getClassLoader()); private static final boolean ASYNC_CLIENT_PRESENT = ClassUtils.isPresent("com.mongodb.async.client.MongoClient", MongoClientVersion.class.getClassLoader()); @@ -37,8 +41,21 @@ public class MongoClientVersion { private static final boolean REACTIVE_CLIENT_PRESENT = ClassUtils .isPresent("com.mongodb.reactivestreams.client.MongoClient", MongoClientVersion.class.getClassLoader()); - private static final boolean IS_VERSION_5_OR_NEWER = ClassUtils - .isPresent("com.mongodb.internal.connection.StreamFactoryFactory", MongoClientVersion.class.getClassLoader()); + private static final boolean IS_VERSION_5_OR_NEWER; + + private static final Version CLIENT_VERSION; + + static { + + ClassLoader classLoader = MongoClientVersion.class.getClassLoader(); + Version version = readVersionFromClass(classLoader); + if (version == null) { + version = guessDriverVersionFromClassPath(classLoader); + } + + CLIENT_VERSION = version; + IS_VERSION_5_OR_NEWER = CLIENT_VERSION.isGreaterThanOrEqualTo(Version.parse("5.0")); + } /** * @return {@literal true} if the async MongoDB Java driver is on classpath. @@ -70,4 +87,25 @@ public static boolean isReactiveClientPresent() { public static boolean isVersion5OrNewer() { return IS_VERSION_5_OR_NEWER; } + + @Nullable + private static Version readVersionFromClass(ClassLoader classLoader) { + + if (ClassUtils.isPresent("com.mongodb.internal.build.MongoDriverVersion", classLoader)) { + try { + return Version.parse(MongoDriverVersion.VERSION); + } catch (IllegalArgumentException exception) { + // well not much we can do, right? + } + } + return null; + } + + private static Version guessDriverVersionFromClassPath(ClassLoader classLoader) { + + if (ClassUtils.isPresent("com.mongodb.internal.connection.StreamFactoryFactory", classLoader)) { + return Version.parse("5"); + } + return Version.parse("4.11"); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoClientVersionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoClientVersionUnitTests.java new file mode 100644 index 0000000000..534146083f --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoClientVersionUnitTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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.util; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.test.util.ClassPathExclusions; +import org.springframework.util.ClassUtils; + +import com.mongodb.internal.build.MongoDriverVersion; + +/** + * Tests for {@link MongoClientVersion}. + * + * @author Christoph Strobl + */ +class MongoClientVersionUnitTests { + + @Test // GH-4578 + void parsesClientVersionCorrectly() { + assertThat(MongoClientVersion.isVersion5OrNewer()).isEqualTo(MongoDriverVersion.VERSION.startsWith("5")); + } + + @Test // GH-4578 + @ClassPathExclusions(packages = { "com.mongodb.internal.build" }) + void fallsBackToClassLookupIfDriverVersionNotPresent() { + assertThat(MongoClientVersion.isVersion5OrNewer()).isEqualTo( + ClassUtils.isPresent("com.mongodb.internal.connection.StreamFactoryFactory", this.getClass().getClassLoader())); + } +}