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
This commit is contained in:
Andy Wilkinson
2026-03-25 10:51:01 +00:00
parent 328267249f
commit 025b527e87
2 changed files with 89 additions and 50 deletions
@@ -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<String, AsyncTaskExecutor> 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<String, AsyncTaskExecutor> taskExecutors) {
WebSocketMessageConverterConfiguration(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.executor = determineAsyncTaskExecutor(taskExecutors);
}
private static AsyncTaskExecutor determineAsyncTaskExecutor(Map<String, AsyncTaskExecutor> 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<String, AsyncTaskExecutor> taskExecutors) {
this.executor = determineAsyncTaskExecutor(taskExecutors);
}
private static AsyncTaskExecutor determineAsyncTaskExecutor(Map<String, AsyncTaskExecutor> 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;
}
}
@@ -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<Object>) configurers.subList(1, 3)).contains(
this.context.getBean(WebSocketMessageConverterConfiguration.class),
this.context.getBean(WebSocketMessageBrokerExecutorConfigurer.class));
assertThat((List<Object>) configurers.subList(3, 5)).contains(
this.context.getBean(WebSocketMessagingConfiguration.class),
this.context.getBean(CustomLowWebSocketMessageBrokerConfigurer.class));
});
}
private List<MessageConverter> getCustomizedConverters() {
List<MessageConverter> 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 {