Fix dynamic Websocket endpoints for SockJS (#3581)
* Fix dynamic Websocket endpoints for SockJS Related to https://stackoverflow.com/questions/67971467/registration-of-dynamic-websocket-at-application-initialization-time-and-at-runt The SockJS wrapper for dynamic endpoint is not initialized properly. Technically we just don't map to the SockJS service if such one is requested from the `ServerWebSocketContainer` configuration * Postpone the path mapping for the target endpoint until after the `ServerWebSocketContainer` applies all the options into its registration to expose. * Fix `ServerWebSocketContainer` to propagate a default `TaskScheduler` for underlying SockJS Service on the endpoint * Fix `IntegrationDynamicWebSocketHandlerMapping` to deal with path patterns as well, which is the case for the mentioned SockJS wrapper: the SockJS Service is able to handle the rest of the path according its setting and request requirements * * Fix unused imports * Add Javadoc for new `ServerWebSocketContainer.setSockJsTaskScheduler()` API * * Cover SockJS server configuration in the WebSocketDslTests
This commit is contained in:
@@ -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.
|
||||
@@ -37,7 +37,7 @@ import org.springframework.web.socket.sockjs.transport.TransportHandler;
|
||||
|
||||
/**
|
||||
* The {@link IntegrationWebSocketContainer} implementation for the {@code server}
|
||||
* {@link org.springframework.web.socket.WebSocketHandler} registration.
|
||||
* {@link WebSocketHandler} registration.
|
||||
* <p>
|
||||
* Registers an internal {@code IntegrationWebSocketContainer.IntegrationWebSocketHandler}
|
||||
* for provided {@link #paths} with the {@link WebSocketHandlerRegistry}.
|
||||
@@ -69,6 +69,8 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer
|
||||
|
||||
private int phase = 0;
|
||||
|
||||
private TaskScheduler sockJsTaskScheduler;
|
||||
|
||||
public ServerWebSocketContainer(String... paths) {
|
||||
Assert.notEmpty(paths, "'paths' must not be empty");
|
||||
this.paths = Arrays.copyOf(paths, paths.length);
|
||||
@@ -118,11 +120,11 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer
|
||||
|
||||
public ServerWebSocketContainer withSockJs(SockJsServiceOptions... sockJsServiceOptions) {
|
||||
if (ObjectUtils.isEmpty(sockJsServiceOptions)) {
|
||||
this.sockJsServiceOptions = new SockJsServiceOptions();
|
||||
setSockJsServiceOptions(new SockJsServiceOptions());
|
||||
}
|
||||
else {
|
||||
Assert.state(sockJsServiceOptions.length == 1, "Only one 'sockJsServiceOptions' is applicable.");
|
||||
this.sockJsServiceOptions = sockJsServiceOptions[0];
|
||||
setSockJsServiceOptions(sockJsServiceOptions[0]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -131,6 +133,21 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer
|
||||
this.sockJsServiceOptions = sockJsServiceOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link TaskScheduler} for SockJS fallback service.
|
||||
* This is an alternative for default SockJS service scheduler
|
||||
* when Websocket endpoint (this server container) is registered at runtime.
|
||||
* @param sockJsTaskScheduler the {@link TaskScheduler} for SockJS fallback service.
|
||||
* @since 5.5.1
|
||||
*/
|
||||
public void setSockJsTaskScheduler(TaskScheduler sockJsTaskScheduler) {
|
||||
this.sockJsTaskScheduler = sockJsTaskScheduler;
|
||||
}
|
||||
|
||||
public TaskScheduler getSockJsTaskScheduler() {
|
||||
return this.sockJsTaskScheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
WebSocketHandler webSocketHandler = this.webSocketHandler;
|
||||
@@ -153,6 +170,8 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer
|
||||
if (this.sockJsServiceOptions != null) {
|
||||
SockJsServiceRegistration sockJsServiceRegistration = registration.withSockJS();
|
||||
JavaUtils.INSTANCE
|
||||
.acceptIfCondition(this.sockJsServiceOptions.taskScheduler == null,
|
||||
this.sockJsTaskScheduler, this.sockJsServiceOptions::setTaskScheduler)
|
||||
.acceptIfNotNull(this.sockJsServiceOptions.webSocketEnabled,
|
||||
sockJsServiceRegistration::setWebSocketEnabled)
|
||||
.acceptIfNotNull(this.sockJsServiceOptions.clientLibraryUrl,
|
||||
@@ -226,7 +245,7 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer
|
||||
}
|
||||
|
||||
/**
|
||||
* @see org.springframework.web.socket.config.annotation.SockJsServiceRegistration
|
||||
* @see SockJsServiceRegistration
|
||||
*/
|
||||
public static class SockJsServiceOptions {
|
||||
|
||||
|
||||
@@ -16,14 +16,23 @@
|
||||
|
||||
package org.springframework.integration.websocket.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.server.PathContainer;
|
||||
import org.springframework.http.server.RequestPath;
|
||||
import org.springframework.web.HttpRequestHandler;
|
||||
import org.springframework.web.servlet.HandlerExecutionChain;
|
||||
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
|
||||
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
import org.springframework.web.util.pattern.PathPattern;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* The {@link AbstractHandlerMapping} implementation for dynamic WebSocket endpoint registrations in Spring Integration.
|
||||
@@ -34,23 +43,60 @@ import org.springframework.web.servlet.handler.AbstractHandlerMapping;
|
||||
*
|
||||
* @since 5.5
|
||||
*/
|
||||
class IntegrationDynamicWebSocketHandlerMapping extends AbstractHandlerMapping {
|
||||
class IntegrationDynamicWebSocketHandlerMapping extends AbstractUrlHandlerMapping {
|
||||
|
||||
private final Map<String, HttpRequestHandler> handlerMap = new HashMap<>();
|
||||
|
||||
private final Map<PathPattern, HttpRequestHandler> pathPatternHandlerMap = new LinkedHashMap<>();
|
||||
|
||||
@Override
|
||||
protected Object getHandlerInternal(HttpServletRequest request) {
|
||||
String lookupPath = initLookupPath(request);
|
||||
HttpRequestHandler httpRequestHandler = this.handlerMap.get(lookupPath);
|
||||
if (httpRequestHandler == null && usesPathPatterns()) {
|
||||
RequestPath path = ServletRequestPathUtils.getParsedRequestPath(request);
|
||||
return lookupByPattern(path);
|
||||
}
|
||||
return httpRequestHandler != null ? new HandlerExecutionChain(httpRequestHandler) : null;
|
||||
}
|
||||
|
||||
private Object lookupByPattern(RequestPath path) {
|
||||
List<PathPattern> matches = null;
|
||||
for (PathPattern pattern : this.pathPatternHandlerMap.keySet()) {
|
||||
if (pattern.matches(path.pathWithinApplication())) {
|
||||
matches = (matches != null ? matches : new ArrayList<>());
|
||||
matches.add(pattern);
|
||||
}
|
||||
}
|
||||
if (matches == null) {
|
||||
return null;
|
||||
}
|
||||
if (matches.size() > 1) {
|
||||
matches.sort(PathPattern.SPECIFICITY_COMPARATOR);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Matching patterns " + matches);
|
||||
}
|
||||
}
|
||||
PathPattern pattern = matches.get(0);
|
||||
HttpRequestHandler handler = this.pathPatternHandlerMap.get(pattern);
|
||||
PathContainer pathWithinMapping = pattern.extractPathWithinPattern(path.pathWithinApplication());
|
||||
return buildPathExposingHandler(handler, pattern.getPatternString(), pathWithinMapping.value(), null);
|
||||
}
|
||||
|
||||
void registerHandler(String path, HttpRequestHandler httpHandler) {
|
||||
this.handlerMap.put(path, httpHandler);
|
||||
PathPatternParser patternParser = getPatternParser();
|
||||
if (patternParser != null) {
|
||||
this.pathPatternHandlerMap.put(patternParser.parse(path), httpHandler);
|
||||
}
|
||||
}
|
||||
|
||||
void unregisterHandler(String path) {
|
||||
this.handlerMap.remove(path);
|
||||
PathPatternParser patternParser = getPatternParser();
|
||||
if (patternParser != null) {
|
||||
this.pathPatternHandlerMap.remove(patternParser.parse(path));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,10 +46,14 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistra
|
||||
class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandlerRegistry
|
||||
implements ApplicationContextAware, DestructionAwareBeanPostProcessor {
|
||||
|
||||
private final ThreadLocal<IntegrationDynamicWebSocketHandlerRegistration> currentRegistration = new ThreadLocal<>();
|
||||
|
||||
private final Map<WebSocketHandler, List<String>> dynamicRegistrations = new HashMap<>();
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private TaskScheduler sockJsTaskScheduler;
|
||||
|
||||
private volatile IntegrationDynamicWebSocketHandlerMapping dynamicHandlerMapping;
|
||||
|
||||
IntegrationServletWebSocketHandlerRegistry() {
|
||||
@@ -60,15 +64,10 @@ class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandler
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected boolean requiresTaskScheduler() { // NOSONAR visibility
|
||||
return super.requiresTaskScheduler();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setTaskScheduler(TaskScheduler scheduler) { // NOSONAR visibility
|
||||
protected void setTaskScheduler(TaskScheduler scheduler) {
|
||||
super.setTaskScheduler(scheduler);
|
||||
this.sockJsTaskScheduler = scheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -85,15 +84,7 @@ class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandler
|
||||
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);
|
||||
}
|
||||
}
|
||||
this.currentRegistration.set(registration);
|
||||
return registration;
|
||||
}
|
||||
else {
|
||||
@@ -102,9 +93,24 @@ class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandler
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (this.dynamicHandlerMapping != null && bean instanceof ServerWebSocketContainer) {
|
||||
((ServerWebSocketContainer) bean).registerWebSocketHandlers(this);
|
||||
ServerWebSocketContainer serverWebSocketContainer = (ServerWebSocketContainer) bean;
|
||||
if (serverWebSocketContainer.getSockJsTaskScheduler() == null) {
|
||||
serverWebSocketContainer.setSockJsTaskScheduler(this.sockJsTaskScheduler);
|
||||
}
|
||||
serverWebSocketContainer.registerWebSocketHandlers(this);
|
||||
IntegrationDynamicWebSocketHandlerRegistration registration = this.currentRegistration.get();
|
||||
this.currentRegistration.remove();
|
||||
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(registration.handler, patterns);
|
||||
for (String pattern : patterns) {
|
||||
this.dynamicHandlerMapping.registerHandler(pattern, httpHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
@@ -133,6 +139,15 @@ class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandler
|
||||
private static final class IntegrationDynamicWebSocketHandlerRegistration
|
||||
extends ServletWebSocketHandlerRegistration {
|
||||
|
||||
private WebSocketHandler handler;
|
||||
|
||||
@Override
|
||||
public WebSocketHandlerRegistration addHandler(WebSocketHandler handler, String... paths) {
|
||||
// The IntegrationWebSocketContainer comes only with a single WebSocketHandler
|
||||
this.handler = handler;
|
||||
return super.addHandler(handler, paths);
|
||||
}
|
||||
|
||||
MultiValueMap<HttpRequestHandler, String> getMapping() {
|
||||
return getMappings();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.socket.config.annotation.DelegatingWebSocketConfiguration;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.util.pattern.PathPatternParser;
|
||||
|
||||
/**
|
||||
* The WebSocket Integration infrastructure {@code beanFactory} initializer.
|
||||
@@ -102,6 +103,7 @@ public class WebSocketIntegrationConfigurationInitializer implements Integration
|
||||
() -> {
|
||||
IntegrationDynamicWebSocketHandlerMapping dynamicWebSocketHandlerMapping =
|
||||
new IntegrationDynamicWebSocketHandlerMapping();
|
||||
dynamicWebSocketHandlerMapping.setPatternParser(new PathPatternParser());
|
||||
dynamicWebSocketHandlerMapping.setOrder(0);
|
||||
return dynamicWebSocketHandlerMapping;
|
||||
}),
|
||||
@@ -143,9 +145,7 @@ public class WebSocketIntegrationConfigurationInitializer implements Integration
|
||||
.values()
|
||||
.forEach(configurer -> configurer.registerWebSocketHandlers(this.registry));
|
||||
}
|
||||
if (this.registry.requiresTaskScheduler()) {
|
||||
this.registry.setTaskScheduler(this.sockJsTaskScheduler);
|
||||
}
|
||||
this.registry.setTaskScheduler(this.sockJsTaskScheduler);
|
||||
return this.registry.getHandlerMapping();
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,8 @@ public class WebSocketDslTests {
|
||||
IntegrationFlowContext serverIntegrationFlowContext = serverContext.getBean(IntegrationFlowContext.class);
|
||||
ServerWebSocketContainer serverWebSocketContainer =
|
||||
new ServerWebSocketContainer("/dynamic")
|
||||
.setHandshakeHandler(serverContext.getBean(HandshakeHandler.class));
|
||||
.setHandshakeHandler(serverContext.getBean(HandshakeHandler.class))
|
||||
.withSockJs();
|
||||
|
||||
WebSocketInboundChannelAdapter webSocketInboundChannelAdapter =
|
||||
new WebSocketInboundChannelAdapter(serverWebSocketContainer);
|
||||
@@ -88,7 +89,7 @@ public class WebSocketDslTests {
|
||||
|
||||
// Dynamic client flow
|
||||
ClientWebSocketContainer clientWebSocketContainer =
|
||||
new ClientWebSocketContainer(this.webSocketClient, this.server.getWsBaseUrl() + "/dynamic");
|
||||
new ClientWebSocketContainer(this.webSocketClient, this.server.getWsBaseUrl() + "/dynamic/websocket");
|
||||
clientWebSocketContainer.setAutoStartup(true);
|
||||
WebSocketOutboundMessageHandler webSocketOutboundMessageHandler =
|
||||
new WebSocketOutboundMessageHandler(clientWebSocketContainer);
|
||||
|
||||
Reference in New Issue
Block a user