diff --git a/spring-websocket/spring-websocket.gradle b/spring-websocket/spring-websocket.gradle index 27d12b292e..2f9bcc1c79 100644 --- a/spring-websocket/spring-websocket.gradle +++ b/spring-websocket/spring-websocket.gradle @@ -17,6 +17,7 @@ dependencies { } optional("org.eclipse.jetty.websocket:jetty-websocket-jetty-api") optional("org.eclipse.jetty:jetty-client") + optional("tools.jackson.core:jackson-databind") testImplementation(testFixtures(project(":spring-core"))) testImplementation(testFixtures(project(":spring-web"))) testImplementation("io.projectreactor.netty:reactor-netty-http") diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java index a8db15ecbd..b402ae2653 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/config/annotation/WebSocketMessageBrokerConfigurationSupport.java @@ -23,8 +23,6 @@ import org.springframework.beans.factory.config.CustomScopeConfigurer; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.core.task.TaskExecutor; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; -import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.SimpSessionScope; import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler; @@ -162,17 +160,4 @@ public abstract class WebSocketMessageBrokerConfigurationSupport extends Abstrac return stats; } - @Override - protected MappingJackson2MessageConverter createJacksonConverter() { - MappingJackson2MessageConverter messageConverter = super.createJacksonConverter(); - // Use Jackson builder in order to have well-known modules registered automatically. - Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json(); - ApplicationContext applicationContext = getApplicationContext(); - if (applicationContext != null) { - builder.applicationContext(applicationContext); - } - messageConverter.setObjectMapper(builder.build()); - return messageConverter; - } - } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/AbstractClientSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/AbstractClientSockJsSession.java index 6ad89a93b0..59fcad6253 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/AbstractClientSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/AbstractClientSockJsSession.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -47,6 +47,7 @@ import org.springframework.web.socket.sockjs.frame.SockJsMessageCodec; * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Sebastien Deleuze * @since 4.1 */ public abstract class AbstractClientSockJsSession implements WebSocketSession { @@ -268,7 +269,7 @@ public abstract class AbstractClientSockJsSession implements WebSocketSession { try { messages = getMessageCodec().decode(frameData); } - catch (IOException ex) { + catch (RuntimeException | IOException ex) { if (logger.isErrorEnabled()) { logger.error("Failed to decode data for SockJS \"message\" frame: " + frame + " in " + this, ex); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java index 726832bc07..ba67e7c35b 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/SockJsClient.java @@ -40,6 +40,7 @@ import org.springframework.web.socket.WebSocketHttpHeaders; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.client.WebSocketClient; import org.springframework.web.socket.sockjs.frame.Jackson2SockJsMessageCodec; +import org.springframework.web.socket.sockjs.frame.JacksonJsonSockJsMessageCodec; import org.springframework.web.socket.sockjs.frame.SockJsMessageCodec; import org.springframework.web.socket.sockjs.transport.TransportType; import org.springframework.web.util.UriComponentsBuilder; @@ -62,6 +63,9 @@ import org.springframework.web.util.UriComponentsBuilder; */ public class SockJsClient implements WebSocketClient, Lifecycle { + private static final boolean jacksonPresent = ClassUtils.isPresent( + "tools.jackson.databind.ObjectMapper", SockJsClient.class.getClassLoader()); + private static final boolean jackson2Present = ClassUtils.isPresent( "com.fasterxml.jackson.databind.ObjectMapper", SockJsClient.class.getClassLoader()); @@ -97,7 +101,10 @@ public class SockJsClient implements WebSocketClient, Lifecycle { Assert.notEmpty(transports, "No transports provided"); this.transports = new ArrayList<>(transports); this.infoReceiver = initInfoReceiver(transports); - if (jackson2Present) { + if (jacksonPresent) { + this.messageCodec = new JacksonJsonSockJsMessageCodec(); + } + else if (jackson2Present) { this.messageCodec = new Jackson2SockJsMessageCodec(); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/JacksonJsonSockJsMessageCodec.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/JacksonJsonSockJsMessageCodec.java new file mode 100644 index 0000000000..86cf14b785 --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/frame/JacksonJsonSockJsMessageCodec.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2025 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.socket.sockjs.frame; + +import java.io.InputStream; + +import com.fasterxml.jackson.core.io.JsonStringEncoder; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.util.Assert; + +/** + * A Jackson 3.x codec for encoding and decoding SockJS messages. + * + *

The default constructor loads {@link tools.jackson.databind.JacksonModule}s + * found by {@link MapperBuilder#findModules(ClassLoader)}. + * + * @author Sebastien Deleuze + * @since 7.0 + */ +public class JacksonJsonSockJsMessageCodec extends AbstractSockJsMessageCodec { + + private final ObjectMapper objectMapper; + + + /** + * Construct a new instance with a {@link JsonMapper} customized with the + * {@link tools.jackson.databind.JacksonModule}s found by + * {@link MapperBuilder#findModules(ClassLoader)}. + */ + public JacksonJsonSockJsMessageCodec() { + this.objectMapper = JsonMapper.builder().findAndAddModules(JacksonJsonSockJsMessageCodec.class.getClassLoader()).build(); + } + + /** + * Construct a new instance with the provided {@link ObjectMapper}. + * @see JsonMapper#builder() + * @see MapperBuilder#findAndAddModules(ClassLoader) + */ + public JacksonJsonSockJsMessageCodec(ObjectMapper objectMapper) { + Assert.notNull(objectMapper, "ObjectMapper must not be null"); + this.objectMapper = objectMapper; + } + + + @Override + public String @Nullable [] decode(String content) { + return this.objectMapper.readValue(content, String[].class); + } + + @Override + public String @Nullable [] decodeInputStream(InputStream content) { + return this.objectMapper.readValue(content, String[].class); + } + + @Override + protected char[] applyJsonQuoting(String content) { + return JsonStringEncoder.getInstance().quoteAsString(content); + } + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlingSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlingSockJsService.java index 10dca2b581..2dd90c876b 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlingSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/TransportHandlingSockJsService.java @@ -51,6 +51,7 @@ import org.springframework.web.socket.server.HandshakeInterceptor; import org.springframework.web.socket.server.support.HandshakeInterceptorChain; import org.springframework.web.socket.sockjs.SockJsException; import org.springframework.web.socket.sockjs.frame.Jackson2SockJsMessageCodec; +import org.springframework.web.socket.sockjs.frame.JacksonJsonSockJsMessageCodec; import org.springframework.web.socket.sockjs.frame.SockJsMessageCodec; import org.springframework.web.socket.sockjs.support.AbstractSockJsService; @@ -70,6 +71,9 @@ import org.springframework.web.socket.sockjs.support.AbstractSockJsService; */ public class TransportHandlingSockJsService extends AbstractSockJsService implements SockJsServiceConfig, Lifecycle { + private static final boolean jacksonPresent = ClassUtils.isPresent( + "tools.jackson.databind.ObjectMapper", TransportHandlingSockJsService.class.getClassLoader()); + private static final boolean jackson2Present = ClassUtils.isPresent( "com.fasterxml.jackson.databind.ObjectMapper", TransportHandlingSockJsService.class.getClassLoader()); @@ -118,7 +122,10 @@ public class TransportHandlingSockJsService extends AbstractSockJsService implem } } - if (jackson2Present) { + if (jacksonPresent) { + this.messageCodec = new JacksonJsonSockJsMessageCodec(); + } + else if (jackson2Present) { this.messageCodec = new Jackson2SockJsMessageCodec(); } } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/ClientSockJsSessionTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/ClientSockJsSessionTests.java index 80ca072fc9..5bd2386642 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/ClientSockJsSessionTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/ClientSockJsSessionTests.java @@ -29,7 +29,7 @@ import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketExtension; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; -import org.springframework.web.socket.sockjs.frame.Jackson2SockJsMessageCodec; +import org.springframework.web.socket.sockjs.frame.JacksonJsonSockJsMessageCodec; import org.springframework.web.socket.sockjs.frame.SockJsFrame; import org.springframework.web.socket.sockjs.transport.TransportType; @@ -48,7 +48,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; */ class ClientSockJsSessionTests { - private static final Jackson2SockJsMessageCodec CODEC = new Jackson2SockJsMessageCodec(); + private static final JacksonJsonSockJsMessageCodec CODEC = new JacksonJsonSockJsMessageCodec(); private WebSocketHandler handler = mock(); diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequestTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequestTests.java index b7c792dfab..f97ef91269 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequestTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/DefaultTransportRequestTests.java @@ -30,7 +30,7 @@ import org.mockito.ArgumentCaptor; import org.springframework.http.HttpHeaders; import org.springframework.scheduling.TaskScheduler; import org.springframework.web.socket.WebSocketSession; -import org.springframework.web.socket.sockjs.frame.Jackson2SockJsMessageCodec; +import org.springframework.web.socket.sockjs.frame.JacksonJsonSockJsMessageCodec; import org.springframework.web.socket.sockjs.transport.TransportType; import static org.assertj.core.api.Assertions.assertThat; @@ -47,7 +47,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; */ class DefaultTransportRequestTests { - private final Jackson2SockJsMessageCodec CODEC = new Jackson2SockJsMessageCodec(); + private final JacksonJsonSockJsMessageCodec CODEC = new JacksonJsonSockJsMessageCodec(); private CompletableFuture connectFuture = new CompletableFuture<>(); diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/RestTemplateXhrTransportTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/RestTemplateXhrTransportTests.java index 119ac3d6ae..175d0327a4 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/RestTemplateXhrTransportTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/RestTemplateXhrTransportTests.java @@ -50,7 +50,7 @@ import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; -import org.springframework.web.socket.sockjs.frame.Jackson2SockJsMessageCodec; +import org.springframework.web.socket.sockjs.frame.JacksonJsonSockJsMessageCodec; import org.springframework.web.socket.sockjs.frame.SockJsFrame; import org.springframework.web.socket.sockjs.transport.TransportType; @@ -69,7 +69,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; */ class RestTemplateXhrTransportTests { - private static final Jackson2SockJsMessageCodec CODEC = new Jackson2SockJsMessageCodec(); + private static final JacksonJsonSockJsMessageCodec CODEC = new JacksonJsonSockJsMessageCodec(); private final WebSocketHandler webSocketHandler = mock(); @@ -114,7 +114,7 @@ class RestTemplateXhrTransportTests { Message message = MessageBuilder.createMessage("body".getBytes(UTF_8), headers); byte[] bytes = new StompEncoder().encode(message); TextMessage textMessage = new TextMessage(bytes); - SockJsFrame frame = SockJsFrame.messageFrame(new Jackson2SockJsMessageCodec(), textMessage.getPayload()); + SockJsFrame frame = SockJsFrame.messageFrame(new JacksonJsonSockJsMessageCodec(), textMessage.getPayload()); String body = """ o diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/frame/SockJsFrameTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/frame/SockJsFrameTests.java index 80b490152d..29a858e900 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/frame/SockJsFrameTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/frame/SockJsFrameTests.java @@ -49,7 +49,7 @@ class SockJsFrameTests { @Test void messageArrayFrame() { - SockJsFrame frame = SockJsFrame.messageFrame(new Jackson2SockJsMessageCodec(), "m1", "m2"); + SockJsFrame frame = SockJsFrame.messageFrame(new JacksonJsonSockJsMessageCodec(), "m1", "m2"); assertThat(frame.getContent()).isEqualTo("a[\"m1\",\"m2\"]"); assertThat(frame.getType()).isEqualTo(SockJsFrameType.MESSAGE); diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/StubSockJsServiceConfig.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/StubSockJsServiceConfig.java index d8521ce4c2..3dad317688 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/StubSockJsServiceConfig.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/StubSockJsServiceConfig.java @@ -18,7 +18,7 @@ package org.springframework.web.socket.sockjs.transport.session; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.web.socket.sockjs.frame.Jackson2SockJsMessageCodec; +import org.springframework.web.socket.sockjs.frame.JacksonJsonSockJsMessageCodec; import org.springframework.web.socket.sockjs.frame.SockJsMessageCodec; import org.springframework.web.socket.sockjs.transport.SockJsServiceConfig; @@ -33,7 +33,7 @@ public class StubSockJsServiceConfig implements SockJsServiceConfig { private TaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); - private SockJsMessageCodec messageCodec = new Jackson2SockJsMessageCodec(); + private SockJsMessageCodec messageCodec = new JacksonJsonSockJsMessageCodec(); private int httpMessageCacheSize = 100; diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/WebSocketServerSockJsSessionTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/WebSocketServerSockJsSessionTests.java index 79733172cb..c419768f72 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/WebSocketServerSockJsSessionTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/session/WebSocketServerSockJsSessionTests.java @@ -16,13 +16,13 @@ package org.springframework.web.socket.sockjs.transport.session; -import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import tools.jackson.core.JacksonException; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; @@ -118,7 +118,7 @@ class WebSocketServerSockJsSessionTests extends AbstractSockJsSessionTests