From b3dfba8719d0994052d33bee0842838860c95de5 Mon Sep 17 00:00:00 2001 From: shenjianeng Date: Fri, 8 May 2026 11:19:22 +0800 Subject: [PATCH 1/4] Refactor map initialization using CollectionUtils Closes gh-36763 Signed-off-by: shenjianeng --- .../org/springframework/web/util/DefaultUriBuilderFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java b/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java index d15e89e3697..28df199b0af 100644 --- a/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java +++ b/spring-web/src/main/java/org/springframework/web/util/DefaultUriBuilderFactory.java @@ -433,7 +433,7 @@ public class DefaultUriBuilderFactory implements UriBuilderFactory { @Override public URI build(Map uriVars) { if (!CollectionUtils.isEmpty(defaultUriVariables)) { - Map map = new HashMap<>(defaultUriVariables.size() + uriVars.size()); + Map map = CollectionUtils.newHashMap(defaultUriVariables.size() + uriVars.size()); map.putAll(defaultUriVariables); map.putAll(uriVars); uriVars = map; From 84efa46e093c2a6d2695897ce5d819daf6e44c74 Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Wed, 20 May 2026 11:41:45 -0500 Subject: [PATCH 2/4] DefaultPathContainer uses immutable map for SEPARATORS Closes gh-36821 Signed-off-by: Patrick Strawderman --- .../http/server/DefaultPathContainer.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/DefaultPathContainer.java b/spring-web/src/main/java/org/springframework/http/server/DefaultPathContainer.java index 5e68f500a95..5ada0ecc014 100644 --- a/spring-web/src/main/java/org/springframework/http/server/DefaultPathContainer.java +++ b/spring-web/src/main/java/org/springframework/http/server/DefaultPathContainer.java @@ -20,7 +20,6 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -42,14 +41,11 @@ import org.springframework.util.StringUtils; */ final class DefaultPathContainer implements PathContainer { - private static final PathContainer EMPTY_PATH = new DefaultPathContainer("", Collections.emptyList()); + private static final PathContainer EMPTY_PATH = new DefaultPathContainer("", List.of()); - private static final Map SEPARATORS = new HashMap<>(2); - - static { - SEPARATORS.put('/', new DefaultSeparator('/', "%2F")); - SEPARATORS.put('.', new DefaultSeparator('.', "%2E")); - } + private static final Map SEPARATORS = Map.of( + '/', new DefaultSeparator('/', "%2F"), + '.', new DefaultSeparator('.', "%2E")); private final String path; From f9b75b02da81d6ff6c294684833f245a1c2ab712 Mon Sep 17 00:00:00 2001 From: Max Guiking <32510398+mguiking@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:07:59 +0200 Subject: [PATCH 3/4] Fix last flag check in JettyWebSocketSession See gh-36650 Signed-off-by: Max Guiking --- .../socket/adapter/JettyWebSocketSession.java | 2 +- .../adapter/JettyWebSocketSessionTests.java | 144 ++++++++++++++++++ 2 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 spring-webflux/src/test/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSessionTests.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java index 33e8d9cefcf..84f6d056ec5 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSession.java @@ -209,7 +209,7 @@ public class JettyWebSocketSession extends AbstractWebSocketSession { } ByteBuffer buffer = iterator.next(); - boolean last = iterator.hasNext(); + boolean last = !iterator.hasNext(); session.sendPartialBinary(buffer, last, Callback.from(this::succeeded, this::failed)); return Action.SCHEDULED; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSessionTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSessionTests.java new file mode 100644 index 00000000000..5f41dfac45f --- /dev/null +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSessionTests.java @@ -0,0 +1,144 @@ +/* + * 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.reactive.socket.adapter; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.NoSuchElementException; + +import org.eclipse.jetty.websocket.api.Callback; +import org.eclipse.jetty.websocket.api.Session; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Mono; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferWrapper; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.web.reactive.socket.HandshakeInfo; +import org.springframework.web.reactive.socket.WebSocketMessage; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link JettyWebSocketSession}. + * + * @author Max Guiking + */ +class JettyWebSocketSessionTests { + + private final DefaultDataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; + + private final Session jettySession = mock(Session.class); + + private final JettyWebSocketSession session = new JettyWebSocketSession(this.jettySession, + new HandshakeInfo(URI.create("ws://example.org"), new HttpHeaders(), Mono.empty(), null), + this.bufferFactory); + + + @Test + void sendBinaryMessageWithSingleFragmentMarksFragmentAsLast() { + succeedOnSendPartialBinary(); + + DataBuffer payload = this.bufferFactory.wrap("hello".getBytes(StandardCharsets.UTF_8)); + WebSocketMessage message = new WebSocketMessage(WebSocketMessage.Type.BINARY, payload); + + this.session.sendMessage(message).block(); + + ArgumentCaptor lastCaptor = ArgumentCaptor.forClass(Boolean.class); + verify(this.jettySession).sendPartialBinary(any(ByteBuffer.class), lastCaptor.capture(), any(Callback.class)); + assertThat(lastCaptor.getValue()).as("FIN bit must be set for the final (and only) fragment").isTrue(); + } + + @Test + void sendBinaryMessageWithMultipleFragmentsMarksOnlyFinalFragmentAsLast() { + succeedOnSendPartialBinary(); + + List fragments = List.of( + ByteBuffer.wrap("one".getBytes(StandardCharsets.UTF_8)), + ByteBuffer.wrap("two".getBytes(StandardCharsets.UTF_8)), + ByteBuffer.wrap("three".getBytes(StandardCharsets.UTF_8))); + WebSocketMessage message = new WebSocketMessage(WebSocketMessage.Type.BINARY, + new MultiBufferDataBuffer(this.bufferFactory, fragments)); + + this.session.sendMessage(message).block(); + + ArgumentCaptor lastCaptor = ArgumentCaptor.forClass(Boolean.class); + verify(this.jettySession, times(fragments.size())) + .sendPartialBinary(any(ByteBuffer.class), lastCaptor.capture(), any(Callback.class)); + assertThat(lastCaptor.getAllValues()).containsExactly(false, false, true); + } + + private void succeedOnSendPartialBinary() { + doAnswer(invocation -> { + Callback callback = invocation.getArgument(2); + callback.succeed(); + return null; + }).when(this.jettySession).sendPartialBinary(any(ByteBuffer.class), anyBoolean(), any(Callback.class)); + } + + + /** + * Minimal {@link DataBuffer} whose {@link #readableByteBuffers()} yields a + * caller-supplied list of buffers, exercising the multi-fragment branch of + * {@link JettyWebSocketSession#sendMessage(WebSocketMessage)}. + */ + private static final class MultiBufferDataBuffer extends DataBufferWrapper { + + private final List buffers; + + MultiBufferDataBuffer(DefaultDataBufferFactory factory, List buffers) { + super(factory.allocateBuffer(0)); + this.buffers = buffers; + } + + @Override + public DataBuffer.ByteBufferIterator readableByteBuffers() { + return new DataBuffer.ByteBufferIterator() { + + private int index = 0; + + @Override + public boolean hasNext() { + return this.index < MultiBufferDataBuffer.this.buffers.size(); + } + + @Override + public ByteBuffer next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return MultiBufferDataBuffer.this.buffers.get(this.index++).asReadOnlyBuffer(); + } + + @Override + public void close() { + } + }; + } + } + +} From 6fd45c118d6fa438061113a506342fff6c53dc24 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 27 May 2026 17:15:01 +0100 Subject: [PATCH 4/4] Polishing contribution Closes gh-36650 --- .../adapter/JettyWebSocketSessionTests.java | 55 ++++++++----------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSessionTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSessionTests.java index 5f41dfac45f..24cd027f8ff 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSessionTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/socket/adapter/JettyWebSocketSessionTests.java @@ -19,6 +19,7 @@ package org.springframework.web.reactive.socket.adapter; import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.List; import java.util.NoSuchElementException; @@ -45,51 +46,45 @@ import static org.mockito.Mockito.verify; /** * Tests for {@link JettyWebSocketSession}. - * * @author Max Guiking */ class JettyWebSocketSessionTests { - private final DefaultDataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; + private final Session nativeSession = mock(Session.class); - private final Session jettySession = mock(Session.class); - - private final JettyWebSocketSession session = new JettyWebSocketSession(this.jettySession, - new HandshakeInfo(URI.create("ws://example.org"), new HttpHeaders(), Mono.empty(), null), - this.bufferFactory); + private final JettyWebSocketSession webSocketSession = new JettyWebSocketSession( + this.nativeSession, new HandshakeInfo(URI.create("ws://example.org"), + new HttpHeaders(), Mono.empty(), null), DefaultDataBufferFactory.sharedInstance); @Test - void sendBinaryMessageWithSingleFragmentMarksFragmentAsLast() { + void sendBinaryMessageWithSingleBuffer() { succeedOnSendPartialBinary(); - DataBuffer payload = this.bufferFactory.wrap("hello".getBytes(StandardCharsets.UTF_8)); + DataBuffer payload = DefaultDataBufferFactory.sharedInstance.wrap("hello".getBytes(StandardCharsets.UTF_8)); WebSocketMessage message = new WebSocketMessage(WebSocketMessage.Type.BINARY, payload); - this.session.sendMessage(message).block(); + this.webSocketSession.sendMessage(message).block(); - ArgumentCaptor lastCaptor = ArgumentCaptor.forClass(Boolean.class); - verify(this.jettySession).sendPartialBinary(any(ByteBuffer.class), lastCaptor.capture(), any(Callback.class)); - assertThat(lastCaptor.getValue()).as("FIN bit must be set for the final (and only) fragment").isTrue(); + ArgumentCaptor last = ArgumentCaptor.forClass(Boolean.class); + verify(this.nativeSession).sendPartialBinary(any(ByteBuffer.class), last.capture(), any(Callback.class)); + assertThat(last.getValue()).isTrue(); } @Test - void sendBinaryMessageWithMultipleFragmentsMarksOnlyFinalFragmentAsLast() { + void sendBinaryMessageWithMultipleBuffers() { succeedOnSendPartialBinary(); - List fragments = List.of( + WebSocketMessage message = new WebSocketMessage(WebSocketMessage.Type.BINARY, new MultiBufferDataBuffer( ByteBuffer.wrap("one".getBytes(StandardCharsets.UTF_8)), ByteBuffer.wrap("two".getBytes(StandardCharsets.UTF_8)), - ByteBuffer.wrap("three".getBytes(StandardCharsets.UTF_8))); - WebSocketMessage message = new WebSocketMessage(WebSocketMessage.Type.BINARY, - new MultiBufferDataBuffer(this.bufferFactory, fragments)); + ByteBuffer.wrap("three".getBytes(StandardCharsets.UTF_8)))); - this.session.sendMessage(message).block(); + this.webSocketSession.sendMessage(message).block(); - ArgumentCaptor lastCaptor = ArgumentCaptor.forClass(Boolean.class); - verify(this.jettySession, times(fragments.size())) - .sendPartialBinary(any(ByteBuffer.class), lastCaptor.capture(), any(Callback.class)); - assertThat(lastCaptor.getAllValues()).containsExactly(false, false, true); + ArgumentCaptor last = ArgumentCaptor.forClass(Boolean.class); + verify(this.nativeSession, times(3)).sendPartialBinary(any(ByteBuffer.class), last.capture(), any(Callback.class)); + assertThat(last.getAllValues()).containsExactly(false, false, true); } private void succeedOnSendPartialBinary() { @@ -97,22 +92,20 @@ class JettyWebSocketSessionTests { Callback callback = invocation.getArgument(2); callback.succeed(); return null; - }).when(this.jettySession).sendPartialBinary(any(ByteBuffer.class), anyBoolean(), any(Callback.class)); + }).when(this.nativeSession).sendPartialBinary(any(ByteBuffer.class), anyBoolean(), any(Callback.class)); } /** - * Minimal {@link DataBuffer} whose {@link #readableByteBuffers()} yields a - * caller-supplied list of buffers, exercising the multi-fragment branch of - * {@link JettyWebSocketSession#sendMessage(WebSocketMessage)}. + * Minimal DataBuffer that returns a given list of buffers from {@link #readableByteBuffers()}. */ private static final class MultiBufferDataBuffer extends DataBufferWrapper { private final List buffers; - MultiBufferDataBuffer(DefaultDataBufferFactory factory, List buffers) { - super(factory.allocateBuffer(0)); - this.buffers = buffers; + MultiBufferDataBuffer(ByteBuffer... buffers) { + super(DefaultDataBufferFactory.sharedInstance.allocateBuffer(0)); + this.buffers = Arrays.asList(buffers); } @Override @@ -123,7 +116,7 @@ class JettyWebSocketSessionTests { @Override public boolean hasNext() { - return this.index < MultiBufferDataBuffer.this.buffers.size(); + return (this.index < MultiBufferDataBuffer.this.buffers.size()); } @Override