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