Skip to content

Commit 93743f6

Browse files
authored
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
1 parent 8348b91 commit 93743f6

File tree

5 files changed

+110
-29
lines changed

5 files changed

+110
-29
lines changed

spring-integration-websocket/src/main/java/org/springframework/integration/websocket/ServerWebSocketContainer.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014-2020 the original author or authors.
2+
* Copyright 2014-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,7 +37,7 @@
3737

3838
/**
3939
* The {@link IntegrationWebSocketContainer} implementation for the {@code server}
40-
* {@link org.springframework.web.socket.WebSocketHandler} registration.
40+
* {@link WebSocketHandler} registration.
4141
* <p>
4242
* Registers an internal {@code IntegrationWebSocketContainer.IntegrationWebSocketHandler}
4343
* for provided {@link #paths} with the {@link WebSocketHandlerRegistry}.
@@ -69,6 +69,8 @@ public class ServerWebSocketContainer extends IntegrationWebSocketContainer
6969

7070
private int phase = 0;
7171

72+
private TaskScheduler sockJsTaskScheduler;
73+
7274
public ServerWebSocketContainer(String... paths) {
7375
Assert.notEmpty(paths, "'paths' must not be empty");
7476
this.paths = Arrays.copyOf(paths, paths.length);
@@ -118,11 +120,11 @@ public ServerWebSocketContainer setAllowedOrigins(String... origins) {
118120

119121
public ServerWebSocketContainer withSockJs(SockJsServiceOptions... sockJsServiceOptions) {
120122
if (ObjectUtils.isEmpty(sockJsServiceOptions)) {
121-
this.sockJsServiceOptions = new SockJsServiceOptions();
123+
setSockJsServiceOptions(new SockJsServiceOptions());
122124
}
123125
else {
124126
Assert.state(sockJsServiceOptions.length == 1, "Only one 'sockJsServiceOptions' is applicable.");
125-
this.sockJsServiceOptions = sockJsServiceOptions[0];
127+
setSockJsServiceOptions(sockJsServiceOptions[0]);
126128
}
127129
return this;
128130
}
@@ -131,6 +133,21 @@ public void setSockJsServiceOptions(SockJsServiceOptions sockJsServiceOptions) {
131133
this.sockJsServiceOptions = sockJsServiceOptions;
132134
}
133135

136+
/**
137+
* Configure a {@link TaskScheduler} for SockJS fallback service.
138+
* This is an alternative for default SockJS service scheduler
139+
* when Websocket endpoint (this server container) is registered at runtime.
140+
* @param sockJsTaskScheduler the {@link TaskScheduler} for SockJS fallback service.
141+
* @since 5.5.1
142+
*/
143+
public void setSockJsTaskScheduler(TaskScheduler sockJsTaskScheduler) {
144+
this.sockJsTaskScheduler = sockJsTaskScheduler;
145+
}
146+
147+
public TaskScheduler getSockJsTaskScheduler() {
148+
return this.sockJsTaskScheduler;
149+
}
150+
134151
@Override
135152
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
136153
WebSocketHandler webSocketHandler = this.webSocketHandler;
@@ -153,6 +170,8 @@ private void configureSockJsOptionsIfAny(WebSocketHandlerRegistration registrati
153170
if (this.sockJsServiceOptions != null) {
154171
SockJsServiceRegistration sockJsServiceRegistration = registration.withSockJS();
155172
JavaUtils.INSTANCE
173+
.acceptIfCondition(this.sockJsServiceOptions.taskScheduler == null,
174+
this.sockJsTaskScheduler, this.sockJsServiceOptions::setTaskScheduler)
156175
.acceptIfNotNull(this.sockJsServiceOptions.webSocketEnabled,
157176
sockJsServiceRegistration::setWebSocketEnabled)
158177
.acceptIfNotNull(this.sockJsServiceOptions.clientLibraryUrl,
@@ -226,7 +245,7 @@ public void stop(Runnable callback) {
226245
}
227246

228247
/**
229-
* @see org.springframework.web.socket.config.annotation.SockJsServiceRegistration
248+
* @see SockJsServiceRegistration
230249
*/
231250
public static class SockJsServiceOptions {
232251

spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/IntegrationDynamicWebSocketHandlerMapping.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,23 @@
1616

1717
package org.springframework.integration.websocket.config;
1818

19+
import java.util.ArrayList;
1920
import java.util.HashMap;
21+
import java.util.LinkedHashMap;
22+
import java.util.List;
2023
import java.util.Map;
2124

2225
import javax.servlet.http.HttpServletRequest;
2326

27+
import org.springframework.http.server.PathContainer;
28+
import org.springframework.http.server.RequestPath;
2429
import org.springframework.web.HttpRequestHandler;
2530
import org.springframework.web.servlet.HandlerExecutionChain;
2631
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
32+
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
33+
import org.springframework.web.util.ServletRequestPathUtils;
34+
import org.springframework.web.util.pattern.PathPattern;
35+
import org.springframework.web.util.pattern.PathPatternParser;
2736

2837
/**
2938
* The {@link AbstractHandlerMapping} implementation for dynamic WebSocket endpoint registrations in Spring Integration.
@@ -34,23 +43,60 @@
3443
*
3544
* @since 5.5
3645
*/
37-
class IntegrationDynamicWebSocketHandlerMapping extends AbstractHandlerMapping {
46+
class IntegrationDynamicWebSocketHandlerMapping extends AbstractUrlHandlerMapping {
3847

3948
private final Map<String, HttpRequestHandler> handlerMap = new HashMap<>();
4049

50+
private final Map<PathPattern, HttpRequestHandler> pathPatternHandlerMap = new LinkedHashMap<>();
51+
4152
@Override
4253
protected Object getHandlerInternal(HttpServletRequest request) {
4354
String lookupPath = initLookupPath(request);
4455
HttpRequestHandler httpRequestHandler = this.handlerMap.get(lookupPath);
56+
if (httpRequestHandler == null && usesPathPatterns()) {
57+
RequestPath path = ServletRequestPathUtils.getParsedRequestPath(request);
58+
return lookupByPattern(path);
59+
}
4560
return httpRequestHandler != null ? new HandlerExecutionChain(httpRequestHandler) : null;
4661
}
4762

63+
private Object lookupByPattern(RequestPath path) {
64+
List<PathPattern> matches = null;
65+
for (PathPattern pattern : this.pathPatternHandlerMap.keySet()) {
66+
if (pattern.matches(path.pathWithinApplication())) {
67+
matches = (matches != null ? matches : new ArrayList<>());
68+
matches.add(pattern);
69+
}
70+
}
71+
if (matches == null) {
72+
return null;
73+
}
74+
if (matches.size() > 1) {
75+
matches.sort(PathPattern.SPECIFICITY_COMPARATOR);
76+
if (logger.isTraceEnabled()) {
77+
logger.trace("Matching patterns " + matches);
78+
}
79+
}
80+
PathPattern pattern = matches.get(0);
81+
HttpRequestHandler handler = this.pathPatternHandlerMap.get(pattern);
82+
PathContainer pathWithinMapping = pattern.extractPathWithinPattern(path.pathWithinApplication());
83+
return buildPathExposingHandler(handler, pattern.getPatternString(), pathWithinMapping.value(), null);
84+
}
85+
4886
void registerHandler(String path, HttpRequestHandler httpHandler) {
4987
this.handlerMap.put(path, httpHandler);
88+
PathPatternParser patternParser = getPatternParser();
89+
if (patternParser != null) {
90+
this.pathPatternHandlerMap.put(patternParser.parse(path), httpHandler);
91+
}
5092
}
5193

5294
void unregisterHandler(String path) {
5395
this.handlerMap.remove(path);
96+
PathPatternParser patternParser = getPatternParser();
97+
if (patternParser != null) {
98+
this.pathPatternHandlerMap.remove(patternParser.parse(path));
99+
}
54100
}
55101

56102
}

spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/IntegrationServletWebSocketHandlerRegistry.java

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,14 @@
4646
class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandlerRegistry
4747
implements ApplicationContextAware, DestructionAwareBeanPostProcessor {
4848

49+
private final ThreadLocal<IntegrationDynamicWebSocketHandlerRegistration> currentRegistration = new ThreadLocal<>();
50+
4951
private final Map<WebSocketHandler, List<String>> dynamicRegistrations = new HashMap<>();
5052

5153
private ApplicationContext applicationContext;
5254

55+
private TaskScheduler sockJsTaskScheduler;
56+
5357
private volatile IntegrationDynamicWebSocketHandlerMapping dynamicHandlerMapping;
5458

5559
IntegrationServletWebSocketHandlerRegistry() {
@@ -60,15 +64,10 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
6064
this.applicationContext = applicationContext;
6165
}
6266

63-
6467
@Override
65-
protected boolean requiresTaskScheduler() { // NOSONAR visibility
66-
return super.requiresTaskScheduler();
67-
}
68-
69-
@Override
70-
protected void setTaskScheduler(TaskScheduler scheduler) { // NOSONAR visibility
68+
protected void setTaskScheduler(TaskScheduler scheduler) {
7169
super.setTaskScheduler(scheduler);
70+
this.sockJsTaskScheduler = scheduler;
7271
}
7372

7473
@Override
@@ -85,15 +84,7 @@ public WebSocketHandlerRegistration addHandler(WebSocketHandler handler, String.
8584
IntegrationDynamicWebSocketHandlerRegistration registration =
8685
new IntegrationDynamicWebSocketHandlerRegistration();
8786
registration.addHandler(handler, paths);
88-
MultiValueMap<HttpRequestHandler, String> mappings = registration.getMapping();
89-
for (Map.Entry<HttpRequestHandler, List<String>> entry : mappings.entrySet()) {
90-
HttpRequestHandler httpHandler = entry.getKey();
91-
List<String> patterns = entry.getValue();
92-
this.dynamicRegistrations.put(handler, patterns);
93-
for (String pattern : patterns) {
94-
this.dynamicHandlerMapping.registerHandler(pattern, httpHandler);
95-
}
96-
}
87+
this.currentRegistration.set(registration);
9788
return registration;
9889
}
9990
else {
@@ -102,9 +93,24 @@ public WebSocketHandlerRegistration addHandler(WebSocketHandler handler, String.
10293
}
10394

10495
@Override
105-
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
96+
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
10697
if (this.dynamicHandlerMapping != null && bean instanceof ServerWebSocketContainer) {
107-
((ServerWebSocketContainer) bean).registerWebSocketHandlers(this);
98+
ServerWebSocketContainer serverWebSocketContainer = (ServerWebSocketContainer) bean;
99+
if (serverWebSocketContainer.getSockJsTaskScheduler() == null) {
100+
serverWebSocketContainer.setSockJsTaskScheduler(this.sockJsTaskScheduler);
101+
}
102+
serverWebSocketContainer.registerWebSocketHandlers(this);
103+
IntegrationDynamicWebSocketHandlerRegistration registration = this.currentRegistration.get();
104+
this.currentRegistration.remove();
105+
MultiValueMap<HttpRequestHandler, String> mappings = registration.getMapping();
106+
for (Map.Entry<HttpRequestHandler, List<String>> entry : mappings.entrySet()) {
107+
HttpRequestHandler httpHandler = entry.getKey();
108+
List<String> patterns = entry.getValue();
109+
this.dynamicRegistrations.put(registration.handler, patterns);
110+
for (String pattern : patterns) {
111+
this.dynamicHandlerMapping.registerHandler(pattern, httpHandler);
112+
}
113+
}
108114
}
109115
return bean;
110116
}
@@ -133,6 +139,15 @@ void removeRegistration(ServerWebSocketContainer serverWebSocketContainer) {
133139
private static final class IntegrationDynamicWebSocketHandlerRegistration
134140
extends ServletWebSocketHandlerRegistration {
135141

142+
private WebSocketHandler handler;
143+
144+
@Override
145+
public WebSocketHandlerRegistration addHandler(WebSocketHandler handler, String... paths) {
146+
// The IntegrationWebSocketContainer comes only with a single WebSocketHandler
147+
this.handler = handler;
148+
return super.addHandler(handler, paths);
149+
}
150+
136151
MultiValueMap<HttpRequestHandler, String> getMapping() {
137152
return getMappings();
138153
}

spring-integration-websocket/src/main/java/org/springframework/integration/websocket/config/WebSocketIntegrationConfigurationInitializer.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.web.servlet.HandlerMapping;
3636
import org.springframework.web.socket.config.annotation.DelegatingWebSocketConfiguration;
3737
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
38+
import org.springframework.web.util.pattern.PathPatternParser;
3839

3940
/**
4041
* The WebSocket Integration infrastructure {@code beanFactory} initializer.
@@ -102,6 +103,7 @@ private void registerEnableWebSocketIfNecessary(BeanDefinitionRegistry registry)
102103
() -> {
103104
IntegrationDynamicWebSocketHandlerMapping dynamicWebSocketHandlerMapping =
104105
new IntegrationDynamicWebSocketHandlerMapping();
106+
dynamicWebSocketHandlerMapping.setPatternParser(new PathPatternParser());
105107
dynamicWebSocketHandlerMapping.setOrder(0);
106108
return dynamicWebSocketHandlerMapping;
107109
}),
@@ -143,9 +145,7 @@ protected HandlerMapping createInstance() {
143145
.values()
144146
.forEach(configurer -> configurer.registerWebSocketHandlers(this.registry));
145147
}
146-
if (this.registry.requiresTaskScheduler()) {
147-
this.registry.setTaskScheduler(this.sockJsTaskScheduler);
148-
}
148+
this.registry.setTaskScheduler(this.sockJsTaskScheduler);
149149
return this.registry.getHandlerMapping();
150150
}
151151

spring-integration-websocket/src/test/java/org/springframework/integration/websocket/dsl/WebSocketDslTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ void testDynamicServerEndpointRegistration() {
6969
IntegrationFlowContext serverIntegrationFlowContext = serverContext.getBean(IntegrationFlowContext.class);
7070
ServerWebSocketContainer serverWebSocketContainer =
7171
new ServerWebSocketContainer("/dynamic")
72-
.setHandshakeHandler(serverContext.getBean(HandshakeHandler.class));
72+
.setHandshakeHandler(serverContext.getBean(HandshakeHandler.class))
73+
.withSockJs();
7374

7475
WebSocketInboundChannelAdapter webSocketInboundChannelAdapter =
7576
new WebSocketInboundChannelAdapter(serverWebSocketContainer);
@@ -88,7 +89,7 @@ void testDynamicServerEndpointRegistration() {
8889

8990
// Dynamic client flow
9091
ClientWebSocketContainer clientWebSocketContainer =
91-
new ClientWebSocketContainer(this.webSocketClient, this.server.getWsBaseUrl() + "/dynamic");
92+
new ClientWebSocketContainer(this.webSocketClient, this.server.getWsBaseUrl() + "/dynamic/websocket");
9293
clientWebSocketContainer.setAutoStartup(true);
9394
WebSocketOutboundMessageHandler webSocketOutboundMessageHandler =
9495
new WebSocketOutboundMessageHandler(clientWebSocketContainer);

0 commit comments

Comments
 (0)