From a41af448ec23b124d8f5a450c4b43c67a4674771 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Sun, 24 Aug 2025 00:43:06 +0300 Subject: [PATCH] Ensure SingletonSupplier is singleton even if singletonInstance is null Previously, SingletonSupplier stored "null" in singletonInstance when the supplied instance was "null". On subsequent get() calls, this was treated as "uninitialized" and triggered another attempt to obtain an instance from the Supplier. This commit ensures that a "null" returned from the instanceSupplier or defaultSupplier is handled correctly, so that subsequent calls to get() return "null" consistently instead of repeatedly invoking the Supplier. Closes gh-35380 Signed-off-by: Dmytro Nosan --- .../util/function/SingletonSupplier.java | 19 +- .../util/function/SingletonSupplierTests.java | 190 ++++++++++++++++++ ...LErrorCodeSQLExceptionTranslatorTests.java | 1 + 3 files changed, 201 insertions(+), 9 deletions(-) create mode 100644 spring-core/src/test/java/org/springframework/util/function/SingletonSupplierTests.java diff --git a/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java b/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java index e080683d6dc..572141a1675 100644 --- a/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java +++ b/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java @@ -47,6 +47,7 @@ public class SingletonSupplier implements Supplier implements Supplier implements Supplier implements Supplier singletonSupplier = new SingletonSupplier<>(() -> null, () -> "Default"); + assertThat(singletonSupplier.get()).isEqualTo("Default"); + } + + @Test + void shouldReturnNullForOfNullableWithNullInstance() { + SingletonSupplier singletonSupplier = SingletonSupplier.ofNullable((String) null); + assertThat(singletonSupplier).isNull(); + } + + @Test + void shouldReturnNullForOfNullableWithNullSupplier() { + SingletonSupplier singletonSupplier = SingletonSupplier.ofNullable((Supplier) null); + assertThat(singletonSupplier).isNull(); + } + + @Test + void shouldReturnNullWhenAllSuppliersReturnNull() { + SingletonSupplier singletonSupplier = new SingletonSupplier<>(() -> null, () -> null); + assertThat(singletonSupplier.get()).isNull(); + } + + @Test + void shouldReturnNullWhenNoInstanceOrDefaultSupplier() { + SingletonSupplier singletonSupplier = new SingletonSupplier<>((String) null, null); + assertThat(singletonSupplier.get()).isNull(); + } + + @Test + void shouldReturnSingletonInstanceOnMultipleCalls() { + SingletonSupplier singletonSupplier = SingletonSupplier.of("Hello"); + assertThat(singletonSupplier.get()).isEqualTo("Hello"); + assertThat(singletonSupplier.get()).isEqualTo("Hello"); + } + + + @Test + void shouldReturnSingletonInstanceOnMultipleSupplierCalls() { + SingletonSupplier singletonSupplier = SingletonSupplier.of(new HelloStringSupplier()); + assertThat(singletonSupplier.get()).isEqualTo("Hello 0"); + assertThat(singletonSupplier.get()).isEqualTo("Hello 0"); + } + + @Test + void shouldReturnSupplierForOfNullableWithNonNullInstance() { + SingletonSupplier singletonSupplier = SingletonSupplier.ofNullable("Hello"); + assertThat(singletonSupplier).isNotNull(); + assertThat(singletonSupplier.get()).isEqualTo("Hello"); + } + + @Test + void shouldReturnSupplierForOfNullableWithNonNullSupplier() { + SingletonSupplier singletonSupplier = SingletonSupplier.ofNullable(() -> "Hello"); + assertThat(singletonSupplier).isNotNull(); + assertThat(singletonSupplier.get()).isEqualTo("Hello"); + } + + @Test + void shouldThrowWhenObtainCalledAndNoInstanceAvailable() { + SingletonSupplier singletonSupplier = new SingletonSupplier<>((String) null, null); + assertThatThrownBy(singletonSupplier::obtain).isInstanceOf(IllegalStateException.class) + .hasMessage("No instance from Supplier"); + } + + @Test + void shouldUseDefaultSupplierWhenInstanceIsNull() { + SingletonSupplier singletonSupplier = new SingletonSupplier<>((String) null, () -> "defaultSupplier"); + assertThat(singletonSupplier.get()).isEqualTo("defaultSupplier"); + } + + @Test + void shouldUseDefaultSupplierWhenInstanceSupplierReturnsNull() { + SingletonSupplier singletonSupplier = new SingletonSupplier<>((Supplier) null, () -> "defaultSupplier"); + assertThat(singletonSupplier.get()).isEqualTo("defaultSupplier"); + } + + @Test + void shouldUseInstanceSupplierWhenProvidedAndIgnoreDefaultSupplier() { + AtomicInteger defaultValue = new AtomicInteger(); + SingletonSupplier singletonSupplier = new SingletonSupplier<>(() -> -1, defaultValue::incrementAndGet); + assertThat(singletonSupplier.get()).isEqualTo(-1); + assertThat(defaultValue.get()).isEqualTo(0); + } + + @Test + void shouldUseInstanceWhenProvidedAndIgnoreDefaultSupplier() { + AtomicInteger defaultValue = new AtomicInteger(); + SingletonSupplier singletonSupplier = new SingletonSupplier<>(-1, defaultValue::incrementAndGet); + assertThat(singletonSupplier.get()).isEqualTo(-1); + assertThat(defaultValue.get()).isEqualTo(0); + } + + @Test + void shouldReturnConsistentlyNullSingletonInstanceOnMultipleSupplierCalls() { + SingletonSupplier singletonSupplier = SingletonSupplier.of(new Supplier<>() { + + int count = 0; + + @Override + public String get() { + if (this.count++ == 0) { + return null; + } + return "Hello"; + } + }); + + assertThat(singletonSupplier.get()).isNull(); + assertThat(singletonSupplier.get()).isNull(); + } + + @RepeatedTest(100) + void shouldReturnSingletonInstanceOnMultipleConcurrentSupplierCalls() throws Exception { + int numberOfThreads = 4; + CountDownLatch ready = new CountDownLatch(numberOfThreads); + CountDownLatch start = new CountDownLatch(1); + List> futures = new ArrayList<>(); + SingletonSupplier singletonSupplier = SingletonSupplier.of(new HelloStringSupplier()); + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); + try { + for (int i = 0; i < numberOfThreads; i++) { + futures.add(executorService.submit(() -> { + ready.countDown(); + start.await(); + return singletonSupplier.obtain(); + })); + } + ready.await(); + start.countDown(); + assertThat(futures).extracting(Future::get).containsOnly("Hello 0"); + } + finally { + executorService.shutdown(); + } + } + + + private static final class HelloStringSupplier implements Supplier { + + private final AtomicInteger count = new AtomicInteger(); + + @Override + public String get() { + return "Hello " + this.count.getAndIncrement(); + } + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java index d8baf969244..fba4ada29b0 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java @@ -209,6 +209,7 @@ class SQLErrorCodeSQLExceptionTranslatorTests { reset(dataSource); given(dataSource.getConnection()).willReturn(connection); + translator = new SQLErrorCodeSQLExceptionTranslator(dataSource); assertThat(translator.translate("test", null, duplicateKeyException)) .isInstanceOf(DuplicateKeyException.class);