Browse Source

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
pull/49790/head
Andy Wilkinson 1 week ago
parent
commit
025b527e87
  1. 56
      spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfiguration.java
  2. 83
      spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/websocket/servlet/WebSocketMessagingAutoConfigurationTests.java

56
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; @@ -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; @@ -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 @@ -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 { @@ -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 { @@ -107,9 +125,9 @@ public class WebSocketMessagingAutoConfiguration {
}
}
@Bean
static LazyInitializationExcludeFilter eagerStompWebSocketHandlerMapping() {
return (name, definition, type) -> name.equals("stompWebSocketHandlerMapping");
@Override
public int getOrder() {
return 0;
}
}

83
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; @@ -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; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 {

Loading…
Cancel
Save