Skip to content

Commit bce85c4

Browse files
christophstroblmp911de
authored andcommitted
Assert compatibility with MongoDB Driver 5.
We now are compatible with MongoDB driver versions 4 and 5. Driver versions can be interchanged and our adapter bridges changed methods via reflection. Usage of removed functionality is either ignored or fails with an exception. Original pull request: #4624 Closes: #4578
1 parent 3bc214c commit bce85c4

33 files changed

+1557
-128
lines changed

Diff for: Jenkinsfile

+28
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,34 @@ pipeline {
265265
}
266266
}
267267

268+
stage("test: MongoDB 7.0 (driver-next)") {
269+
agent {
270+
label 'data'
271+
}
272+
options { timeout(time: 30, unit: 'MINUTES') }
273+
environment {
274+
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
275+
DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}")
276+
DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}")
277+
}
278+
steps {
279+
script {
280+
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']) {
281+
sh 'mkdir -p /tmp/mongodb/db /tmp/mongodb/log'
282+
sh 'mongod --setParameter transactionLifetimeLimitSeconds=90 --setParameter maxTransactionLockRequestTimeoutMillis=10000 --dbpath /tmp/mongodb/db --replSet rs0 --fork --logpath /tmp/mongodb/log/mongod.log &'
283+
sh 'sleep 10'
284+
sh 'mongosh --eval "rs.initiate({_id: \'rs0\', members:[{_id: 0, host: \'127.0.0.1:27017\'}]});"'
285+
sh 'sleep 15'
286+
sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' +
287+
"DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR} " +
288+
"DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW} " +
289+
"GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY} " +
290+
"./mvnw -s settings.xml -Pmongo-5.0 clean dependency:list test -Dsort -U -B -Dgradle.cache.local.enabled=false -Dgradle.cache.remote.enabled=false"
291+
}
292+
}
293+
}
294+
}
295+
268296
stage("test: MongoDB 7.0 (next)") {
269297
agent {
270298
label 'data'

Diff for: pom.xml

+7
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@
132132
<module>spring-data-mongodb-benchmarks</module>
133133
</modules>
134134
</profile>
135+
<profile>
136+
<id>mongo-5.0</id>
137+
<properties>
138+
<mongo>5.0.0-beta0</mongo>
139+
</properties>
140+
</profile>
141+
135142
</profiles>
136143

137144
<dependencies>

Diff for: spring-data-mongodb/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,12 @@
260260
<scope>test</scope>
261261
</dependency>
262262

263+
<dependency>
264+
<groupId>org.junit.platform</groupId>
265+
<artifactId>junit-platform-launcher</artifactId>
266+
<scope>test</scope>
267+
</dependency>
268+
263269
<dependency>
264270
<groupId>jakarta.transaction</groupId>
265271
<artifactId>jakarta.transaction-api</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb;
17+
18+
import java.lang.reflect.Method;
19+
import java.net.InetAddress;
20+
import java.net.InetSocketAddress;
21+
22+
import com.mongodb.ServerAddress;
23+
import org.apache.commons.logging.Log;
24+
import org.apache.commons.logging.LogFactory;
25+
import org.springframework.data.mongodb.core.MongoClientSettingsFactoryBean;
26+
import org.springframework.data.mongodb.util.MongoClientVersion;
27+
import org.springframework.util.ClassUtils;
28+
import org.springframework.util.ReflectionUtils;
29+
30+
import com.mongodb.MongoClientSettings;
31+
import com.mongodb.MongoClientSettings.Builder;
32+
import com.mongodb.client.MapReduceIterable;
33+
import com.mongodb.client.model.IndexOptions;
34+
import com.mongodb.reactivestreams.client.MapReducePublisher;
35+
36+
/**
37+
* @author Christoph Strobl
38+
* @since 2023/12
39+
*/
40+
public class MongoCompatibilityAdapter {
41+
42+
private static final String NO_LONGER_SUPPORTED = "%s is no longer supported on Mongo Client 5+";
43+
44+
public static ClientSettingsBuilderAdapter clientSettingsBuilderAdapter(MongoClientSettings.Builder builder) {
45+
return new MongoStreamFactoryFactorySettingsConfigurer(builder)::setStreamFactory;
46+
}
47+
48+
public static ClientSettingsAdapter clientSettingsAdapter(MongoClientSettings clientSettings) {
49+
return new ClientSettingsAdapter() {
50+
@Override
51+
public <T> T getStreamFactoryFactory() {
52+
if (MongoClientVersion.is5PlusClient()) {
53+
return null;
54+
}
55+
56+
Method getStreamFactoryFactory = ReflectionUtils.findMethod(MongoClientSettings.class,
57+
"getStreamFactoryFactory");
58+
return getStreamFactoryFactory != null
59+
? (T) ReflectionUtils.invokeMethod(getStreamFactoryFactory, clientSettings)
60+
: null;
61+
}
62+
};
63+
}
64+
65+
public static IndexOptionsAdapter indexOptionsAdapter(IndexOptions options) {
66+
return new IndexOptionsAdapter() {
67+
@Override
68+
public void setBucketSize(Double bucketSize) {
69+
70+
if (MongoClientVersion.is5PlusClient()) {
71+
throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("IndexOptions.bucketSize"));
72+
}
73+
74+
Method setBucketSize = ReflectionUtils.findMethod(IndexOptions.class, "bucketSize", Double.class);
75+
ReflectionUtils.invokeMethod(setBucketSize, options, bucketSize);
76+
}
77+
};
78+
}
79+
80+
@SuppressWarnings({ "deprecation" })
81+
public static MapReduceIterableAdapter mapReduceIterableAdapter(MapReduceIterable<?> iterable) {
82+
return sharded -> {
83+
if (MongoClientVersion.is5PlusClient()) {
84+
throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded"));
85+
}
86+
87+
Method shardedMethod = ReflectionUtils.findMethod(iterable.getClass(), "MapReduceIterable.sharded",
88+
boolean.class);
89+
ReflectionUtils.invokeMethod(shardedMethod, iterable, shardedMethod);
90+
};
91+
}
92+
93+
public static MapReducePublisherAdapter mapReducePublisherAdapter(MapReducePublisher<?> publisher) {
94+
return sharded -> {
95+
if (MongoClientVersion.is5PlusClient()) {
96+
throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded"));
97+
}
98+
99+
Method shardedMethod = ReflectionUtils.findMethod(publisher.getClass(), "MapReduceIterable.sharded",
100+
boolean.class);
101+
ReflectionUtils.invokeMethod(shardedMethod, publisher, shardedMethod);
102+
};
103+
}
104+
105+
public static ServerAddressAdapter serverAddressAdapter(ServerAddress serverAddress) {
106+
return new ServerAddressAdapter() {
107+
@Override
108+
public InetSocketAddress getSocketAddress() {
109+
110+
if(MongoClientVersion.is5PlusClient()) {
111+
return null;
112+
}
113+
114+
Method serverAddressMethod = ReflectionUtils.findMethod(serverAddress.getClass(), "getSocketAddress");
115+
Object value = ReflectionUtils.invokeMethod(serverAddressMethod, serverAddress);
116+
return value != null ? InetSocketAddress.class.cast(value) : null;
117+
}
118+
};
119+
}
120+
121+
public interface IndexOptionsAdapter {
122+
void setBucketSize(Double bucketSize);
123+
}
124+
125+
public interface ClientSettingsAdapter {
126+
<T> T getStreamFactoryFactory();
127+
}
128+
129+
public interface ClientSettingsBuilderAdapter {
130+
<T> void setStreamFactoryFactory(T streamFactory);
131+
}
132+
133+
public interface MapReduceIterableAdapter {
134+
void sharded(boolean sharded);
135+
}
136+
137+
public interface MapReducePublisherAdapter {
138+
void sharded(boolean sharded);
139+
}
140+
141+
public interface ServerAddressAdapter {
142+
InetSocketAddress getSocketAddress();
143+
}
144+
145+
static class MongoStreamFactoryFactorySettingsConfigurer {
146+
147+
private static final Log logger = LogFactory.getLog(MongoClientSettingsFactoryBean.class);
148+
149+
private static final String STREAM_FACTORY_NAME = "com.mongodb.connection.StreamFactoryFactory";
150+
private static final boolean STREAM_FACTORY_PRESENT = ClassUtils.isPresent(STREAM_FACTORY_NAME,
151+
MongoCompatibilityAdapter.class.getClassLoader());
152+
private final MongoClientSettings.Builder settingsBuilder;
153+
154+
static boolean isStreamFactoryPresent() {
155+
return STREAM_FACTORY_PRESENT;
156+
}
157+
158+
public MongoStreamFactoryFactorySettingsConfigurer(Builder settingsBuilder) {
159+
this.settingsBuilder = settingsBuilder;
160+
}
161+
162+
void setStreamFactory(Object streamFactory) {
163+
164+
if (MongoClientVersion.is5PlusClient()) {
165+
logger.warn("StreamFactoryFactory is no longer available. Use TransportSettings instead.");
166+
}
167+
168+
if (isStreamFactoryPresent()) { //
169+
try {
170+
Class<?> streamFactoryType = ClassUtils.forName(STREAM_FACTORY_NAME,
171+
streamFactory.getClass().getClassLoader());
172+
if (!ClassUtils.isAssignable(streamFactoryType, streamFactory.getClass())) {
173+
throw new IllegalArgumentException("Expected %s but found %s".formatted(streamFactoryType, streamFactory));
174+
}
175+
176+
Method setter = ReflectionUtils.findMethod(settingsBuilder.getClass(), "streamFactoryFactory",
177+
streamFactoryType);
178+
if (setter != null) {
179+
ReflectionUtils.invokeMethod(setter, settingsBuilder, streamFactoryType.cast(streamFactory));
180+
}
181+
} catch (ClassNotFoundException e) {
182+
throw new IllegalArgumentException("Cannot set StreamFactoryFactory for %s".formatted(settingsBuilder), e);
183+
}
184+
}
185+
}
186+
}
187+
188+
}

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java

+17
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
import org.springframework.util.ClassUtils;
2626

2727
/**
28+
* Collection of {@link Predicate predicates} to determine dynamic library aspects during AOT computation.
29+
* Intended for internal usage only.
30+
*
2831
* @author Christoph Strobl
2932
* @since 4.0
3033
*/
@@ -33,13 +36,27 @@ public class MongoAotPredicates {
3336
public static final Predicate<Class<?>> IS_SIMPLE_TYPE = (type) -> MongoSimpleTypes.HOLDER.isSimpleType(type) || TypeUtils.type(type).isPartOf("org.bson");
3437
public static final Predicate<ReactiveLibrary> IS_REACTIVE_LIBARARY_AVAILABLE = ReactiveWrappers::isAvailable;
3538
public static final Predicate<ClassLoader> IS_SYNC_CLIENT_PRESENT = (classLoader) -> ClassUtils.isPresent("com.mongodb.client.MongoClient", classLoader);
39+
public static final Predicate<ClassLoader> IS_REACTIVE_CLIENT_PRESENT = (classLoader) -> ClassUtils.isPresent("com.mongodb.reactivestreams.client.MongoClient", classLoader);
3640

3741
public static boolean isReactorPresent() {
3842
return IS_REACTIVE_LIBARARY_AVAILABLE.test(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR);
3943
}
4044

45+
/**
46+
* @param classLoader can be {@literal null}.
47+
* @return {@literal true} if the {@link com.mongodb.client.MongoClient} is present.
48+
* @since 4.0
49+
*/
4150
public static boolean isSyncClientPresent(@Nullable ClassLoader classLoader) {
4251
return IS_SYNC_CLIENT_PRESENT.test(classLoader);
4352
}
4453

54+
/**
55+
* @param classLoader can be {@literal null}.
56+
* @return {@literal true} if the {@link com.mongodb.reactivestreams.client.MongoClient} is present.
57+
* @since 4.3
58+
*/
59+
public static boolean isReactiveClientPresent(@Nullable ClassLoader classLoader) {
60+
return IS_REACTIVE_CLIENT_PRESENT.test(classLoader);
61+
}
4562
}

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java

+38
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@
1919

2020
import java.util.Arrays;
2121

22+
import com.mongodb.MongoClientSettings;
23+
import com.mongodb.ServerAddress;
24+
import com.mongodb.UnixServerAddress;
25+
import com.mongodb.client.MapReduceIterable;
26+
import com.mongodb.client.MongoDatabase;
27+
import com.mongodb.client.model.IndexOptions;
28+
import com.mongodb.reactivestreams.client.MapReducePublisher;
2229
import org.springframework.aot.hint.MemberCategory;
2330
import org.springframework.aot.hint.RuntimeHints;
2431
import org.springframework.aot.hint.RuntimeHintsRegistrar;
@@ -31,6 +38,7 @@
3138
import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterSaveCallback;
3239
import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback;
3340
import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCallback;
41+
import org.springframework.data.mongodb.util.MongoClientVersion;
3442
import org.springframework.lang.Nullable;
3543
import org.springframework.util.ClassUtils;
3644

@@ -53,6 +61,7 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader)
5361
MemberCategory.INVOKE_PUBLIC_METHODS));
5462

