GH-3533: Register WebSocket endpoints at runtime (#3548)
* GH-3533: Register WebSocket endpoints at runtime Fixes https://github.com/spring-projects/spring-integration/issues/3533 * Rework `WebSocketIntegrationConfigurationInitializer` to register beans functional way to avoid reflection for Spring Native support * Move `IntegrationServletWebSocketHandlerRegistry` into a separate file for better readability * Implement `DestructionAwareBeanPostProcessor` for `IntegrationServletWebSocketHandlerRegistry` to track runtime bean registrations and removals * Introduce an `IntegrationDynamicWebSocketHandlerMapping` to manage runtime mapping registrations and removals * Add `servlet-api` dependency into `websocket` to be able to compile an `IntegrationDynamicWebSocketHandlerMapping` * Fix typo in the exception message of the `StandardIntegrationFlowRegistration` * Start dynamically added beans together with associated `IntegrationFlow` in the `StandardIntegrationFlowContext` * Document new feature * * Fix language in docs * Don't start those `SmartLifecycle`s together with a dynamic flow which are not `isAutoStartup()` * * Fix `TomcatWebSocketTestServer` to configure servlet for `loadOnStartup = 1` * Fix `WebSocketDslTests` to make `clientWebSocketContainer.setAutoStartup(true)`
This commit is contained in:
@@ -104,6 +104,10 @@ public abstract class IntegrationWebSocketContainer implements DisposableBean {
|
||||
}
|
||||
}
|
||||
|
||||
public WebSocketHandler getWebSocketHandler() {
|
||||
return this.webSocketHandler;
|
||||
}
|
||||
|
||||
public List<String> getSubProtocols() {
|
||||
List<String> protocols = new ArrayList<>();
|
||||
if (this.messageListener != null) {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2021 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.integration.websocket.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.web.HttpRequestHandler;
|
||||
import org.springframework.web.servlet.HandlerExecutionChain;
|
||||
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
|
||||
|
||||
/**
|
||||
* The {@link AbstractHandlerMapping} implementation for dynamic WebSocket endpoint registrations in Spring Integration.
|
||||
* <p>
|
||||
* TODO until https://github.com/spring-projects/spring-framework/issues/26798
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 5.5
|
||||
*/
|
||||
class IntegrationDynamicWebSocketHandlerMapping extends AbstractHandlerMapping {
|
||||
|
||||
private final Map<String, HttpRequestHandler> handlerMap = new HashMap<>();
|
||||
|
||||
@Override
|
||||
protected Object getHandlerInternal(HttpServletRequest request) {
|
||||
String lookupPath = initLookupPath(request);
|
||||
HttpRequestHandler httpRequestHandler = this.handlerMap.get(lookupPath);
|
||||
return httpRequestHandler != null ? new HandlerExecutionChain(httpRequestHandler) : null;
|
||||
}
|
||||
|
||||
void registerHandler(String path, HttpRequestHandler httpHandler) {
|
||||
this.handlerMap.put(path, httpHandler);
|
||||
}
|
||||
|
||||
void unregisterHandler(String path) {
|
||||
this.handlerMap.remove(path);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2021 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.integration.websocket.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.integration.websocket.ServerWebSocketContainer;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.HttpRequestHandler;
|
||||
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.config.annotation.ServletWebSocketHandlerRegistration;
|
||||
import org.springframework.web.socket.config.annotation.ServletWebSocketHandlerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistration;
|
||||
|
||||
/**
|
||||
* The {@link ServletWebSocketHandlerRegistry} extension for Spring Integration purpose, especially
|
||||
* a dynamic WebSocket endpoint registrations.
|
||||
*
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 5.5
|
||||
*/
|
||||
class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandlerRegistry
|
||||
implements ApplicationContextAware, DestructionAwareBeanPostProcessor {
|
||||
|
||||
private final Map<WebSocketHandler, List<String>> dynamicRegistrations = new HashMap<>();
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private volatile IntegrationDynamicWebSocketHandlerMapping dynamicHandlerMapping;
|
||||
|
||||
IntegrationServletWebSocketHandlerRegistry() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean requiresTaskScheduler() { // NOSONAR visibility
|
||||
return super.requiresTaskScheduler();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setTaskScheduler(TaskScheduler scheduler) { // NOSONAR visibility
|
||||
super.setTaskScheduler(scheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractHandlerMapping getHandlerMapping() {
|
||||
AbstractHandlerMapping originHandlerMapping = super.getHandlerMapping();
|
||||
originHandlerMapping.setApplicationContext(this.applicationContext);
|
||||
this.dynamicHandlerMapping = this.applicationContext.getBean(IntegrationDynamicWebSocketHandlerMapping.class);
|
||||
return originHandlerMapping;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSocketHandlerRegistration addHandler(WebSocketHandler handler, String... paths) {
|
||||
if (this.dynamicHandlerMapping != null) {
|
||||
IntegrationDynamicWebSocketHandlerRegistration registration =
|
||||
new IntegrationDynamicWebSocketHandlerRegistration();
|
||||
registration.addHandler(handler, paths);
|
||||
MultiValueMap<HttpRequestHandler, String> mappings = registration.getMapping();
|
||||
for (Map.Entry<HttpRequestHandler, List<String>> entry : mappings.entrySet()) {
|
||||
HttpRequestHandler httpHandler = entry.getKey();
|
||||
List<String> patterns = entry.getValue();
|
||||
this.dynamicRegistrations.put(handler, patterns);
|
||||
for (String pattern : patterns) {
|
||||
this.dynamicHandlerMapping.registerHandler(pattern, httpHandler);
|
||||
}
|
||||
}
|
||||
return registration;
|
||||
}
|
||||
else {
|
||||
return super.addHandler(handler, paths);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (this.dynamicHandlerMapping != null && bean instanceof ServerWebSocketContainer) {
|
||||
((ServerWebSocketContainer) bean).registerWebSocketHandlers(this);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresDestruction(Object bean) {
|
||||
return bean instanceof ServerWebSocketContainer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
|
||||
if (requiresDestruction(bean)) {
|
||||
removeRegistration((ServerWebSocketContainer) bean);
|
||||
}
|
||||
}
|
||||
|
||||
void removeRegistration(ServerWebSocketContainer serverWebSocketContainer) {
|
||||
List<String> patterns = this.dynamicRegistrations.remove(serverWebSocketContainer.getWebSocketHandler());
|
||||
if (this.dynamicHandlerMapping != null && !CollectionUtils.isEmpty(patterns)) {
|
||||
for (String pattern : patterns) {
|
||||
this.dynamicHandlerMapping.unregisterHandler(pattern);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class IntegrationDynamicWebSocketHandlerRegistration
|
||||
extends ServletWebSocketHandlerRegistration {
|
||||
|
||||
MultiValueMap<HttpRequestHandler, String> getMapping() {
|
||||
return getMappings();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2019 the original author or authors.
|
||||
* Copyright 2014-2021 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.
|
||||
@@ -26,17 +26,14 @@ import org.springframework.beans.factory.config.AbstractFactoryBean;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.integration.config.IntegrationConfigurationInitializer;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
|
||||
import org.springframework.web.socket.config.annotation.DelegatingWebSocketConfiguration;
|
||||
import org.springframework.web.socket.config.annotation.ServletWebSocketHandlerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
|
||||
/**
|
||||
@@ -60,7 +57,7 @@ public class WebSocketIntegrationConfigurationInitializer implements Integration
|
||||
@Override
|
||||
public void initialize(ConfigurableListableBeanFactory beanFactory) throws BeansException {
|
||||
if (beanFactory instanceof BeanDefinitionRegistry) {
|
||||
this.registerEnableWebSocketIfNecessary((BeanDefinitionRegistry) beanFactory);
|
||||
registerEnableWebSocketIfNecessary((BeanDefinitionRegistry) beanFactory);
|
||||
}
|
||||
else {
|
||||
LOGGER.warn("'DelegatingWebSocketConfiguration' isn't registered because 'beanFactory'" +
|
||||
@@ -87,22 +84,30 @@ public class WebSocketIntegrationConfigurationInitializer implements Integration
|
||||
private void registerEnableWebSocketIfNecessary(BeanDefinitionRegistry registry) {
|
||||
if (SERVLET_PRESENT) {
|
||||
if (!registry.containsBeanDefinition("defaultSockJsTaskScheduler")) {
|
||||
BeanDefinitionBuilder sockJsTaskSchedulerBuilder =
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ThreadPoolTaskScheduler.class)
|
||||
.addPropertyValue("threadNamePrefix", "SockJS-")
|
||||
.addPropertyValue("poolSize", Runtime.getRuntime().availableProcessors())
|
||||
.addPropertyValue("removeOnCancelPolicy", true);
|
||||
|
||||
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
|
||||
taskScheduler.setThreadNamePrefix("SockJS-");
|
||||
taskScheduler.setPoolSize(Runtime.getRuntime().availableProcessors());
|
||||
taskScheduler.setRemoveOnCancelPolicy(true);
|
||||
registry.registerBeanDefinition("defaultSockJsTaskScheduler",
|
||||
sockJsTaskSchedulerBuilder.getBeanDefinition());
|
||||
new RootBeanDefinition(ThreadPoolTaskScheduler.class, () -> taskScheduler));
|
||||
}
|
||||
|
||||
if (!registry.containsBeanDefinition(DelegatingWebSocketConfiguration.class.getName()) &&
|
||||
!registry.containsBeanDefinition(WEB_SOCKET_HANDLER_MAPPING_BEAN_NAME)) {
|
||||
|
||||
registry.registerBeanDefinition("integrationServletWebSocketHandlerRegistry",
|
||||
new RootBeanDefinition(IntegrationServletWebSocketHandlerRegistry.class,
|
||||
IntegrationServletWebSocketHandlerRegistry::new));
|
||||
|
||||
BeanDefinitionReaderUtils.registerWithGeneratedName(
|
||||
new RootBeanDefinition(IntegrationDynamicWebSocketHandlerMapping.class,
|
||||
IntegrationDynamicWebSocketHandlerMapping::new),
|
||||
registry);
|
||||
|
||||
BeanDefinitionBuilder enableWebSocketBuilder =
|
||||
BeanDefinitionBuilder.genericBeanDefinition(WebSocketHandlerMappingFactoryBean.class)
|
||||
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
.addPropertyReference("sockJsTaskScheduler", "defaultSockJsTaskScheduler");
|
||||
BeanDefinitionBuilder.genericBeanDefinition(WebSocketHandlerMappingFactoryBean.class,
|
||||
() -> createWebSocketHandlerMapping((BeanFactory) registry))
|
||||
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
|
||||
registry.registerBeanDefinition(WEB_SOCKET_HANDLER_MAPPING_BEAN_NAME,
|
||||
enableWebSocketBuilder.getBeanDefinition());
|
||||
@@ -110,25 +115,22 @@ public class WebSocketIntegrationConfigurationInitializer implements Integration
|
||||
}
|
||||
}
|
||||
|
||||
private static class WebSocketHandlerMappingFactoryBean extends AbstractFactoryBean<HandlerMapping>
|
||||
implements ApplicationContextAware {
|
||||
private static WebSocketHandlerMappingFactoryBean createWebSocketHandlerMapping(BeanFactory beanFactory) {
|
||||
WebSocketHandlerMappingFactoryBean mappingFactoryBean = new WebSocketHandlerMappingFactoryBean();
|
||||
mappingFactoryBean.registry =
|
||||
beanFactory.getBean("integrationServletWebSocketHandlerRegistry",
|
||||
IntegrationServletWebSocketHandlerRegistry.class);
|
||||
mappingFactoryBean.sockJsTaskScheduler =
|
||||
beanFactory.getBean("defaultSockJsTaskScheduler", ThreadPoolTaskScheduler.class);
|
||||
return mappingFactoryBean;
|
||||
}
|
||||
|
||||
private final IntegrationServletWebSocketHandlerRegistry registry =
|
||||
new IntegrationServletWebSocketHandlerRegistry();
|
||||
private static class WebSocketHandlerMappingFactoryBean extends AbstractFactoryBean<HandlerMapping> {
|
||||
|
||||
private IntegrationServletWebSocketHandlerRegistry registry;
|
||||
|
||||
private ThreadPoolTaskScheduler sockJsTaskScheduler;
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
public void setSockJsTaskScheduler(ThreadPoolTaskScheduler sockJsTaskScheduler) {
|
||||
this.sockJsTaskScheduler = sockJsTaskScheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HandlerMapping createInstance() {
|
||||
BeanFactory beanFactory = getBeanFactory();
|
||||
@@ -141,9 +143,7 @@ public class WebSocketIntegrationConfigurationInitializer implements Integration
|
||||
if (this.registry.requiresTaskScheduler()) {
|
||||
this.registry.setTaskScheduler(this.sockJsTaskScheduler);
|
||||
}
|
||||
AbstractHandlerMapping handlerMapping = this.registry.getHandlerMapping();
|
||||
handlerMapping.setApplicationContext(this.applicationContext);
|
||||
return handlerMapping;
|
||||
return this.registry.getHandlerMapping();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,21 +153,4 @@ public class WebSocketIntegrationConfigurationInitializer implements Integration
|
||||
|
||||
}
|
||||
|
||||
private static class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandlerRegistry {
|
||||
|
||||
IntegrationServletWebSocketHandlerRegistry() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresTaskScheduler() { // NOSONAR visibility
|
||||
return super.requiresTaskScheduler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTaskScheduler(TaskScheduler scheduler) { // NOSONAR visibility
|
||||
super.setTaskScheduler(scheduler);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2014-2020 the original author or authors.
|
||||
* Copyright 2014-2021 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.
|
||||
@@ -280,7 +280,7 @@ public class WebSocketInboundChannelAdapter extends MessageProducerSupport
|
||||
public boolean isActive() {
|
||||
boolean active = super.isActive();
|
||||
if (!active) {
|
||||
logger.warn("MessageProducer '" + this + "' isn't started to accept WebSocket events.");
|
||||
logger.warn(() -> "MessageProducer '" + this + "' isn't started to accept WebSocket events.");
|
||||
}
|
||||
return active;
|
||||
}
|
||||
@@ -308,8 +308,8 @@ public class WebSocketInboundChannelAdapter extends MessageProducerSupport
|
||||
if (this.useBroker) {
|
||||
this.brokerHandler.handleMessage(message);
|
||||
}
|
||||
else if (logger.isDebugEnabled()) {
|
||||
logger.debug("Messages with non 'SimpMessageType.MESSAGE' type are ignored for sending to the " +
|
||||
else {
|
||||
logger.debug(() -> "Messages with non 'SimpMessageType.MESSAGE' type are ignored for sending to the " +
|
||||
"'outputChannel'. They have to be emitted as 'ApplicationEvent's " +
|
||||
"from the 'SubProtocolHandler'. Or using 'AbstractBrokerMessageHandler'(useBroker = true) " +
|
||||
"from server side. Received message: " + message);
|
||||
|
||||
Reference in New Issue
Block a user