From 025b527e87f9820c1142468fdcf0b21db3cd192d Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 25 Mar 2026 10:51:01 +0000 Subject: [PATCH] Fix WebSocketMessagingAutoConfiguration in the absence of Jackson Previously, the absence of Jackson would result in the loss of some configuration that does not depend on Jackson. That configuration is: - the inbound and outbound channels' executors - the bean that forces the stompWebSocketHandlerMapping bean to be eager when lazy init is enabled This commit updates the auto-configuration so that only the Jackson message converter configuration backs off in Jackson's absence. Fixes gh-49750 --- .../WebSocketMessagingAutoConfiguration.java | 56 ++++++++----- ...SocketMessagingAutoConfigurationTests.java | 83 ++++++++++++------- 2 files changed, 89 insertions(+), 50 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java index 73e5ebc30d7..567c7785a71 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java @@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.messaging.converter.ByteArrayMessageConverter; @@ -39,7 +40,6 @@ import org.springframework.messaging.converter.DefaultContentTypeResolver; import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.StringMessageConverter; -import org.springframework.messaging.simp.config.AbstractMessageBrokerConfiguration; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.util.MimeTypeUtils; import org.springframework.web.socket.config.annotation.DelegatingWebSocketMessageBrokerConfiguration; @@ -55,30 +55,31 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo */ @AutoConfiguration(after = JacksonAutoConfiguration.class) @ConditionalOnWebApplication(type = Type.SERVLET) -@ConditionalOnClass(WebSocketMessageBrokerConfigurer.class) +@ConditionalOnClass({ WebSocketMessageBrokerConfigurer.class, DelegatingWebSocketMessageBrokerConfiguration.class }) +@ConditionalOnBean(DelegatingWebSocketMessageBrokerConfiguration.class) public class WebSocketMessagingAutoConfiguration { + @Bean + static LazyInitializationExcludeFilter eagerStompWebSocketHandlerMapping() { + return (name, definition, type) -> name.equals("stompWebSocketHandlerMapping"); + } + + @Bean + WebSocketMessageBrokerExecutorConfigurer webSocketMessageBrokerExecutorConfigurer( + Map taskExecutors) { + return new WebSocketMessageBrokerExecutorConfigurer(taskExecutors); + } + @Configuration(proxyBeanMethods = false) - @ConditionalOnBean({ DelegatingWebSocketMessageBrokerConfiguration.class, ObjectMapper.class }) - @ConditionalOnClass({ ObjectMapper.class, AbstractMessageBrokerConfiguration.class }) + @ConditionalOnBean(ObjectMapper.class) + @ConditionalOnClass(ObjectMapper.class) @Order(0) static class WebSocketMessageConverterConfiguration implements WebSocketMessageBrokerConfigurer { private final ObjectMapper objectMapper; - private final AsyncTaskExecutor executor; - - WebSocketMessageConverterConfiguration(ObjectMapper objectMapper, - Map taskExecutors) { + WebSocketMessageConverterConfiguration(ObjectMapper objectMapper) { this.objectMapper = objectMapper; - this.executor = determineAsyncTaskExecutor(taskExecutors); - } - - private static AsyncTaskExecutor determineAsyncTaskExecutor(Map taskExecutors) { - if (taskExecutors.size() == 1) { - return taskExecutors.values().iterator().next(); - } - return taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME); } @Override @@ -93,6 +94,23 @@ public class WebSocketMessagingAutoConfiguration { return false; } + } + + static class WebSocketMessageBrokerExecutorConfigurer implements WebSocketMessageBrokerConfigurer, Ordered { + + private final AsyncTaskExecutor executor; + + WebSocketMessageBrokerExecutorConfigurer(Map taskExecutors) { + this.executor = determineAsyncTaskExecutor(taskExecutors); + } + + private static AsyncTaskExecutor determineAsyncTaskExecutor(Map taskExecutors) { + if (taskExecutors.size() == 1) { + return taskExecutors.values().iterator().next(); + } + return taskExecutors.get(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME); + } + @Override public void configureClientInboundChannel(ChannelRegistration registration) { if (this.executor != null) { @@ -107,9 +125,9 @@ public class WebSocketMessagingAutoConfiguration { } } - @Bean - static LazyInitializationExcludeFilter eagerStompWebSocketHandlerMapping() { - return (name, definition, type) -> name.equals("stompWebSocketHandlerMapping"); + @Override + public int getOrder() { + return 0; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfigurationTests.java index 75d6283806f..d25cb6ea391 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfigurationTests.java @@ -19,11 +19,10 @@ package org.springframework.boot.autoconfigure.websocket.servlet; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -38,9 +37,9 @@ import org.skyscreamer.jsonassert.JSONAssert; import org.springframework.boot.LazyInitializationBeanFactoryPostProcessor; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; -import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration.WebSocketMessageBrokerExecutorConfigurer; import org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.util.TestPropertyValues; @@ -56,7 +55,6 @@ import org.springframework.messaging.converter.CompositeMessageConverter; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.SimpleMessageConverter; import org.springframework.messaging.simp.annotation.SubscribeMapping; -import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.messaging.simp.stomp.StompCommand; import org.springframework.messaging.simp.stomp.StompFrameHandler; @@ -142,51 +140,74 @@ class WebSocketMessagingAutoConfigurationTests { } @Test - void predefinedThreadExecutorIsSelectedForInboundChannel() { + void asyncTaskExecutorBeanIsSelectedForInboundChannel() { AsyncTaskExecutor expectedExecutor = new SimpleAsyncTaskExecutor(); - ChannelRegistration registration = new ChannelRegistration(); - WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration configuration = new WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration( - new ObjectMapper(), - Map.of(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, expectedExecutor)); - configuration.configureClientInboundChannel(registration); - assertThat(registration).extracting("executor").isEqualTo(expectedExecutor); + this.context.register(WebSocketMessagingConfiguration.class); + this.context.registerBean(AsyncTaskExecutor.class, () -> expectedExecutor); + this.context.refresh(); + assertThat(this.context.getBean("clientInboundChannelExecutor", Executor.class)).isSameAs(expectedExecutor); } @Test - void predefinedThreadExecutorIsSelectedForOutboundChannel() { + void asyncTaskExecutorBeanIsSelectedForOutboundChannel() { AsyncTaskExecutor expectedExecutor = new SimpleAsyncTaskExecutor(); - ChannelRegistration registration = new ChannelRegistration(); - WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration configuration = new WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration( - new ObjectMapper(), - Map.of(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME, expectedExecutor)); - configuration.configureClientOutboundChannel(registration); - assertThat(registration).extracting("executor").isEqualTo(expectedExecutor); + this.context.register(WebSocketMessagingConfiguration.class); + this.context.registerBean(AsyncTaskExecutor.class, () -> expectedExecutor); + this.context.refresh(); + assertThat(this.context.getBean("clientOutboundChannelExecutor", Executor.class)).isSameAs(expectedExecutor); + } + + @Test + void withMultipleAsyncTaskExecutorBeansApplicationTaskExecutorIsSelectedForInboundChannel() { + AsyncTaskExecutor applicationTaskExecutor = new SimpleAsyncTaskExecutor(); + AsyncTaskExecutor additionalTaskExecutor = new SimpleAsyncTaskExecutor(); + this.context.registerBean("applicationTaskExecutor", AsyncTaskExecutor.class, () -> applicationTaskExecutor); + this.context.registerBean(AsyncTaskExecutor.class, () -> additionalTaskExecutor); + this.context.register(WebSocketMessagingConfiguration.class); + this.context.refresh(); + assertThat(this.context.getBean("clientInboundChannelExecutor", Executor.class)) + .isSameAs(applicationTaskExecutor); + } + + @Test + void withMultipleAsyncTaskExecutorBeansApplicationTaskExecutorIsSelectedForOutboundChannel() { + AsyncTaskExecutor applicationTaskExecutor = new SimpleAsyncTaskExecutor(); + AsyncTaskExecutor additionalTaskExecutor = new SimpleAsyncTaskExecutor(); + this.context.registerBean("applicationTaskExecutor", AsyncTaskExecutor.class, () -> applicationTaskExecutor); + this.context.registerBean(AsyncTaskExecutor.class, () -> additionalTaskExecutor); + this.context.register(WebSocketMessagingConfiguration.class); + this.context.refresh(); + assertThat(this.context.getBean("clientOutboundChannelExecutor", Executor.class)) + .isSameAs(applicationTaskExecutor); } @Test + @SuppressWarnings("unchecked") void webSocketMessageBrokerConfigurerOrdering() throws Throwable { - TestPropertyValues.of("server.port:0", "spring.jackson.serialization.indent-output:true").applyTo(this.context); + TestPropertyValues.of("server.port:0").applyTo(this.context); this.context.register(WebSocketMessagingConfiguration.class, CustomLowWebSocketMessageBrokerConfigurer.class, - CustomHighWebSocketMessageBrokerConfigurer.class); + CustomHighWebSocketMessageBrokerConfigurer.class, JacksonAutoConfiguration.class); this.context.refresh(); DelegatingWebSocketMessageBrokerConfiguration delegatingConfiguration = this.context .getBean(DelegatingWebSocketMessageBrokerConfiguration.class); - CustomHighWebSocketMessageBrokerConfigurer high = this.context - .getBean(CustomHighWebSocketMessageBrokerConfigurer.class); - WebSocketMessageConverterConfiguration autoConfiguration = this.context - .getBean(WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration.class); - WebSocketMessagingConfiguration configuration = this.context.getBean(WebSocketMessagingConfiguration.class); - CustomLowWebSocketMessageBrokerConfigurer low = this.context - .getBean(CustomLowWebSocketMessageBrokerConfigurer.class); assertThat(delegatingConfiguration).extracting("configurers") .asInstanceOf(InstanceOfAssertFactories.LIST) - .containsExactly(high, autoConfiguration, configuration, low); + .satisfies((configurers) -> { + assertThat(configurers).hasSize(5); + assertThat(configurers).first().isInstanceOf(CustomHighWebSocketMessageBrokerConfigurer.class); + assertThat((List) configurers.subList(1, 3)).contains( + this.context.getBean(WebSocketMessageConverterConfiguration.class), + this.context.getBean(WebSocketMessageBrokerExecutorConfigurer.class)); + assertThat((List) configurers.subList(3, 5)).contains( + this.context.getBean(WebSocketMessagingConfiguration.class), + this.context.getBean(CustomLowWebSocketMessageBrokerConfigurer.class)); + }); } private List getCustomizedConverters() { List customizedConverters = new ArrayList<>(); WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration configuration = new WebSocketMessagingAutoConfiguration.WebSocketMessageConverterConfiguration( - new ObjectMapper(), Collections.emptyMap()); + new ObjectMapper()); configuration.configureMessageConverters(customizedConverters); return customizedConverters; } @@ -198,7 +219,7 @@ class WebSocketMessagingAutoConfigurationTests { } private Object performStompSubscription(String topic) throws Throwable { - TestPropertyValues.of("server.port:0", "spring.jackson.serialization.indent-output:true").applyTo(this.context); + TestPropertyValues.of("server.port:0").applyTo(this.context); this.context.register(WebSocketMessagingConfiguration.class); this.context.refresh(); WebSocketStompClient stompClient = new WebSocketStompClient(this.sockJsClient); @@ -261,7 +282,7 @@ class WebSocketMessagingAutoConfigurationTests { @EnableWebSocket @EnableConfigurationProperties @EnableWebSocketMessageBroker - @ImportAutoConfiguration({ JacksonAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class, + @ImportAutoConfiguration({ ServletWebServerFactoryAutoConfiguration.class, WebSocketMessagingAutoConfiguration.class, DispatcherServletAutoConfiguration.class }) static class WebSocketMessagingConfiguration implements WebSocketMessageBrokerConfigurer {