5563
registerTransactionProxyHints(hints, classLoader);
64+
registerMongoCompatibilityAdapterHints(hints, classLoader);
5665

5766
if (isReactorPresent()) {
5867

@@ -80,4 +89,33 @@ private static void registerTransactionProxyHints(RuntimeHints hints, @Nullable
8089
}
8190
}
8291

92+
private static void registerMongoCompatibilityAdapterHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
93+
94+
hints.reflection() //
95+
.registerType(MongoClientSettings.class, MemberCategory.INVOKE_PUBLIC_METHODS)
96+
.registerType(MongoClientSettings.Builder.class, MemberCategory.INVOKE_PUBLIC_METHODS)
97+
.registerType(IndexOptions.class, MemberCategory.INVOKE_PUBLIC_METHODS)
98+
.registerType(ServerAddress.class, MemberCategory.INVOKE_PUBLIC_METHODS)
99+
.registerType(UnixServerAddress.class, MemberCategory.INVOKE_PUBLIC_METHODS)
100+
.registerType(TypeReference.of("com.mongodb.connection.StreamFactoryFactory"), MemberCategory.INTROSPECT_PUBLIC_METHODS);
101+
102+
if(MongoAotPredicates.isSyncClientPresent(classLoader)) {
103+
104+
hints.reflection() //
105+
.registerType(MongoDatabase.class, MemberCategory.INVOKE_PUBLIC_METHODS)
106+
.registerType(TypeReference.of("com.mongodb.client.internal.MongoDatabaseImpl"), MemberCategory.INVOKE_PUBLIC_METHODS)
107+
.registerType(MapReduceIterable.class, MemberCategory.INVOKE_PUBLIC_METHODS)
108+
.registerType(TypeReference.of("com.mongodb.client.internal.MapReduceIterableImpl"), MemberCategory.INVOKE_PUBLIC_METHODS);
109+
}
110+
111+
if(MongoAotPredicates.isReactiveClientPresent(classLoader)) {
112+
113+
hints.reflection() //
114+
.registerType(com.mongodb.reactivestreams.client.MongoDatabase.class, MemberCategory.INVOKE_PUBLIC_METHODS)
115+
.registerType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MongoDatabaseImpl"), MemberCategory.INVOKE_PUBLIC_METHODS)
116+
.registerType(MapReducePublisher.class, MemberCategory.INVOKE_PUBLIC_METHODS)
117+
.registerType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MapReducePublisherImpl"), MemberCategory.INVOKE_PUBLIC_METHODS);
118+
}
119+
}
120+
83121
}

Diff for: spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919

2020
import org.bson.Document;
2121
import org.springframework.core.convert.converter.Converter;
22+
import org.springframework.data.mongodb.MongoCompatibilityAdapter;
2223
import org.springframework.data.mongodb.core.index.IndexDefinition;
2324
import org.springframework.data.mongodb.core.index.IndexInfo;
25+
import org.springframework.data.mongodb.util.MongoClientVersion;
2426
import org.springframework.lang.Nullable;
2527
import org.springframework.util.ObjectUtils;
2628

@@ -89,7 +91,7 @@ private static Converter<IndexDefinition, IndexOptions> getIndexDefinitionIndexO
8991
ops = ops.bits((Integer) indexOptions.get("bits"));
9092
}
9193
if (indexOptions.containsKey("bucketSize")) {
92-
ops = ops.bucketSize(((Number) indexOptions.get("bucketSize")).doubleValue());
94+
MongoCompatibilityAdapter.indexOptionsAdapter(ops).setBucketSize(((Number) indexOptions.get("bucketSize")).doubleValue());
9395
}
9496
if (indexOptions.containsKey("default_language")) {
9597
ops = ops.defaultLanguage(indexOptions.get("default_language").toString());

0 commit comments

Comments
 (0)