From 3fa56db88b62483183aa941d01a2a24d4fc0fcd0 Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Thu, 13 Nov 2025 11:51:39 -0800 Subject: [PATCH 001/235] Fix Spliterator characteristics in ConcurrentReferenceHashMap The Spliterators returned by values, entrySet, and keySet incorrectly reported the SIZED characteristic, instead of CONCURRENT. This could lead to bugs when the map is concurrently modified during a stream operation. For keySet and values, the incorrect characteristics are inherited from AbstractMap, so to rectify that the respective methods are overridden, and custom collections are provided that report the correct Spliterator characteristics. Closes gh-35817 Signed-off-by: Patrick Strawderman (cherry picked from commit ed7590683467efcea27507bd5cfab6acc006239c) --- .../util/ConcurrentReferenceHashMap.java | 160 ++++++++++++++- .../util/ConcurrentReferenceHashMapTests.java | 194 ++++++++++++++++-- 2 files changed, 339 insertions(+), 15 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java index 7ebcb78aa808..b4daff966d76 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -20,8 +20,10 @@ import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.lang.reflect.Array; +import java.util.AbstractCollection; import java.util.AbstractMap; import java.util.AbstractSet; +import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; @@ -29,6 +31,8 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; @@ -104,6 +108,18 @@ public class ConcurrentReferenceHashMap extends AbstractMap implemen @Nullable private volatile Set> entrySet; + /** + * Late binding key set. + */ + @Nullable + private Set keySet; + + /** + * Late binding values collection. + */ + @Nullable + private Collection values; + /** * Create a new {@code ConcurrentReferenceHashMap} instance. @@ -528,6 +544,26 @@ public Set> entrySet() { return entrySet; } + @Override + public Set keySet() { + Set keySet = this.keySet; + if (keySet == null) { + keySet = new KeySet(); + this.keySet = keySet; + } + return keySet; + } + + @Override + public Collection values() { + Collection values = this.values; + if (values == null) { + values = new Values(); + this.values = values; + } + return values; + } + @Nullable private T doTask(@Nullable Object key, Task task) { int hash = getHash(key); @@ -969,7 +1005,7 @@ private interface Entries { /** * Internal entry-set implementation. */ - private class EntrySet extends AbstractSet> { + private final class EntrySet extends AbstractSet> { @Override public Iterator> iterator() { @@ -1005,13 +1041,133 @@ public int size() { public void clear() { ConcurrentReferenceHashMap.this.clear(); } + + @Override + public Spliterator> spliterator() { + return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.CONCURRENT); + } + } + + /** + * Internal key-set implementation. + */ + private final class KeySet extends AbstractSet { + @Override + public Iterator iterator() { + return new KeyIterator(); + } + + @Override + public int size() { + return ConcurrentReferenceHashMap.this.size(); + } + + @Override + public boolean isEmpty() { + return ConcurrentReferenceHashMap.this.isEmpty(); + } + + @Override + public void clear() { + ConcurrentReferenceHashMap.this.clear(); + } + + @Override + public boolean contains(Object k) { + return ConcurrentReferenceHashMap.this.containsKey(k); + } + + @Override + public Spliterator spliterator() { + return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.CONCURRENT); + } + } + + /** + * Internal key iterator implementation. + */ + private final class KeyIterator implements Iterator { + + private final Iterator> iterator = entrySet().iterator(); + + @Override + public boolean hasNext() { + return this.iterator.hasNext(); + } + + @Override + public void remove() { + this.iterator.remove(); + } + + @Override + public K next() { + return this.iterator.next().getKey(); + } + } + + /** + * Internal values collection implementation. + */ + private final class Values extends AbstractCollection { + @Override + public Iterator iterator() { + return new ValueIterator(); + } + + @Override + public int size() { + return ConcurrentReferenceHashMap.this.size(); + } + + @Override + public boolean isEmpty() { + return ConcurrentReferenceHashMap.this.isEmpty(); + } + + @Override + public void clear() { + ConcurrentReferenceHashMap.this.clear(); + } + + @Override + public boolean contains(Object v) { + return ConcurrentReferenceHashMap.this.containsValue(v); + } + + @Override + public Spliterator spliterator() { + return Spliterators.spliterator(this, Spliterator.CONCURRENT); + } } + /** + * Internal value iterator implementation. + */ + private final class ValueIterator implements Iterator { + + private final Iterator> iterator = entrySet().iterator(); + + @Override + public boolean hasNext() { + return this.iterator.hasNext(); + } + + @Override + public void remove() { + this.iterator.remove(); + } + + @Override + public V next() { + return this.iterator.next().getValue(); + } + } /** * Internal entry iterator implementation. */ - private class EntryIterator implements Iterator> { + private final class EntryIterator implements Iterator> { private int segmentIndex; diff --git a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java index 5a001e74b7bf..8e67f3bd44f8 100644 --- a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java @@ -16,8 +16,8 @@ package org.springframework.util; -import java.util.ArrayList; -import java.util.Comparator; +import java.util.AbstractMap; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -25,6 +25,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Spliterator; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -32,12 +34,12 @@ import org.springframework.util.ConcurrentReferenceHashMap.Entry; import org.springframework.util.ConcurrentReferenceHashMap.Reference; import org.springframework.util.ConcurrentReferenceHashMap.Restructure; -import org.springframework.util.comparator.Comparators; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link ConcurrentReferenceHashMap}. @@ -47,8 +49,6 @@ */ class ConcurrentReferenceHashMapTests { - private static final Comparator NULL_SAFE_STRING_SORT = Comparators.nullsLow(); - private TestWeakConcurrentCache map = new TestWeakConcurrentCache<>(); @@ -450,19 +450,176 @@ void keySet() { assertThat(this.map.keySet()).isEqualTo(expected); } + @Test + void keySetContains() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + assertThat(this.map.keySet()).containsExactlyInAnyOrder(123, 456, null); + } + + @Test + void keySetRemove() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + assertThat(this.map.keySet().remove(123)).isTrue(); + assertThat(this.map).doesNotContainKey(123); + assertThat(this.map.keySet().remove(123)).isFalse(); + } + + @Test + void keySetIterator() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + Iterator it = this.map.keySet().iterator(); + assertThat(it).toIterable().containsExactlyInAnyOrder(123, 456, null); + assertThat(it).isExhausted(); + } + + @Test + void keySetIteratorRemove() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + Iterator keySetIterator = this.map.keySet().iterator(); + while (keySetIterator.hasNext()) { + Integer key = keySetIterator.next(); + if (key != null && key.equals(456)) { + keySetIterator.remove(); + } + } + assertThat(this.map).containsOnlyKeys(123, null); + } + + @Test + void keySetClear() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + this.map.keySet().clear(); + assertThat(this.map).isEmpty(); + assertThat(this.map.keySet()).isEmpty(); + } + + @Test + void keySetAdd() { + assertThatThrownBy(() -> + this.map.keySet().add(12345) + ).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void keySetStream() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + Set keys = this.map.keySet().stream().collect(Collectors.toSet()); + assertThat(keys).containsExactlyInAnyOrder(123, 456, null); + } + + @Test + void keySetSpliteratorCharacteristics() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + Spliterator spliterator = this.map.keySet().spliterator(); + assertThat(spliterator).hasOnlyCharacteristics(Spliterator.CONCURRENT, Spliterator.DISTINCT); + assertThat(spliterator.estimateSize()).isEqualTo(3L); + assertThat(spliterator.getExactSizeIfKnown()).isEqualTo(-1L); + } + @Test void valuesCollection() { this.map.put(123, "123"); this.map.put(456, null); this.map.put(null, "789"); - List actual = new ArrayList<>(this.map.values()); - List expected = new ArrayList<>(); - expected.add("123"); - expected.add(null); - expected.add("789"); - actual.sort(NULL_SAFE_STRING_SORT); - expected.sort(NULL_SAFE_STRING_SORT); - assertThat(actual).isEqualTo(expected); + assertThat(this.map.values()).containsExactlyInAnyOrder("123", null, "789"); + } + + @Test + void valuesCollectionAdd() { + assertThatThrownBy(() -> + this.map.values().add("12345") + ).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void valuesCollectionClear() { + Collection values = this.map.values(); + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + assertThat(values).isNotEmpty(); + values.clear(); + assertThat(values).isEmpty(); + assertThat(this.map).isEmpty(); + } + + @Test + void valuesCollectionRemoval() { + Collection values = this.map.values(); + assertThat(values).isEmpty(); + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + assertThat(values).containsExactlyInAnyOrder("123", null, "789"); + values.remove(null); + assertThat(values).containsExactlyInAnyOrder("123", "789"); + assertThat(map).containsOnly(new AbstractMap.SimpleEntry<>(123, "123"), new AbstractMap.SimpleEntry<>(null, "789")); + values.remove("123"); + values.remove("789"); + assertThat(values).isEmpty(); + assertThat(map).isEmpty(); + } + + @Test + void valuesCollectionIterator() { + Iterator iterator = this.map.values().iterator(); + assertThat(iterator).isExhausted(); + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + iterator = this.map.values().iterator(); + assertThat(iterator).toIterable().containsExactlyInAnyOrder("123", null, "789"); + } + + @Test + void valuesCollectionIteratorRemoval() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + Iterator iterator = this.map.values().iterator(); + while (iterator.hasNext()) { + String value = iterator.next(); + if (value != null && value.equals("789")) { + iterator.remove(); + } + } + assertThat(iterator).isExhausted(); + assertThat(this.map.values()).containsExactlyInAnyOrder("123", null); + assertThat(this.map).containsOnlyKeys(123, 456); + } + + @Test + void valuesCollectionStream() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + List values = this.map.values().stream().collect(Collectors.toList()); + assertThat(values).containsExactlyInAnyOrder("123", null, "789"); + } + + @Test + void valuesCollectionSpliteratorCharacteristics() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + Spliterator spliterator = this.map.values().spliterator(); + assertThat(spliterator).hasOnlyCharacteristics(Spliterator.CONCURRENT); + assertThat(spliterator.estimateSize()).isEqualTo(3L); + assertThat(spliterator.getExactSizeIfKnown()).isEqualTo(-1L); } @Test @@ -541,6 +698,17 @@ void containsViaEntrySet() { copy.forEach(entry -> assertThat(entrySet).doesNotContain(entry)); } + @Test + void entrySetSpliteratorCharacteristics() { + this.map.put(1, "1"); + this.map.put(2, "2"); + this.map.put(3, "3"); + Spliterator> spliterator = this.map.entrySet().spliterator(); + assertThat(spliterator).hasOnlyCharacteristics(Spliterator.CONCURRENT, Spliterator.DISTINCT); + assertThat(spliterator.estimateSize()).isEqualTo(3L); + assertThat(spliterator.getExactSizeIfKnown()).isEqualTo(-1L); + } + @Test void supportNullReference() { // GC could happen during restructure so we must be able to create a reference for a null entry From 6be1c29fda9ef30e4e4faf20d449f4705e40b77b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:20:12 +0100 Subject: [PATCH 002/235] Polish contribution See gh-35817 (cherry picked from commit 09a8bbc0c7113b09bca26c6ede44fb1b26c73d9b) --- .../util/ConcurrentReferenceHashMap.java | 2 + .../util/ConcurrentReferenceHashMapTests.java | 52 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java index b4daff966d76..563ea1529e5e 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -1052,6 +1052,7 @@ public Spliterator> spliterator() { * Internal key-set implementation. */ private final class KeySet extends AbstractSet { + @Override public Iterator iterator() { return new KeyIterator(); @@ -1110,6 +1111,7 @@ public K next() { * Internal values collection implementation. */ private final class Values extends AbstractCollection { + @Override public Iterator iterator() { return new ValueIterator(); diff --git a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java index 8e67f3bd44f8..4307e6a20213 100644 --- a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java @@ -16,7 +16,6 @@ package org.springframework.util; -import java.util.AbstractMap; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -36,10 +35,11 @@ import org.springframework.util.ConcurrentReferenceHashMap.Restructure; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatNoException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; /** * Tests for {@link ConcurrentReferenceHashMap}. @@ -450,7 +450,7 @@ void keySet() { assertThat(this.map.keySet()).isEqualTo(expected); } - @Test + @Test // gh-35817 void keySetContains() { this.map.put(123, "123"); this.map.put(456, null); @@ -458,7 +458,7 @@ void keySetContains() { assertThat(this.map.keySet()).containsExactlyInAnyOrder(123, 456, null); } - @Test + @Test // gh-35817 void keySetRemove() { this.map.put(123, "123"); this.map.put(456, null); @@ -468,7 +468,7 @@ void keySetRemove() { assertThat(this.map.keySet().remove(123)).isFalse(); } - @Test + @Test // gh-35817 void keySetIterator() { this.map.put(123, "123"); this.map.put(456, null); @@ -478,7 +478,7 @@ void keySetIterator() { assertThat(it).isExhausted(); } - @Test + @Test // gh-35817 void keySetIteratorRemove() { this.map.put(123, "123"); this.map.put(456, null); @@ -493,7 +493,7 @@ void keySetIteratorRemove() { assertThat(this.map).containsOnlyKeys(123, null); } - @Test + @Test // gh-35817 void keySetClear() { this.map.put(123, "123"); this.map.put(456, null); @@ -503,14 +503,13 @@ void keySetClear() { assertThat(this.map.keySet()).isEmpty(); } - @Test + @Test // gh-35817 void keySetAdd() { - assertThatThrownBy(() -> - this.map.keySet().add(12345) - ).isInstanceOf(UnsupportedOperationException.class); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.map.keySet().add(12345)); } - @Test + @Test // gh-35817 void keySetStream() { this.map.put(123, "123"); this.map.put(456, null); @@ -519,7 +518,7 @@ void keySetStream() { assertThat(keys).containsExactlyInAnyOrder(123, 456, null); } - @Test + @Test // gh-35817 void keySetSpliteratorCharacteristics() { this.map.put(123, "123"); this.map.put(456, null); @@ -538,26 +537,25 @@ void valuesCollection() { assertThat(this.map.values()).containsExactlyInAnyOrder("123", null, "789"); } - @Test + @Test // gh-35817 void valuesCollectionAdd() { - assertThatThrownBy(() -> - this.map.values().add("12345") - ).isInstanceOf(UnsupportedOperationException.class); + assertThatExceptionOfType(UnsupportedOperationException.class) + .isThrownBy(() -> this.map.values().add("12345")); } - @Test + @Test // gh-35817 void valuesCollectionClear() { Collection values = this.map.values(); this.map.put(123, "123"); this.map.put(456, null); this.map.put(null, "789"); - assertThat(values).isNotEmpty(); + assertThat(values).hasSize(3); values.clear(); assertThat(values).isEmpty(); assertThat(this.map).isEmpty(); } - @Test + @Test // gh-35817 void valuesCollectionRemoval() { Collection values = this.map.values(); assertThat(values).isEmpty(); @@ -567,14 +565,14 @@ void valuesCollectionRemoval() { assertThat(values).containsExactlyInAnyOrder("123", null, "789"); values.remove(null); assertThat(values).containsExactlyInAnyOrder("123", "789"); - assertThat(map).containsOnly(new AbstractMap.SimpleEntry<>(123, "123"), new AbstractMap.SimpleEntry<>(null, "789")); + assertThat(map).containsOnly(entry(123, "123"), entry(null, "789")); values.remove("123"); values.remove("789"); assertThat(values).isEmpty(); assertThat(map).isEmpty(); } - @Test + @Test // gh-35817 void valuesCollectionIterator() { Iterator iterator = this.map.values().iterator(); assertThat(iterator).isExhausted(); @@ -585,7 +583,7 @@ void valuesCollectionIterator() { assertThat(iterator).toIterable().containsExactlyInAnyOrder("123", null, "789"); } - @Test + @Test // gh-35817 void valuesCollectionIteratorRemoval() { this.map.put(123, "123"); this.map.put(456, null); @@ -602,16 +600,16 @@ void valuesCollectionIteratorRemoval() { assertThat(this.map).containsOnlyKeys(123, 456); } - @Test + @Test // gh-35817 void valuesCollectionStream() { this.map.put(123, "123"); this.map.put(456, null); this.map.put(null, "789"); - List values = this.map.values().stream().collect(Collectors.toList()); + List values = this.map.values().stream().toList(); assertThat(values).containsExactlyInAnyOrder("123", null, "789"); } - @Test + @Test // gh-35817 void valuesCollectionSpliteratorCharacteristics() { this.map.put(123, "123"); this.map.put(456, null); @@ -698,7 +696,7 @@ void containsViaEntrySet() { copy.forEach(entry -> assertThat(entrySet).doesNotContain(entry)); } - @Test + @Test // gh-35817 void entrySetSpliteratorCharacteristics() { this.map.put(1, "1"); this.map.put(2, "2"); From 46ee944accdc35d4e5cac97c11ec413898c72f1b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 13 Nov 2025 10:08:35 +0000 Subject: [PATCH 003/235] Update Antora Spring UI to v0.4.20 Closes gh-35814 --- framework-docs/antora-playbook.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/antora-playbook.yml b/framework-docs/antora-playbook.yml index 62f0f71a8a3f..8e4a0a2cb75c 100644 --- a/framework-docs/antora-playbook.yml +++ b/framework-docs/antora-playbook.yml @@ -36,4 +36,4 @@ runtime: failure_level: warn ui: bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.20/ui-bundle.zip From f3ed04c9d72ee39ea239a4012851270467b283c3 Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Fri, 14 Nov 2025 12:55:11 -0800 Subject: [PATCH 004/235] Fix single-check idiom in UnmodifiableMultiValueMap Read the respective fields only once in the values(), entrySet(), and keySet() methods. Closes gh-35822 Signed-off-by: Patrick Strawderman (cherry picked from commit 3b6be3d4d3fa375735cc27b2a95e87a00a0531bf) --- .../util/UnmodifiableMultiValueMap.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java b/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java index 8209d9b90f61..ce6edbd71eec 100644 --- a/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java +++ b/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java @@ -145,26 +145,32 @@ public String toString() { @Override public Set keySet() { - if (this.keySet == null) { - this.keySet = Collections.unmodifiableSet(this.delegate.keySet()); + Set keySet = this.keySet; + if (keySet == null) { + keySet = Collections.unmodifiableSet(this.delegate.keySet()); + this.keySet = keySet; } - return this.keySet; + return keySet; } @Override public Set>> entrySet() { - if (this.entrySet == null) { - this.entrySet = new UnmodifiableEntrySet<>(this.delegate.entrySet()); + Set>> entrySet = this.entrySet; + if (entrySet == null) { + entrySet = new UnmodifiableEntrySet<>(this.delegate.entrySet()); + this.entrySet = entrySet; } - return this.entrySet; + return entrySet; } @Override public Collection> values() { - if (this.values == null) { - this.values = new UnmodifiableValueCollection<>(this.delegate.values()); + Collection> values = this.values; + if (values == null) { + values = new UnmodifiableValueCollection<>(this.delegate.values()); + this.values = values; } - return this.values; + return values; } // unsupported From 5af1c9b487e06c3bad1b0c7981132d3f391877bf Mon Sep 17 00:00:00 2001 From: potato <65760583+juntae6942@users.noreply.github.com> Date: Sat, 13 Sep 2025 23:15:49 +0900 Subject: [PATCH 005/235] Fix HtmlUtils unescape for supplementary chars See gh-35477 Signed-off-by: potato <65760583+juntae6942@users.noreply.github.com> --- .../web/util/HtmlCharacterEntityDecoder.java | 5 ++- .../util/HtmlCharacterEntityDecoderTest.java | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTest.java diff --git a/spring-web/src/main/java/org/springframework/web/util/HtmlCharacterEntityDecoder.java b/spring-web/src/main/java/org/springframework/web/util/HtmlCharacterEntityDecoder.java index bf6c7952641d..615ff14c571b 100644 --- a/spring-web/src/main/java/org/springframework/web/util/HtmlCharacterEntityDecoder.java +++ b/spring-web/src/main/java/org/springframework/web/util/HtmlCharacterEntityDecoder.java @@ -124,7 +124,10 @@ private boolean processNumberedReference() { int value = (!isHexNumberedReference ? Integer.parseInt(getReferenceSubstring(2)) : Integer.parseInt(getReferenceSubstring(3), 16)); - this.decodedMessage.append((char) value); + if (value > Character.MAX_CODE_POINT) { + return false; + } + this.decodedMessage.appendCodePoint(value); return true; } catch (NumberFormatException ex) { diff --git a/spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTest.java b/spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTest.java new file mode 100644 index 000000000000..88355eadade2 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTest.java @@ -0,0 +1,42 @@ +package org.springframework.web.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class HtmlCharacterEntityDecoderTest { + + @Test + @DisplayName("Should correctly unescape Unicode supplementary characters") + void unescapeHandlesSupplementaryCharactersCorrectly() { + // Arrange: Prepare test cases with the 'grinning face' emoji (😀, U+1F600). + String expectedCharacter = "😀"; + String decimalEntity = "😀"; + String hexEntity = "😀"; + + // Act: Call the HtmlUtils.htmlUnescape method to get the actual results. + String actualResultFromDecimal = HtmlUtils.htmlUnescape(decimalEntity); + String actualResultFromHex = HtmlUtils.htmlUnescape(hexEntity); + + // Assert: Verify that the actual results match the expected character. + assertEquals(expectedCharacter, actualResultFromDecimal, "Decimal entity was not converted correctly."); + assertEquals(expectedCharacter, actualResultFromHex, "Hexadecimal entity was not converted correctly."); + } + + @Test + @DisplayName("Should correctly unescape basic and named HTML entities") + void unescapeHandlesBasicEntities() { + // Arrange + String input = "<p>Tom & Jerry's "Show"</p>"; + String expectedOutput = "

Tom & Jerry's \"Show\"

"; + + // Act + String actualOutput = HtmlUtils.htmlUnescape(input); + + // Assert + assertEquals(expectedOutput, actualOutput, "Basic HTML entities were not unescaped correctly."); + } + +} + From 030dace2be40239f875af9506706ddb30fb29ccc Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 17 Nov 2025 14:27:24 +0100 Subject: [PATCH 006/235] Polishing contribution Closes gh-35477 --- .../util/HtmlCharacterEntityDecoderTest.java | 42 --------------- .../util/HtmlCharacterEntityDecoderTests.java | 53 +++++++++++++++++++ 2 files changed, 53 insertions(+), 42 deletions(-) delete mode 100644 spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTest.java create mode 100644 spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTests.java diff --git a/spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTest.java b/spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTest.java deleted file mode 100644 index 88355eadade2..000000000000 --- a/spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.springframework.web.util; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class HtmlCharacterEntityDecoderTest { - - @Test - @DisplayName("Should correctly unescape Unicode supplementary characters") - void unescapeHandlesSupplementaryCharactersCorrectly() { - // Arrange: Prepare test cases with the 'grinning face' emoji (😀, U+1F600). - String expectedCharacter = "😀"; - String decimalEntity = "😀"; - String hexEntity = "😀"; - - // Act: Call the HtmlUtils.htmlUnescape method to get the actual results. - String actualResultFromDecimal = HtmlUtils.htmlUnescape(decimalEntity); - String actualResultFromHex = HtmlUtils.htmlUnescape(hexEntity); - - // Assert: Verify that the actual results match the expected character. - assertEquals(expectedCharacter, actualResultFromDecimal, "Decimal entity was not converted correctly."); - assertEquals(expectedCharacter, actualResultFromHex, "Hexadecimal entity was not converted correctly."); - } - - @Test - @DisplayName("Should correctly unescape basic and named HTML entities") - void unescapeHandlesBasicEntities() { - // Arrange - String input = "<p>Tom & Jerry's "Show"</p>"; - String expectedOutput = "

Tom & Jerry's \"Show\"

"; - - // Act - String actualOutput = HtmlUtils.htmlUnescape(input); - - // Assert - assertEquals(expectedOutput, actualOutput, "Basic HTML entities were not unescaped correctly."); - } - -} - diff --git a/spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTests.java b/spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTests.java new file mode 100644 index 000000000000..8aa45a08a668 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-present 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.web.util; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Tests for {@link HtmlCharacterEntityDecoder}. + */ +class HtmlCharacterEntityDecoderTests { + + @Test + void unescapeHandlesSupplementaryCharactersAsDecimal() { + String expectedCharacter = "😀"; + String decimalEntity = "😀"; + String actualResultFromDecimal = HtmlUtils.htmlUnescape(decimalEntity); + assertThat(actualResultFromDecimal).as("Decimal entity was not converted correctly.").isEqualTo(expectedCharacter); + } + + @Test + void unescapeHandlesSupplementaryCharactersAsHexadecimal() { + String expectedCharacter = "😀"; + String hexEntity = "😀"; + String actualResultFromHex = HtmlUtils.htmlUnescape(hexEntity); + assertThat(actualResultFromHex).as("Hexadecimal entity was not converted correctly.").isEqualTo(expectedCharacter); + } + + @Test + void unescapeHandlesBasicEntities() { + String input = "<p>Tom & Jerry's "Show"</p>"; + String expectedOutput = "

Tom & Jerry's \"Show\"

"; + String actualOutput = HtmlUtils.htmlUnescape(input); + assertThat(actualOutput).as("Basic HTML entities were not unescaped correctly.").isEqualTo(expectedOutput); + } + +} From 8553f97df1cffcfef26e75a3aede15487a8ad0f1 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 17 Nov 2025 15:27:51 +0100 Subject: [PATCH 007/235] Merge HtmlCharacterEntityDecoderTests into HtmlUtilsTests See gh-35711 (cherry picked from commit 0342cd0904d4889694bd2c3e1127a720bf79b9ac) --- .../util/HtmlCharacterEntityDecoderTests.java | 53 ------------------ .../web/util/HtmlUtilsTests.java | 55 ++++++++++++++----- 2 files changed, 40 insertions(+), 68 deletions(-) delete mode 100644 spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTests.java diff --git a/spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTests.java b/spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTests.java deleted file mode 100644 index 8aa45a08a668..000000000000 --- a/spring-web/src/test/java/org/springframework/web/util/HtmlCharacterEntityDecoderTests.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2002-present 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.web.util; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - - -/** - * Tests for {@link HtmlCharacterEntityDecoder}. - */ -class HtmlCharacterEntityDecoderTests { - - @Test - void unescapeHandlesSupplementaryCharactersAsDecimal() { - String expectedCharacter = "😀"; - String decimalEntity = "😀"; - String actualResultFromDecimal = HtmlUtils.htmlUnescape(decimalEntity); - assertThat(actualResultFromDecimal).as("Decimal entity was not converted correctly.").isEqualTo(expectedCharacter); - } - - @Test - void unescapeHandlesSupplementaryCharactersAsHexadecimal() { - String expectedCharacter = "😀"; - String hexEntity = "😀"; - String actualResultFromHex = HtmlUtils.htmlUnescape(hexEntity); - assertThat(actualResultFromHex).as("Hexadecimal entity was not converted correctly.").isEqualTo(expectedCharacter); - } - - @Test - void unescapeHandlesBasicEntities() { - String input = "<p>Tom & Jerry's "Show"</p>"; - String expectedOutput = "

Tom & Jerry's \"Show\"

"; - String actualOutput = HtmlUtils.htmlUnescape(input); - assertThat(actualOutput).as("Basic HTML entities were not unescaped correctly.").isEqualTo(expectedOutput); - } - -} diff --git a/spring-web/src/test/java/org/springframework/web/util/HtmlUtilsTests.java b/spring-web/src/test/java/org/springframework/web/util/HtmlUtilsTests.java index 0c0e03fe0656..c9651d236fb8 100644 --- a/spring-web/src/test/java/org/springframework/web/util/HtmlUtilsTests.java +++ b/spring-web/src/test/java/org/springframework/web/util/HtmlUtilsTests.java @@ -21,6 +21,8 @@ import static org.assertj.core.api.Assertions.assertThat; /** + * Tests for {@link HtmlUtils}. + * * @author Alef Arendsen * @author Martin Kersten * @author Rick Evans @@ -28,7 +30,7 @@ class HtmlUtilsTests { @Test - void testHtmlEscape() { + void htmlEscape() { String unescaped = "\"This is a quote'"; String escaped = HtmlUtils.htmlEscape(unescaped); assertThat(escaped).isEqualTo(""This is a quote'"); @@ -39,14 +41,7 @@ void testHtmlEscape() { } @Test - void testHtmlUnescape() { - String escaped = ""This is a quote'"; - String unescaped = HtmlUtils.htmlUnescape(escaped); - assertThat(unescaped).isEqualTo("\"This is a quote'"); - } - - @Test - void testEncodeIntoHtmlCharacterSet() { + void htmlEscapeIntoHtmlCharacterSet() { assertThat(HtmlUtils.htmlEscape("")).as("An empty string should be converted to an empty string").isEmpty(); assertThat(HtmlUtils.htmlEscape("A sentence containing no special characters.")).as("A string containing no special characters should not be affected").isEqualTo("A sentence containing no special characters."); @@ -60,12 +55,11 @@ void testEncodeIntoHtmlCharacterSet() { assertThat(HtmlUtils.htmlEscapeDecimal("" + (char) 977)).as("The special character 977 should be encoded to 'ϑ'").isEqualTo("ϑ"); } - // SPR-9293 - @Test - void testEncodeIntoHtmlCharacterSetFromUtf8() { + @Test // SPR-9293 + void htmlEscapeIntoHtmlCharacterSetFromUtf8() { String utf8 = ("UTF-8"); - assertThat(HtmlUtils.htmlEscape("", utf8)).as("An empty string should be converted to an empty string") - .isEmpty(); + + assertThat(HtmlUtils.htmlEscape("", utf8)).as("An empty string should be converted to an empty string").isEmpty(); assertThat(HtmlUtils.htmlEscape("A sentence containing no special characters.")).as("A string containing no special characters should not be affected").isEqualTo("A sentence containing no special characters."); assertThat(HtmlUtils.htmlEscape("< >", utf8)).as("'< >' should be encoded to '< >'").isEqualTo("< >"); @@ -75,7 +69,38 @@ void testEncodeIntoHtmlCharacterSetFromUtf8() { } @Test - void testDecodeFromHtmlCharacterSet() { + void htmlUnescape() { + String escaped = ""This is a quote'"; + String unescaped = HtmlUtils.htmlUnescape(escaped); + assertThat(unescaped).isEqualTo("\"This is a quote'"); + } + + @Test + void htmlUnescapeHandlesSupplementaryCharactersAsDecimal() { + String expectedCharacter = "😀"; + String decimalEntity = "😀"; + String actualResultFromDecimal = HtmlUtils.htmlUnescape(decimalEntity); + assertThat(actualResultFromDecimal).as("Decimal entity was not converted correctly.").isEqualTo(expectedCharacter); + } + + @Test + void htmlUnescapeHandlesSupplementaryCharactersAsHexadecimal() { + String expectedCharacter = "😀"; + String hexEntity = "😀"; + String actualResultFromHex = HtmlUtils.htmlUnescape(hexEntity); + assertThat(actualResultFromHex).as("Hexadecimal entity was not converted correctly.").isEqualTo(expectedCharacter); + } + + @Test + void htmlUnescapeHandlesBasicEntities() { + String input = "<p>Tom & Jerry's "Show"</p>"; + String expectedOutput = "

Tom & Jerry's \"Show\"

"; + String actualOutput = HtmlUtils.htmlUnescape(input); + assertThat(actualOutput).as("Basic HTML entities were not unescaped correctly.").isEqualTo(expectedOutput); + } + + @Test + void htmlUnescapeFromHtmlCharacterSet() { assertThat(HtmlUtils.htmlUnescape("")).as("An empty string should be converted to an empty string").isEmpty(); assertThat(HtmlUtils.htmlUnescape("This is a sentence containing no special characters.")).as("A string containing no special characters should not be affected").isEqualTo("This is a sentence containing no special characters."); From f94645de17e11d7e15b18e4080a29fb97016eb0d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 18 Nov 2025 13:36:29 +0100 Subject: [PATCH 008/235] Narrow Aware interface exclusion check to BeanFactoryAware only Closes gh-35835 (cherry picked from commit de5b9aab55f40365212949883f58d08bc6ed58a3) --- ...AbstractFallbackJCacheOperationSource.java | 6 ++-- .../AbstractFallbackCacheOperationSource.java | 6 ++-- ...actFallbackTransactionAttributeSource.java | 6 ++-- ...tationTransactionAttributeSourceTests.java | 28 +++++++++++++++---- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java index ed65ed263fd3..aa9d5997c4bd 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java @@ -25,7 +25,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.support.AopUtils; -import org.springframework.beans.factory.Aware; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.core.MethodClassKey; import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; @@ -98,8 +98,8 @@ private JCacheOperation computeCacheOperation(Method method, @Nullable Class< if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } - // Skip methods declared on BeanFactoryAware and co. - if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) { + // Skip setBeanFactory method on BeanFactoryAware. + if (method.getDeclaringClass() == BeanFactoryAware.class) { return null; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java index 548b9c15d56e..ec3384a1813a 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java @@ -27,7 +27,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.support.AopUtils; -import org.springframework.beans.factory.Aware; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.core.MethodClassKey; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; @@ -140,8 +140,8 @@ private Collection computeCacheOperations(Method method, @Nullab if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } - // Skip methods declared on BeanFactoryAware and co. - if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) { + // Skip setBeanFactory method on BeanFactoryAware. + if (method.getDeclaringClass() == BeanFactoryAware.class) { return null; } diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java index f5a60c2e33b0..3848f3b76e59 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/AbstractFallbackTransactionAttributeSource.java @@ -25,7 +25,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aop.support.AopUtils; -import org.springframework.beans.factory.Aware; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.core.MethodClassKey; import org.springframework.lang.Nullable; @@ -167,8 +167,8 @@ protected TransactionAttribute computeTransactionAttribute(Method method, @Nulla if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } - // Skip methods declared on BeanFactoryAware and co. - if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) { + // Skip setBeanFactory method on BeanFactoryAware. + if (method.getDeclaringClass() == BeanFactoryAware.class) { return null; } diff --git a/spring-tx/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSourceTests.java b/spring-tx/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSourceTests.java index a6a6f4de37b1..d428da79fc50 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSourceTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/annotation/AnnotationTransactionAttributeSourceTests.java @@ -32,6 +32,7 @@ import org.springframework.aop.framework.Advised; import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanNameAware; import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.testfixture.io.SerializationTestUtils; @@ -59,6 +60,7 @@ class AnnotationTransactionAttributeSourceTests { private final AnnotationTransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(); + @Test void serializable() throws Exception { TestBean1 tb = new TestBean1(); @@ -123,6 +125,10 @@ void transactionAttributeDeclaredOnCglibClassMethod() { void transactionAttributeDeclaredOnInterfaceMethodOnly() { TransactionAttribute actual = getTransactionAttribute(TestBean2.class, ITestBean2.class, "getAge"); assertThat(actual).satisfies(hasNoRollbackRule()); + actual = getTransactionAttribute(TestBean2.class, ITestBean2X.class, "getAge"); + assertThat(actual).satisfies(hasNoRollbackRule()); + actual = getTransactionAttribute(ITestBean2X.class, ITestBean2X.class, "getAge"); + assertThat(actual).satisfies(hasNoRollbackRule()); } /** @@ -249,6 +255,7 @@ void customMethodAttributeWithReadOnlyOverrideOnInterface() { assertThat(actual.isReadOnly()).isTrue(); } + @Nested class JtaAttributeTests { @@ -276,6 +283,7 @@ void transactionAttributeDeclaredOnInterface() { assertThat(getNameAttr.getPropagationBehavior()).isEqualTo(TransactionAttribute.PROPAGATION_SUPPORTS); } + static class JtaAnnotatedBean1 implements ITestBean1 { private String name; @@ -305,7 +313,6 @@ public void setAge(int age) { } } - @jakarta.transaction.Transactional(jakarta.transaction.Transactional.TxType.SUPPORTS) static class JtaAnnotatedBean2 implements ITestBean1 { @@ -362,7 +369,6 @@ public void setAge(int age) { } } - @jakarta.transaction.Transactional(jakarta.transaction.Transactional.TxType.SUPPORTS) interface ITestJta { @@ -375,9 +381,9 @@ interface ITestJta { void setName(String name); } - } + @Nested class Ejb3AttributeTests { @@ -448,7 +454,6 @@ public void setAge(int age) { } } - @jakarta.ejb.TransactionAttribute(TransactionAttributeType.SUPPORTS) static class Ejb3AnnotatedBean2 implements ITestBean1 { @@ -506,6 +511,7 @@ public void setAge(int age) { } } + @Nested class GroovyTests { @@ -519,6 +525,7 @@ void transactionAttributeDeclaredOnGroovyClass() { assertThat(attributeSource.getTransactionAttribute(getMetaClassMethod, GroovyTestBean.class)).isNull(); } + @Transactional static class GroovyTestBean implements ITestBean1, GroovyObject { @@ -571,6 +578,7 @@ public void setMetaClass(MetaClass metaClass) { } } + private Consumer hasRollbackRules(RollbackRuleAttribute... rollbackRuleAttributes) { return transactionAttribute -> { RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); @@ -626,7 +634,12 @@ interface ITestBean2 { } - interface ITestBean2X extends ITestBean2 { + interface ITestBean2X extends ITestBean2, BeanNameAware { + + @Transactional + int getAge(); + + void setAge(int age); String getName(); @@ -735,6 +748,10 @@ public TestBean2(String name, int age) { this.age = age; } + @Override + public void setBeanName(String name) { + } + @Override public String getName() { return name; @@ -917,6 +934,7 @@ public int getAge() { } } + @Transactional(label = {"retryable", "long-running"}) static class TestBean11 { From 8545a759a7ff0cfa3ee1c1ed4c964bc8e2be1c27 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 18 Nov 2025 13:37:14 +0100 Subject: [PATCH 009/235] Add resetCaches() method to Caffeine/ConcurrentMapCacheManager Closes gh-35840 (cherry picked from commit bc3431f435684dfa1b900a9f64e34c2c41f8ac68) --- .../cache/caffeine/CaffeineCacheManager.java | 57 ++++++++++++------- .../caffeine/CaffeineCacheManagerTests.java | 24 +++++++- .../concurrent/ConcurrentMapCacheManager.java | 37 ++++++++---- .../ConcurrentMapCacheManagerTests.java | 24 +++++++- 4 files changed, 108 insertions(+), 34 deletions(-) diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java index 9087df30efa3..777f82cdb25c 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java @@ -77,7 +77,7 @@ public class CaffeineCacheManager implements CacheManager { private boolean allowNullValues = true; - private boolean dynamic = true; + private volatile boolean dynamic = true; private final Map cacheMap = new ConcurrentHashMap<>(16); @@ -102,10 +102,15 @@ public CaffeineCacheManager(String... cacheNames) { /** * Specify the set of cache names for this CacheManager's 'static' mode. - *

The number of caches and their names will be fixed after a call to this method, - * with no creation of further cache regions at runtime. - *

Calling this with a {@code null} collection argument resets the - * mode to 'dynamic', allowing for further creation of caches again. + *

The number of caches and their names will be fixed after a call + * to this method, with no creation of further cache regions at runtime. + *

Note that this method replaces existing caches of the given names + * and prevents the creation of further cache regions from here on - but + * does not remove unrelated existing caches. For a full reset, + * consider calling {@link #resetCaches()} before calling this method. + *

Calling this method with a {@code null} collection argument resets + * the mode to 'dynamic', allowing for further creation of caches again. + * @see #resetCaches() */ public void setCacheNames(@Nullable Collection cacheNames) { if (cacheNames != null) { @@ -245,11 +250,6 @@ public boolean isAllowNullValues() { } - @Override - public Collection getCacheNames() { - return Collections.unmodifiableSet(this.cacheMap.keySet()); - } - @Override @Nullable public Cache getCache(String name) { @@ -260,6 +260,33 @@ public Cache getCache(String name) { return cache; } + @Override + public Collection getCacheNames() { + return Collections.unmodifiableSet(this.cacheMap.keySet()); + } + + /** + * Reset this cache manager's caches, removing them completely for on-demand + * re-creation in 'dynamic' mode, or simply clearing their entries otherwise. + * @since 6.2.14 + */ + public void resetCaches() { + this.cacheMap.values().forEach(Cache::clear); + if (this.dynamic) { + this.cacheMap.keySet().retainAll(this.customCacheNames); + } + } + + /** + * Remove the specified cache from this cache manager, applying to + * custom caches as well as dynamically registered caches at runtime. + * @param name the name of the cache + * @since 6.1.15 + */ + public void removeCache(String name) { + this.customCacheNames.remove(name); + this.cacheMap.remove(name); + } /** * Register the given native Caffeine Cache instance with this cache manager, @@ -303,16 +330,6 @@ public void registerCustomCache(String name, AsyncCache cache) { this.cacheMap.put(name, adaptCaffeineCache(name, cache)); } - /** - * Remove the specified cache from this cache manager, applying to - * custom caches as well as dynamically registered caches at runtime. - * @param name the name of the cache - * @since 6.1.15 - */ - public void removeCache(String name) { - this.customCacheNames.remove(name); - this.cacheMap.remove(name); - } /** * Adapt the given new native Caffeine Cache instance to Spring's {@link Cache} diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java index 1f4bb58c5239..fdb62edef28f 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java @@ -24,7 +24,6 @@ import org.junit.jupiter.api.Test; import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; import org.springframework.cache.support.SimpleValueWrapper; import static org.assertj.core.api.Assertions.assertThat; @@ -42,7 +41,7 @@ class CaffeineCacheManagerTests { @Test @SuppressWarnings("cast") void dynamicMode() { - CacheManager cm = new CaffeineCacheManager(); + CaffeineCacheManager cm = new CaffeineCacheManager(); Cache cache1 = cm.getCache("c1"); assertThat(cache1).isInstanceOf(CaffeineCache.class); @@ -76,6 +75,14 @@ void dynamicMode() { cache1.evict("key3"); assertThat(cache1.get("key3", () -> (String) null)).isNull(); assertThat(cache1.get("key3", () -> (String) null)).isNull(); + + cm.removeCache("c1"); + assertThat(cm.getCache("c1")).isNotSameAs(cache1); + assertThat(cm.getCache("c2")).isSameAs(cache2); + + cm.resetCaches(); + assertThat(cm.getCache("c1")).isNotSameAs(cache1); + assertThat(cm.getCache("c2")).isNotSameAs(cache2); } @Test @@ -131,11 +138,24 @@ void staticMode() { cm.setAllowNullValues(true); Cache cache1y = cm.getCache("c1"); + Cache cache2y = cm.getCache("c2"); cache1y.put("key3", null); assertThat(cache1y.get("key3").get()).isNull(); cache1y.evict("key3"); assertThat(cache1y.get("key3")).isNull(); + cache2y.put("key4", "value4"); + assertThat(cache2y.get("key4").get()).isEqualTo("value4"); + + cm.removeCache("c1"); + assertThat(cm.getCache("c1")).isNull(); + assertThat(cm.getCache("c2")).isSameAs(cache2y); + assertThat(cache2y.get("key4").get()).isEqualTo("value4"); + + cm.resetCaches(); + assertThat(cm.getCache("c1")).isNull(); + assertThat(cm.getCache("c2")).isSameAs(cache2y); + assertThat(cache2y.get("key4")).isNull(); } @Test diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java index 36176e35b019..7608d524c870 100644 --- a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java @@ -54,7 +54,7 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA private final ConcurrentMap cacheMap = new ConcurrentHashMap<>(16); - private boolean dynamic = true; + private volatile boolean dynamic = true; private boolean allowNullValues = true; @@ -82,10 +82,15 @@ public ConcurrentMapCacheManager(String... cacheNames) { /** * Specify the set of cache names for this CacheManager's 'static' mode. - *

The number of caches and their names will be fixed after a call to this method, - * with no creation of further cache regions at runtime. - *

Calling this with a {@code null} collection argument resets the - * mode to 'dynamic', allowing for further creation of caches again. + *

The number of caches and their names will be fixed after a call + * to this method, with no creation of further cache regions at runtime. + *

Note that this method replaces existing caches of the given names + * and prevents the creation of further cache regions from here on - but + * does not remove unrelated existing caches. For a full reset, + * consider calling {@link #resetCaches()} before calling this method. + *

Calling this method with a {@code null} collection argument resets + * the mode to 'dynamic', allowing for further creation of caches again. + * @see #resetCaches() */ public void setCacheNames(@Nullable Collection cacheNames) { if (cacheNames != null) { @@ -160,11 +165,6 @@ public void setBeanClassLoader(ClassLoader classLoader) { } - @Override - public Collection getCacheNames() { - return Collections.unmodifiableSet(this.cacheMap.keySet()); - } - @Override @Nullable public Cache getCache(String name) { @@ -175,6 +175,23 @@ public Cache getCache(String name) { return cache; } + @Override + public Collection getCacheNames() { + return Collections.unmodifiableSet(this.cacheMap.keySet()); + } + + /** + * Reset this cache manager's caches, removing them completely for on-demand + * re-creation in 'dynamic' mode, or simply clearing their entries otherwise. + * @since 6.2.14 + */ + public void resetCaches() { + this.cacheMap.values().forEach(Cache::clear); + if (this.dynamic) { + this.cacheMap.clear(); + } + } + /** * Remove the specified cache from this cache manager. * @param name the name of the cache diff --git a/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java index 0a366edcf1d1..247dc1e3b598 100644 --- a/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java +++ b/spring-context/src/test/java/org/springframework/cache/concurrent/ConcurrentMapCacheManagerTests.java @@ -19,7 +19,6 @@ import org.junit.jupiter.api.Test; import org.springframework.cache.Cache; -import org.springframework.cache.CacheManager; import static org.assertj.core.api.Assertions.assertThat; @@ -31,7 +30,7 @@ class ConcurrentMapCacheManagerTests { @Test void testDynamicMode() { - CacheManager cm = new ConcurrentMapCacheManager(); + ConcurrentMapCacheManager cm = new ConcurrentMapCacheManager(); Cache cache1 = cm.getCache("c1"); assertThat(cache1).isInstanceOf(ConcurrentMapCache.class); Cache cache1again = cm.getCache("c1"); @@ -65,6 +64,14 @@ void testDynamicMode() { assertThat(cache1.get("key3").get()).isNull(); cache1.evict("key3"); assertThat(cache1.get("key3")).isNull(); + + cm.removeCache("c1"); + assertThat(cm.getCache("c1")).isNotSameAs(cache1); + assertThat(cm.getCache("c2")).isSameAs(cache2); + + cm.resetCaches(); + assertThat(cm.getCache("c1")).isNotSameAs(cache1); + assertThat(cm.getCache("c2")).isNotSameAs(cache2); } @Test @@ -107,11 +114,24 @@ void testStaticMode() { cm.setAllowNullValues(true); Cache cache1y = cm.getCache("c1"); + Cache cache2y = cm.getCache("c2"); cache1y.put("key3", null); assertThat(cache1y.get("key3").get()).isNull(); cache1y.evict("key3"); assertThat(cache1y.get("key3")).isNull(); + cache2y.put("key4", "value4"); + assertThat(cache2y.get("key4").get()).isEqualTo("value4"); + + cm.removeCache("c1"); + assertThat(cm.getCache("c1")).isNull(); + assertThat(cm.getCache("c2")).isSameAs(cache2y); + assertThat(cache2y.get("key4").get()).isEqualTo("value4"); + + cm.resetCaches(); + assertThat(cm.getCache("c1")).isNull(); + assertThat(cm.getCache("c2")).isSameAs(cache2y); + assertThat(cache2y.get("key4")).isNull(); } @Test From e4288170c8433f2eb1b8e383d9a6fca0240c6685 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 18 Nov 2025 13:37:39 +0100 Subject: [PATCH 010/235] Fix getCacheNames() concurrent access in NoOpCacheManager Closes gh-35842 (cherry picked from commit 57a1d4007b82eb6da333d2bc3cd62612ec86f1df) --- .../cache/support/NoOpCacheManager.java | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java index e1b756da6cbd..935945eebabd 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java @@ -18,8 +18,6 @@ import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -32,45 +30,29 @@ * for disabling caching, typically used for backing cache declarations * without an actual backing store. * - *

Will simply accept any items into the cache not actually storing them. + *

This implementation will simply accept any items into the cache, + * not actually storing them. * * @author Costin Leau * @author Stephane Nicoll + * @author Juergen Hoeller * @since 3.1 * @see NoOpCache */ public class NoOpCacheManager implements CacheManager { - private final ConcurrentMap caches = new ConcurrentHashMap<>(16); + private final ConcurrentMap cacheMap = new ConcurrentHashMap<>(16); - private final Set cacheNames = new LinkedHashSet<>(16); - - /** - * This implementation always returns a {@link Cache} implementation that will not store items. - * Additionally, the request cache will be remembered by the manager for consistency. - */ @Override @Nullable public Cache getCache(String name) { - Cache cache = this.caches.get(name); - if (cache == null) { - this.caches.computeIfAbsent(name, NoOpCache::new); - synchronized (this.cacheNames) { - this.cacheNames.add(name); - } - } - return this.caches.get(name); + return this.cacheMap.computeIfAbsent(name, NoOpCache::new); } - /** - * This implementation returns the name of the caches previously requested. - */ @Override public Collection getCacheNames() { - synchronized (this.cacheNames) { - return Collections.unmodifiableSet(this.cacheNames); - } + return Collections.unmodifiableSet(this.cacheMap.keySet()); } } From bd10b7ae0654d9e148c9c46ee27f5d9262ae6fd7 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 19 Nov 2025 16:18:56 +0100 Subject: [PATCH 011/235] Remove javadoc references to deprecated PropertiesBeanDefinitionReader Closes gh-35836 (cherry picked from commit 35b8fbf90110d4705a9661cf64caa734592dc5c0) --- .../beans/factory/support/BeanDefinitionReaderUtils.java | 1 - .../beans/factory/support/BeanDefinitionRegistry.java | 1 - .../factory/support/PropertiesBeanDefinitionReader.java | 6 +++--- .../support/AbstractRefreshableApplicationContext.java | 1 - .../context/support/GenericApplicationContext.java | 3 --- .../jdbc/core/support/JdbcBeanDefinitionReader.java | 7 +++---- 6 files changed, 6 insertions(+), 13 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java index c183733e5739..a7e0ad507c19 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java @@ -32,7 +32,6 @@ * @author Juergen Hoeller * @author Rob Harrop * @since 1.1 - * @see PropertiesBeanDefinitionReader * @see org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader */ public abstract class BeanDefinitionReaderUtils { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionRegistry.java index b97d1fa7329c..4381cf7e61a8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionRegistry.java @@ -43,7 +43,6 @@ * @see DefaultListableBeanFactory * @see org.springframework.context.support.GenericApplicationContext * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader - * @see PropertiesBeanDefinitionReader */ public interface BeanDefinitionRegistry extends AliasRegistry { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java index db4ea48a25d7..f1afba74fc5a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/PropertiesBeanDefinitionReader.java @@ -74,10 +74,10 @@ * @author Rob Harrop * @since 26.11.2003 * @see DefaultListableBeanFactory - * @deprecated as of 5.3, in favor of Spring's common bean definition formats - * and/or custom reader implementations + * @deprecated in favor of Spring's common bean definition formats and/or + * custom BeanDefinitionReader implementations */ -@Deprecated +@Deprecated(since = "5.3") public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader { /** diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java index 249f01147134..1328c49726c9 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java @@ -227,7 +227,6 @@ protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) { * @param beanFactory the bean factory to load bean definitions into * @throws BeansException if parsing of the bean definitions failed * @throws IOException if loading of bean definition files failed - * @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader */ protected abstract void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java index e529204716e7..2d60ea29b000 100644 --- a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java @@ -78,8 +78,6 @@ * GenericApplicationContext ctx = new GenericApplicationContext(); * XmlBeanDefinitionReader xmlReader = new XmlBeanDefinitionReader(ctx); * xmlReader.loadBeanDefinitions(new ClassPathResource("applicationContext.xml")); - * PropertiesBeanDefinitionReader propReader = new PropertiesBeanDefinitionReader(ctx); - * propReader.loadBeanDefinitions(new ClassPathResource("otherBeans.properties")); * ctx.refresh(); * * MyBean myBean = (MyBean) ctx.getBean("myBean"); @@ -101,7 +99,6 @@ * @see #registerBeanDefinition * @see #refresh() * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader - * @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader */ public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java index 8ccbe5810443..bcd1e80770ed 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcBeanDefinitionReader.java @@ -40,11 +40,10 @@ * @author Rod Johnson * @author Juergen Hoeller * @see #loadBeanDefinitions - * @see org.springframework.beans.factory.support.PropertiesBeanDefinitionReader - * @deprecated as of 5.3, in favor of Spring's common bean definition formats - * and/or custom reader implementations + * @deprecated in favor of Spring's common bean definition formats and/or + * custom BeanDefinitionReader implementations */ -@Deprecated +@Deprecated(since = "5.3") public class JdbcBeanDefinitionReader { private final org.springframework.beans.factory.support.PropertiesBeanDefinitionReader propReader; From 4e970135957bbebcc7193818c4867cc96da78f61 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 19 Nov 2025 16:19:14 +0100 Subject: [PATCH 012/235] Polishing (cherry picked from commit f456674529a24bc0f9585855874c97f84fad2232) --- .../org/springframework/util/ConcurrentReferenceHashMap.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java index 563ea1529e5e..435dfa705f9e 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -1048,6 +1048,7 @@ public Spliterator> spliterator() { } } + /** * Internal key-set implementation. */ @@ -1084,6 +1085,7 @@ public Spliterator spliterator() { } } + /** * Internal key iterator implementation. */ @@ -1107,6 +1109,7 @@ public K next() { } } + /** * Internal values collection implementation. */ @@ -1143,6 +1146,7 @@ public Spliterator spliterator() { } } + /** * Internal value iterator implementation. */ @@ -1166,6 +1170,7 @@ public V next() { } } + /** * Internal entry iterator implementation. */ From ff62e7355f1508fd78ea065b6fa8c54d29ed7213 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Wed, 19 Nov 2025 22:04:06 +0700 Subject: [PATCH 013/235] Fix cross-reference links in HtmlUnit sections Closes gh-35853 Signed-off-by: Tran Ngoc Nhan (cherry picked from commit 91d2a51f3f3d766db1e9e7e0d0141a970c7b1e15) --- .../modules/ROOT/pages/testing/mockmvc/htmlunit/geb.adoc | 6 +++--- .../modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc | 6 +++--- .../ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc | 7 +++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/geb.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/geb.adoc index ad4ece55138b..d25894625c31 100644 --- a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/geb.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/geb.adoc @@ -8,7 +8,7 @@ use https://www.gebish.org/[Geb] to make our tests even Groovy-er. == Why Geb and MockMvc? Geb is backed by WebDriver, so it offers many of the -xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[same benefits] that we get from +xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[same benefits] that we get from WebDriver. However, Geb makes things even easier by taking care of some of the boilerplate code for us. @@ -28,7 +28,7 @@ def setup() { ---- NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced -usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`]. +usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`]. This ensures that any URL referencing `localhost` as the server is directed to our `MockMvc` instance without the need for a real HTTP connection. Any other URL is @@ -62,7 +62,7 @@ forwarded to the current page object. This removes a lot of the boilerplate code needed when using WebDriver directly. As with direct WebDriver usage, this improves on the design of our -xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test] by using the Page Object +xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test] by using the Page Object Pattern. As mentioned previously, we can use the Page Object Pattern with HtmlUnit and WebDriver, but it is even easier with Geb. Consider our new Groovy-based `CreateMessagePage` implementation: diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc index 97dd4171b956..59bf2cfb35b4 100644 --- a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc @@ -45,7 +45,7 @@ Kotlin:: ====== NOTE: This is a simple example of using `MockMvcWebClientBuilder`. For advanced usage, -see xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`]. +see <>. This ensures that any URL that references `localhost` as the server is directed to our `MockMvc` instance without the need for a real HTTP connection. Any other URL is @@ -77,7 +77,7 @@ Kotlin:: ====== NOTE: The default context path is `""`. Alternatively, we can specify the context path, -as described in xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`]. +as described in <>. Once we have a reference to the `HtmlPage`, we can then fill out the form and submit it to create a message, as the following example shows: @@ -144,7 +144,7 @@ Kotlin:: ====== The preceding code improves on our -xref:testing/mockmvc/htmlunit/why.adoc#spring-mvc-test-server-htmlunit-mock-mvc-test[MockMvc test] in a number of ways. +xref:testing/mockmvc/htmlunit/why.adoc#mockmvc-server-htmlunit-mock-mvc-test[MockMvc test] in a number of ways. First, we no longer have to explicitly verify our form and then create a request that looks like the form. Instead, we request the form, fill it out, and submit it, thereby significantly reducing the overhead. diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc index a9e3533cac50..14dfaabc01b5 100644 --- a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc @@ -203,7 +203,7 @@ Kotlin:: ====== NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced -usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`]. +usage, see <>. The preceding example ensures that any URL that references `localhost` as the server is directed to our `MockMvc` instance without the need for a real HTTP connection. Any other @@ -259,9 +259,8 @@ Kotlin:: ====== -- -This improves on the design of our xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test] -by leveraging the Page Object Pattern. As we mentioned in -xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[Why WebDriver and MockMvc?], we can use the Page Object Pattern +This improves on the design of our xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test] +by leveraging the Page Object Pattern. As we mentioned in <>, we can use the Page Object Pattern with HtmlUnit, but it is much easier with WebDriver. Consider the following `CreateMessagePage` implementation: From e146e809e5b2d29238f4de1ee70fa73ffa583952 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:05:32 +0100 Subject: [PATCH 014/235] Polishing (cherry picked from commit d178930186c630d4df858cf87b098b63d69084c8) --- .../pages/testing/mockmvc/htmlunit/geb.adoc | 17 +++++++++-------- .../pages/testing/mockmvc/htmlunit/mah.adoc | 3 +-- .../testing/mockmvc/htmlunit/webdriver.adoc | 8 +++++--- .../pages/testing/mockmvc/htmlunit/why.adoc | 6 +++--- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/geb.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/geb.adoc index d25894625c31..c92d0cc2dde3 100644 --- a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/geb.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/geb.adoc @@ -8,9 +8,9 @@ use https://www.gebish.org/[Geb] to make our tests even Groovy-er. == Why Geb and MockMvc? Geb is backed by WebDriver, so it offers many of the -xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[same benefits] that we get from -WebDriver. However, Geb makes things even easier by taking care of some of the -boilerplate code for us. +xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[same benefits] +that we get from WebDriver. However, Geb makes things even easier by taking care of some +of the boilerplate code for us. [[mockmvc-server-htmlunit-geb-setup]] == MockMvc and Geb Setup @@ -28,7 +28,8 @@ def setup() { ---- NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced -usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`]. +usage, see +xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`]. This ensures that any URL referencing `localhost` as the server is directed to our `MockMvc` instance without the need for a real HTTP connection. Any other URL is @@ -62,10 +63,10 @@ forwarded to the current page object. This removes a lot of the boilerplate code needed when using WebDriver directly. As with direct WebDriver usage, this improves on the design of our -xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test] by using the Page Object -Pattern. As mentioned previously, we can use the Page Object Pattern with HtmlUnit and -WebDriver, but it is even easier with Geb. Consider our new Groovy-based -`CreateMessagePage` implementation: +xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test] +by using the Page Object Pattern. As mentioned previously, we can use the Page Object +Pattern with HtmlUnit and WebDriver, but it is even easier with Geb. Consider our new +Groovy-based `CreateMessagePage` implementation: [source,groovy] ---- diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc index 59bf2cfb35b4..d5dc763e520e 100644 --- a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc @@ -7,8 +7,7 @@ to use the raw HtmlUnit libraries. [[mockmvc-server-htmlunit-mah-setup]] == MockMvc and HtmlUnit Setup -First, make sure that you have included a test dependency on -`org.htmlunit:htmlunit`. +First, make sure that you have included a test dependency on `org.htmlunit:htmlunit`. We can easily create an HtmlUnit `WebClient` that integrates with MockMvc by using the `MockMvcWebClientBuilder`, as follows: diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc index 14dfaabc01b5..d94c0dd55ed4 100644 --- a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/webdriver.adoc @@ -259,9 +259,11 @@ Kotlin:: ====== -- -This improves on the design of our xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test] -by leveraging the Page Object Pattern. As we mentioned in <>, we can use the Page Object Pattern -with HtmlUnit, but it is much easier with WebDriver. Consider the following +This improves on the design of our +xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test] +by leveraging the Page Object Pattern. As we mentioned in +<>, we can use the Page Object Pattern with +HtmlUnit, but it is much easier with WebDriver. Consider the following `CreateMessagePage` implementation: -- diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/why.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/why.adoc index 710a310f1d5d..9c24ae54bfb2 100644 --- a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/why.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/why.adoc @@ -60,7 +60,7 @@ assume our form looks like the following snippet: ---- -How do we ensure that our form produce the correct request to create a new message? A +How do we ensure that our form produces the correct request to create a new message? A naive attempt might resemble the following: [tabs] @@ -154,7 +154,7 @@ validation. [[mockmvc-server-htmlunit-why-integration]] == Integration Testing to the Rescue? -To resolve the issues mentioned earlier, we could perform end-to-end integration testing, +To resolve the issues mentioned above, we could perform end-to-end integration testing, but this has some drawbacks. Consider testing the view that lets us page through the messages. We might need the following tests: @@ -171,7 +171,7 @@ leads to a number of additional challenges: * Testing can become slow, since each test would need to ensure that the database is in the correct state. * Since our database needs to be in a specific state, we cannot run tests in parallel. -* Performing assertions on such items as auto-generated IDs, timestamps, and others can +* Performing assertions on items such as auto-generated IDs, timestamps, and others can be difficult. These challenges do not mean that we should abandon end-to-end integration testing From e5de8b9bc68d837c9c5b77a5698273dc3f4e000d Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 19 Nov 2025 17:06:05 +0100 Subject: [PATCH 015/235] Fix link to MockMvc test in HtmlUnit section See gh-35853 (cherry picked from commit 9fe4e7798d77123119efa4cb502d611d078f049f) --- .../modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc index d5dc763e520e..060c015aa9e7 100644 --- a/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc +++ b/framework-docs/modules/ROOT/pages/testing/mockmvc/htmlunit/mah.adoc @@ -143,10 +143,10 @@ Kotlin:: ====== The preceding code improves on our -xref:testing/mockmvc/htmlunit/why.adoc#mockmvc-server-htmlunit-mock-mvc-test[MockMvc test] in a number of ways. -First, we no longer have to explicitly verify our form and then create a request that -looks like the form. Instead, we request the form, fill it out, and submit it, thereby -significantly reducing the overhead. +xref:testing/mockmvc/htmlunit/why.adoc#mockmvc-server-htmlunit-why[MockMvc test] in a +number of ways. First, we no longer have to explicitly verify our form and then create a +request that looks like the form. Instead, we request the form, fill it out, and submit +it, thereby significantly reducing the overhead. Another important factor is that https://htmlunit.sourceforge.io/javascript.html[HtmlUnit uses the Mozilla Rhino engine] to evaluate JavaScript. This means that we can also test From 59025c5b9624a38d9ef769781d8a09b465b9309e Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 19 Nov 2025 17:31:27 +0100 Subject: [PATCH 016/235] Lazily initialize ProblemDetail for picking up actual status code Closes gh-35829 (cherry picked from commit 3026f0a49badf4c235df9dd1c756e771c6b8f303) --- .../bind/ServletRequestBindingException.java | 14 ++++--- .../web/ErrorResponseExceptionTests.java | 40 ++++++------------- src/checkstyle/checkstyle-suppressions.xml | 1 + 3 files changed, 22 insertions(+), 33 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestBindingException.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestBindingException.java index f66ff06412a0..cd42052b6672 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestBindingException.java +++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestBindingException.java @@ -25,8 +25,8 @@ import org.springframework.web.ErrorResponse; /** - * Fatal binding exception, thrown when we want to - * treat binding exceptions as unrecoverable. + * Fatal binding exception, thrown when we want to treat binding exceptions + * as unrecoverable. * *

Extends ServletException for convenient throwing in any Servlet resource * (such as a Filter), and NestedServletException for proper root cause handling @@ -38,13 +38,14 @@ @SuppressWarnings("serial") public class ServletRequestBindingException extends ServletException implements ErrorResponse { - private final ProblemDetail body = ProblemDetail.forStatus(getStatusCode()); - private final String messageDetailCode; @Nullable private final Object[] messageDetailArguments; + @Nullable + private ProblemDetail body; + /** * Constructor with a message only. @@ -108,7 +109,10 @@ public HttpStatusCode getStatusCode() { } @Override - public ProblemDetail getBody() { + public synchronized ProblemDetail getBody() { + if (this.body == null) { + this.body = ProblemDetail.forStatus(getStatusCode()); + } return this.body; } diff --git a/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java b/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java index 177b8b4df1b9..f55667bfafcc 100644 --- a/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java +++ b/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java @@ -78,7 +78,6 @@ class ErrorResponseExceptionTests { @Test void httpMediaTypeNotSupportedException() { - List mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); @@ -96,7 +95,6 @@ void httpMediaTypeNotSupportedException() { @Test void httpMediaTypeNotSupportedExceptionWithParseError() { - ErrorResponse ex = new HttpMediaTypeNotSupportedException( "Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'"); @@ -109,7 +107,6 @@ void httpMediaTypeNotSupportedExceptionWithParseError() { @Test void httpMediaTypeNotAcceptableException() { - List mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); HttpMediaTypeNotAcceptableException ex = new HttpMediaTypeNotAcceptableException(mediaTypes); @@ -123,7 +120,6 @@ void httpMediaTypeNotAcceptableException() { @Test void httpMediaTypeNotAcceptableExceptionWithParseError() { - ErrorResponse ex = new HttpMediaTypeNotAcceptableException( "Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'"); @@ -136,7 +132,6 @@ void httpMediaTypeNotAcceptableExceptionWithParseError() { @Test void asyncRequestTimeoutException() { - ErrorResponse ex = new AsyncRequestTimeoutException(); assertDetailMessageCode(ex, null, null); @@ -148,7 +143,6 @@ void asyncRequestTimeoutException() { @Test void httpRequestMethodNotSupportedException() { - HttpRequestMethodNotSupportedException ex = new HttpRequestMethodNotSupportedException("PUT", Arrays.asList("GET", "POST")); @@ -162,7 +156,6 @@ void httpRequestMethodNotSupportedException() { @Test void missingRequestHeaderException() { - MissingRequestHeaderException ex = new MissingRequestHeaderException("Authorization", this.methodParameter); assertStatus(ex, HttpStatus.BAD_REQUEST); @@ -174,7 +167,6 @@ void missingRequestHeaderException() { @Test void missingServletRequestParameterException() { - MissingServletRequestParameterException ex = new MissingServletRequestParameterException("query", "String"); assertStatus(ex, HttpStatus.BAD_REQUEST); @@ -186,10 +178,8 @@ void missingServletRequestParameterException() { @Test void missingMatrixVariableException() { - MissingMatrixVariableException ex = new MissingMatrixVariableException("region", this.methodParameter); - assertStatus(ex, HttpStatus.BAD_REQUEST); assertDetail(ex, "Required path parameter 'region' is not present."); assertDetailMessageCode(ex, null, new Object[] {ex.getVariableName()}); @@ -199,7 +189,6 @@ void missingMatrixVariableException() { @Test void missingPathVariableException() { - MissingPathVariableException ex = new MissingPathVariableException("id", this.methodParameter); assertStatus(ex, HttpStatus.INTERNAL_SERVER_ERROR); @@ -210,8 +199,18 @@ void missingPathVariableException() { } @Test - void missingRequestCookieException() { + void missingPathVariableExceptionAfterConversion() { + MissingPathVariableException ex = new MissingPathVariableException("id", this.methodParameter, true); + + assertStatus(ex, HttpStatus.BAD_REQUEST); + assertDetail(ex, "Required path variable 'id' is not present."); + assertDetailMessageCode(ex, null, new Object[] {ex.getVariableName()}); + + assertThat(ex.getHeaders().isEmpty()).isTrue(); + } + @Test + void missingRequestCookieException() { MissingRequestCookieException ex = new MissingRequestCookieException("oreo", this.methodParameter); assertStatus(ex, HttpStatus.BAD_REQUEST); @@ -223,7 +222,6 @@ void missingRequestCookieException() { @Test void unsatisfiedServletRequestParameterException() { - UnsatisfiedServletRequestParameterException ex = new UnsatisfiedServletRequestParameterException( new String[] { "foo=bar", "bar=baz" }, Collections.singletonMap("q", new String[] {"1"})); @@ -236,7 +234,6 @@ void unsatisfiedServletRequestParameterException() { @Test void missingServletRequestPartException() { - MissingServletRequestPartException ex = new MissingServletRequestPartException("file"); assertStatus(ex, HttpStatus.BAD_REQUEST); @@ -248,7 +245,6 @@ void missingServletRequestPartException() { @Test void methodArgumentNotValidException() { - ValidationTestHelper testHelper = new ValidationTestHelper(MethodArgumentNotValidException.class); BindingResult result = testHelper.bindingResult(); @@ -280,7 +276,6 @@ void handlerMethodValidationException() { @Test void unsupportedMediaTypeStatusException() { - List mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); @@ -298,7 +293,6 @@ void unsupportedMediaTypeStatusException() { @Test void unsupportedMediaTypeStatusExceptionWithParseError() { - ErrorResponse ex = new UnsupportedMediaTypeStatusException( "Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'"); @@ -311,7 +305,6 @@ void unsupportedMediaTypeStatusExceptionWithParseError() { @Test void notAcceptableStatusException() { - List mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_CBOR); NotAcceptableStatusException ex = new NotAcceptableStatusException(mediaTypes); @@ -325,7 +318,6 @@ void notAcceptableStatusException() { @Test void notAcceptableStatusExceptionWithParseError() { - ErrorResponse ex = new NotAcceptableStatusException( "Could not parse Accept header: Invalid mime type \"foo\": does not contain '/'"); @@ -338,7 +330,6 @@ void notAcceptableStatusExceptionWithParseError() { @Test void serverErrorException() { - ServerErrorException ex = new ServerErrorException("Failure", null); assertStatus(ex, HttpStatus.INTERNAL_SERVER_ERROR); @@ -350,7 +341,6 @@ void serverErrorException() { @Test void missingRequestValueException() { - MissingRequestValueException ex = new MissingRequestValueException("foo", String.class, "header", this.methodParameter); @@ -363,7 +353,6 @@ void missingRequestValueException() { @Test void unsatisfiedRequestParameterException() { - UnsatisfiedRequestParameterException ex = new UnsatisfiedRequestParameterException( Arrays.asList("foo=bar", "bar=baz"), @@ -378,7 +367,6 @@ void unsatisfiedRequestParameterException() { @Test void webExchangeBindException() { - ValidationTestHelper testHelper = new ValidationTestHelper(WebExchangeBindException.class); BindingResult result = testHelper.bindingResult(); @@ -393,7 +381,6 @@ void webExchangeBindException() { @Test void methodNotAllowedException() { - List supportedMethods = Arrays.asList(HttpMethod.GET, HttpMethod.POST); MethodNotAllowedException ex = new MethodNotAllowedException(HttpMethod.PUT, supportedMethods); @@ -407,7 +394,6 @@ void methodNotAllowedException() { @Test void methodNotAllowedExceptionWithoutSupportedMethods() { - MethodNotAllowedException ex = new MethodNotAllowedException(HttpMethod.PUT, Collections.emptyList()); assertStatus(ex, HttpStatus.METHOD_NOT_ALLOWED); @@ -417,9 +403,8 @@ void methodNotAllowedExceptionWithoutSupportedMethods() { assertThat(ex.getHeaders()).isEmpty(); } - @Test // gh-30300 + @Test // gh-30300 void responseStatusException() { - Locale locale = Locale.UK; LocaleContextHolder.setLocale(locale); @@ -519,7 +504,6 @@ private void assertMessages(ErrorResponse ex, List + From 7a0ea1445226ea32e82984311b251f01bc083a1d Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 20 Nov 2025 09:48:06 +0100 Subject: [PATCH 017/235] Next development version (v6.2.15-SNAPSHOT) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6f38df98ac86..f1ed6d864553 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=6.2.14-SNAPSHOT +version=6.2.15-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m From 1bdd8337c6375a5b31873a1202b5c8fd61001315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Thu, 20 Nov 2025 09:55:04 +0100 Subject: [PATCH 018/235] Update outdated comments in JdbcOperationsExtensions.kt --- .../org/springframework/jdbc/core/JdbcOperationsExtensions.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt b/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt index 0c6609274652..8dddd4779372 100644 --- a/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt +++ b/spring-jdbc/src/main/kotlin/org/springframework/jdbc/core/JdbcOperationsExtensions.kt @@ -55,7 +55,7 @@ inline fun JdbcOperations.queryForObject(sql: String, args: Array JdbcOperations.queryForObject(sql: String, args: Array): T? = queryForObject(sql, args, T::class.java) as T @@ -89,7 +89,7 @@ inline fun JdbcOperations.queryForList(sql: String, args: Array JdbcOperations.queryForList(sql: String, args: Array): List = queryForList(sql, args, T::class.java) From a4b1155ca6b59e4ce100d5bda281b7d464c5a025 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 20 Nov 2025 10:08:12 +0000 Subject: [PATCH 019/235] Update Antora Spring UI to v0.4.22 Closes gh-35859 --- framework-docs/antora-playbook.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/antora-playbook.yml b/framework-docs/antora-playbook.yml index 8e4a0a2cb75c..e30c5942589e 100644 --- a/framework-docs/antora-playbook.yml +++ b/framework-docs/antora-playbook.yml @@ -36,4 +36,4 @@ runtime: failure_level: warn ui: bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.20/ui-bundle.zip + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.22/ui-bundle.zip From a4134663a64eeceda55d14dabb44516812971b33 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:44:14 +0100 Subject: [PATCH 020/235] Update Antora Spring UI to v0.4.25 Closes gh-35877 Co-authored-by: github-actions[bot] --- framework-docs/antora-playbook.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/antora-playbook.yml b/framework-docs/antora-playbook.yml index e30c5942589e..a35ccc750ed2 100644 --- a/framework-docs/antora-playbook.yml +++ b/framework-docs/antora-playbook.yml @@ -36,4 +36,4 @@ runtime: failure_level: warn ui: bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.22/ui-bundle.zip + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.25/ui-bundle.zip From ab93020263aed1d7b26a3b8afabf3fbc4b12722b Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:55:01 +0100 Subject: [PATCH 021/235] Link to Spring Framework Artifacts wiki page This commit revises the Integration Testing chapter to reference the "Spring Framework Artifacts" wiki page instead of the nonexistent "Dependency Management" section of the reference manual. Closes gh-35890 (cherry picked from commit 45c1cd9295a6da25cdb426a0e9895babbc4ba3ca) --- .../ROOT/pages/testing/integration.adoc | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/testing/integration.adoc b/framework-docs/modules/ROOT/pages/testing/integration.adoc index 264798556902..98b533838ed3 100644 --- a/framework-docs/modules/ROOT/pages/testing/integration.adoc +++ b/framework-docs/modules/ROOT/pages/testing/integration.adoc @@ -5,24 +5,25 @@ It is important to be able to perform some integration testing without requiring deployment to your application server or connecting to other enterprise infrastructure. Doing so lets you test things such as: -* The correct wiring of your Spring IoC container contexts. -* Data access using JDBC or an ORM tool. This can include such things as the correctness - of SQL statements, Hibernate queries, JPA entity mappings, and so forth. +* The correct wiring of your Spring components. +* Data access using JDBC or an ORM tool. + ** This can include such things as the correctness of SQL statements, Hibernate queries, + JPA entity mappings, and so forth. The Spring Framework provides first-class support for integration testing in the -`spring-test` module. The name of the actual JAR file might include the release version -and might also be in the long `org.springframework.test` form, depending on where you get -it from (see the xref:core/beans/dependencies.adoc[section on Dependency Management] -for an explanation). This library includes the `org.springframework.test` package, which +`spring-test` module. The name of the actual JAR file might include the release version, +depending on where you get it from (see the +{spring-framework-wiki}/Spring-Framework-Artifacts[Spring Framework Artifacts] wiki page +for details). This library includes the `org.springframework.test` package, which contains valuable classes for integration testing with a Spring container. This testing does not rely on an application server or other deployment environment. Such tests are slower to run than unit tests but much faster than the equivalent Selenium tests or remote tests that rely on deployment to an application server. Unit and integration testing support is provided in the form of the annotation-driven -xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. The TestContext framework is -agnostic of the actual testing framework in use, which allows instrumentation of tests -in various environments, including JUnit, TestNG, and others. +xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. The TestContext +framework is agnostic of the actual testing framework in use, which allows +instrumentation of tests in various environments, including JUnit, TestNG, and others. The following section provides an overview of the high-level goals of Spring's integration support, and the rest of this chapter then focuses on dedicated topics: From 85c47a73dda90f4d9201464f2f36c53d89cc0e0d Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:15:56 +0100 Subject: [PATCH 022/235] Fix formatting for backticks in Kotlin docs (cherry picked from commit e625a28f6def72f72be0b9821d3d71c0a82ee7f6) --- .../modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc index 89541990c3cc..feba63aa20b4 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc @@ -352,7 +352,7 @@ file with a `spring.test.constructor.autowire.mode = all` property. [[per_class-lifecycle]] === `PER_CLASS` Lifecycle -Kotlin lets you specify meaningful test function names between backticks (```). +Kotlin lets you specify meaningful test function names between backticks (+++```+++). With JUnit Jupiter (JUnit 5), Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` annotation to enable single instantiation of test classes, which allows the use of `@BeforeAll` and `@AfterAll` annotations on non-static methods, which is a good fit for Kotlin. From 37e26e0377b71096b19d7271c9e7d8e44b9f3efb Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 26 Nov 2025 13:22:26 +0100 Subject: [PATCH 023/235] Use current links to JUnit documentation Closes gh-35892 (cherry picked from commit f4ee120a423c069ddfc94076cc49e02ec9d797d5) --- .../ROOT/pages/languages/kotlin/spring-projects-in.adoc | 2 +- .../pages/testing/annotations/integration-junit-jupiter.adoc | 2 +- .../pages/testing/testcontext-framework/support-classes.adoc | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc index feba63aa20b4..7ae0b27f84fe 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc @@ -319,7 +319,7 @@ progresses. == Testing This section addresses testing with the combination of Kotlin and Spring Framework. -The recommended testing framework is https://junit.org/junit5/[JUnit 5] along with +The recommended testing framework is https://junit.org/[JUnit Jupiter] along with https://mockk.io/[Mockk] for mocking. NOTE: If you are using Spring Boot, see diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc index b0366adccb66..68b37ef336fb 100644 --- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc +++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit-jupiter.adoc @@ -187,7 +187,7 @@ default mode may be set via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism. The default mode may also be configured as a -https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter]. +https://docs.junit.org/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter]. If the `spring.test.constructor.autowire.mode` property is not set, test class constructors will not be automatically autowired. diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc index c3eacf558bac..6a69c05f5881 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc @@ -22,7 +22,7 @@ TestNG: * Dependency injection for test constructors, test methods, and test lifecycle callback methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency Injection with the `SpringExtension`] for further details. -* Powerful support for link:https://junit.org/junit5/docs/current/user-guide/#extensions-conditions[conditional +* Powerful support for link:https://docs.junit.org/current/user-guide/#extensions-conditions[conditional test execution] based on SpEL expressions, environment variables, system properties, and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] @@ -160,7 +160,7 @@ for further details. === Dependency Injection with the `SpringExtension` The `SpringExtension` implements the -link:https://junit.org/junit5/docs/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`] +link:https://docs.junit.org/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`] extension API from JUnit Jupiter, which lets Spring provide dependency injection for test constructors, test methods, and test lifecycle callback methods. From f9b4fba97a32534d5e2fa0f18d116ffa68077560 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Tue, 25 Nov 2025 19:03:08 +0000 Subject: [PATCH 024/235] Add required type to TypeMismatchException message args Closes gh-35837 --- .../ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc | 2 +- .../method/annotation/ResponseEntityExceptionHandler.java | 5 +++-- .../annotation/ResponseEntityExceptionHandlerTests.java | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc index ce39fbfca07e..6fb85ed96f47 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-ann-rest-exceptions.adoc @@ -175,7 +175,7 @@ Message codes and arguments for each error are also resolved via `MessageSource` | `TypeMismatchException` | (default) -| `+{0}+` property name, `+{1}+` property value +| `+{0}+` property name, `+{1}+` property value, `+{2}+` simple name of required type | `UnsatisfiedServletRequestParameterException` | (default) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java index c1cb7edb1558..c853a6021554 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java @@ -484,7 +484,7 @@ protected ResponseEntity handleConversionNotSupported( /** * Customize the handling of {@link TypeMismatchException}. - *

By default this method creates a {@link ProblemDetail} with the status + *

By default, this method creates a {@link ProblemDetail} with the status * and a short detail message, and also looks up an override for the detail * via {@link MessageSource}, before delegating to * {@link #handleExceptionInternal}. @@ -499,7 +499,8 @@ protected ResponseEntity handleConversionNotSupported( protected ResponseEntity handleTypeMismatch( TypeMismatchException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) { - Object[] args = {ex.getPropertyName(), ex.getValue()}; + Object[] args = {ex.getPropertyName(), ex.getValue(), + (ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "")}; String defaultDetail = "Failed to convert '" + args[0] + "' with value: '" + args[1] + "'"; String messageCode = ErrorResponse.getDefaultDetailMessageCode(TypeMismatchException.class, null); ProblemDetail body = createProblemDetail(ex, status, defaultDetail, messageCode, args, request); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java index 7e1f511b6b0a..4856c2710347 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java @@ -245,7 +245,7 @@ void typeMismatchWithProblemDetailViaMessageSource() { StaticMessageSource messageSource = new StaticMessageSource(); messageSource.addMessage( ErrorResponse.getDefaultDetailMessageCode(TypeMismatchException.class, null), locale, - "Failed to set {0} to value: {1}"); + "Failed to set {0} to value: {1} for type {2}"); this.exceptionHandler.setMessageSource(messageSource); @@ -253,7 +253,7 @@ void typeMismatchWithProblemDetailViaMessageSource() { new TypeMismatchException(new PropertyChangeEvent(this, "name", "John", "James"), String.class)); ProblemDetail body = (ProblemDetail) entity.getBody(); - assertThat(body.getDetail()).isEqualTo("Failed to set name to value: James"); + assertThat(body.getDetail()).isEqualTo("Failed to set name to value: James for type String"); } finally { LocaleContextHolder.resetLocaleContext(); From 19b080b73f3d4ae78fc2aae31ca8782b573beafc Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 26 Nov 2025 15:06:22 +0100 Subject: [PATCH 025/235] Clarify JMS sessionTransacted flag for local versus global transaction Closes gh-35897 (cherry picked from commit 9d4abb63d8151265f96449a625810f4b7390847a) --- .../org/springframework/jms/support/JmsAccessor.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/spring-jms/src/main/java/org/springframework/jms/support/JmsAccessor.java b/spring-jms/src/main/java/org/springframework/jms/support/JmsAccessor.java index 57b2e554b680..4927bd74d55f 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/JmsAccessor.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/JmsAccessor.java @@ -102,12 +102,6 @@ protected final ConnectionFactory obtainConnectionFactory() { /** * Set the transaction mode that is used when creating a JMS {@link Session}. * Default is "false". - *

Note that within a JTA transaction, the parameters passed to - * {@code create(Queue/Topic)Session(boolean transacted, int acknowledgeMode)} - * method are not taken into account. Depending on the Jakarta EE transaction context, - * the container makes its own decisions on these values. Analogously, these - * parameters are not taken into account within a locally managed transaction - * either, since the accessor operates on an existing JMS Session in this case. *

Setting this flag to "true" will use a short local JMS transaction * when running outside a managed transaction, and a synchronized local * JMS transaction in case of a managed transaction (other than an XA @@ -115,6 +109,11 @@ protected final ConnectionFactory obtainConnectionFactory() { * transaction being managed alongside the main transaction (which might * be a native JDBC transaction), with the JMS transaction committing * right after the main transaction. + *

Note that this flag is meant to remain at its default value "false" for + * participating in a global JTA/XA transaction in a Jakarta EE environment. + * While the server may leniently ignore local Session-level transaction + * management in such a scenario, it may also throw unexpected exceptions + * on commit/rollback in case of this Session-level flag being set to "true". * @see jakarta.jms.Connection#createSession(boolean, int) */ public void setSessionTransacted(boolean sessionTransacted) { From ab96576e6753bb7c1ffa7c4c3ff9738bbe3cbefd Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 26 Nov 2025 15:08:24 +0100 Subject: [PATCH 026/235] Expose non-existent resources at the end of the sorted result Closes gh-35895 (cherry picked from commit c1b6bfb6818a91ec2b7ec16a73562b57c8283b01) --- .../jdbc/config/SortedResourcesFactoryBean.java | 15 +++++++++------ .../config/JdbcNamespaceIntegrationTests.java | 9 +++++++++ .../jdbc/config/jdbc-config-nonexistent.xml | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-nonexistent.xml diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/config/SortedResourcesFactoryBean.java b/spring-jdbc/src/main/java/org/springframework/jdbc/config/SortedResourcesFactoryBean.java index 77776adf4466..51ea9aaad720 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/config/SortedResourcesFactoryBean.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/config/SortedResourcesFactoryBean.java @@ -72,20 +72,22 @@ public Class getObjectType() { @Override protected Resource[] createInstance() throws Exception { - List scripts = new ArrayList<>(); + List result = new ArrayList<>(); for (String location : this.locations) { Resource[] resources = this.resourcePatternResolver.getResources(location); // Cache URLs to avoid repeated I/O during sorting Map urlCache = new LinkedHashMap<>(resources.length); + List failingResources = new ArrayList<>(); for (Resource resource : resources) { try { urlCache.put(resource, resource.getURL().toString()); } catch (IOException ex) { - throw new IllegalStateException( - "Failed to resolve URL for resource [" + resource + - "] from location pattern [" + location + "]", ex); + if (logger.isDebugEnabled()) { + logger.debug("Failed to resolve " + resource + " for sorting purposes: " + ex); + } + failingResources.add(resource); } } @@ -93,9 +95,10 @@ protected Resource[] createInstance() throws Exception { List sortedResources = new ArrayList<>(urlCache.keySet()); sortedResources.sort(Comparator.comparing(urlCache::get)); - scripts.addAll(sortedResources); + result.addAll(sortedResources); + result.addAll(failingResources); } - return scripts.toArray(new Resource[0]); + return result.toArray(new Resource[0]); } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/config/JdbcNamespaceIntegrationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/config/JdbcNamespaceIntegrationTests.java index 06b843729189..73d202803e74 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/config/JdbcNamespaceIntegrationTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/config/JdbcNamespaceIntegrationTests.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; @@ -32,6 +33,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.AbstractDriverBasedDataSource; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactoryBean; +import org.springframework.jdbc.datasource.init.CannotReadScriptException; import org.springframework.jdbc.datasource.init.DataSourceInitializer; import org.springframework.lang.Nullable; @@ -67,6 +69,13 @@ void createWithResourcePattern() { assertCorrectSetup("jdbc-config-pattern.xml", "dataSource"); } + @Test + void createWithNonExistentResource() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> assertCorrectSetup("jdbc-config-nonexistent.xml", "dataSource")) + .withCauseInstanceOf(CannotReadScriptException.class); + } + @Test void createWithAnonymousDataSourceAndDefaultDatabaseName() { assertThat(extractDataSourceUrl("jdbc-config-db-name-default-and-anonymous-datasource.xml")) diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-nonexistent.xml b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-nonexistent.xml new file mode 100644 index 000000000000..c8314f0ce5fc --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-nonexistent.xml @@ -0,0 +1,14 @@ + + + + + + + + + + From e7a5452a14f80a44f1db772e841138626942d807 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 26 Nov 2025 15:09:05 +0100 Subject: [PATCH 027/235] Consistent namespace element declarations (cherry picked from commit f58d0f6aaee53e05aa7940c5f284a447f624ea9c) --- ...nfig-db-name-default-and-anonymous-datasource.xml | 4 ++-- .../jdbc/config/jdbc-config-db-name-explicit.xml | 12 ++++++------ .../jdbc/config/jdbc-config-db-name-generated.xml | 12 ++++++------ .../jdbc/config/jdbc-config-db-name-implicit.xml | 12 ++++++------ .../jdbc/config/jdbc-config-multiple-datasources.xml | 5 +++-- .../org/springframework/jdbc/config/jdbc-config.xml | 12 ++++++------ .../config/jdbc-initialize-expression-config.xml | 6 +++--- .../config/jdbc-initialize-placeholder-config.xml | 9 ++++----- 8 files changed, 36 insertions(+), 36 deletions(-) diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-default-and-anonymous-datasource.xml b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-default-and-anonymous-datasource.xml index 06f7c122bc5e..7d57bd8e8928 100644 --- a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-default-and-anonymous-datasource.xml +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-default-and-anonymous-datasource.xml @@ -5,8 +5,8 @@ http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc-4.2.xsd"> - - + + diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-explicit.xml b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-explicit.xml index e378d39625ca..748af6467ecf 100644 --- a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-explicit.xml +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-explicit.xml @@ -1,12 +1,12 @@ - - -