Skip to content

Commit dd6cffd

Browse files
committed
spring-projectsGH-3533: Register WebSocket endpoints at runtime
Fixes spring-projects#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
1 parent c13e40d commit dd6cffd

File tree

11 files changed

+450
-63
lines changed

11 files changed

+450
-63
lines changed

build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,7 @@ project('spring-integration-websocket') {
825825
api project(':spring-integration-core')
826826
api 'org.springframework:spring-websocket'
827827
optionalApi 'org.springframework:spring-webmvc'
828+
providedImplementation "javax.servlet:javax.servlet-api:$servletApiVersion"
828829

829830
testImplementation project(':spring-integration-event')
830831
testImplementation "org.apache.tomcat.embed:tomcat-embed-websocket:$tomcatVersion"

spring-integration-core/src/main/java/org/springframework/integration/dsl/context/StandardIntegrationFlowContext.java

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2016-2020 the original author or authors.
2+
* Copyright 2016-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.
@@ -31,6 +31,7 @@
3131
import org.springframework.beans.factory.support.AbstractBeanDefinition;
3232
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
3333
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
34+
import org.springframework.context.SmartLifecycle;
3435
import org.springframework.core.type.MethodMetadata;
3536
import org.springframework.integration.core.MessagingTemplate;
3637
import org.springframework.integration.dsl.IntegrationFlow;
@@ -121,9 +122,14 @@ else if (this.registry.containsKey(flowId)) {
121122
final String theFlowId = flowId;
122123
builder.additionalBeans.forEach((key, value) -> registerBean(key, value, theFlowId));
123124

124-
IntegrationFlowRegistration registration = new StandardIntegrationFlowRegistration(integrationFlow, this, flowId);
125+
IntegrationFlowRegistration registration =
126+
new StandardIntegrationFlowRegistration(integrationFlow, this, flowId);
125127
if (builder.autoStartup) {
126128
registration.start();
129+
builder.additionalBeans.keySet()
130+
.stream()
131+
.filter(SmartLifecycle.class::isInstance)
132+
.forEach((lifecycle) -> ((SmartLifecycle) lifecycle).start());
127133
}
128134
this.registry.put(flowId, registration);
129135

spring-integration-core/src/main/java/org/springframework/integration/dsl/context/StandardIntegrationFlowRegistration.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2018-2020 the original author or authors.
2+
* Copyright 2018-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.
@@ -52,7 +52,9 @@ class StandardIntegrationFlowRegistration implements IntegrationFlowRegistration
5252

5353
private ConfigurableListableBeanFactory beanFactory;
5454

55-
StandardIntegrationFlowRegistration(IntegrationFlow integrationFlow, IntegrationFlowContext integrationFlowContext, String id) {
55+
StandardIntegrationFlowRegistration(IntegrationFlow integrationFlow, IntegrationFlowContext integrationFlowContext,
56+
String id) {
57+
5658
this.integrationFlow = integrationFlow;
5759
this.integrationFlowContext = integrationFlowContext;
5860
this.id = id;
@@ -81,7 +83,7 @@ public MessageChannel getInputChannel() {
8183
throw new IllegalStateException("Only 'IntegrationFlow' instances started from the 'MessageChannel' " +
8284
"(e.g. extracted from 'IntegrationFlow' Lambdas) can be used " +
8385
"for direct 'send' operation. " +
84-
"But [" + this.integrationFlow + "] ins't one of them.\n" +
86+
"But [" + this.integrationFlow + "] isn't one of them.\n" +
8587
"Consider 'BeanFactory.getBean()' usage for sending messages " +
8688
"to the required 'MessageChannel'.");
8789
}

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

+4
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ public void addSupportedProtocols(String... protocols) {
104104
}
105105
}
106106

107+
public WebSocketHandler getWebSocketHandler() {
108+
return this.webSocketHandler;
109+
}
110+
107111
public List<String> getSubProtocols() {
108112
List<String> protocols = new ArrayList<>();
109113
if (this.messageListener != null) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.websocket.config;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
22+
import javax.servlet.http.HttpServletRequest;
23+
24+
import org.springframework.web.HttpRequestHandler;
25+
import org.springframework.web.servlet.HandlerExecutionChain;
26+
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
27+
28+
/**
29+
* The {@link AbstractHandlerMapping} implementation for dynamic WebSocket endpoint registrations in Spring Integration.
30+
* <p>
31+
* TODO until https://github.com/spring-projects/spring-framework/issues/26798
32+
*
33+
* @author Artem Bilan
34+
*
35+
* @since 5.5
36+
*/
37+
class IntegrationDynamicWebSocketHandlerMapping extends AbstractHandlerMapping {
38+
39+
private final Map<String, HttpRequestHandler> handlerMap = new HashMap<>();
40+
41+
@Override
42+
protected Object getHandlerInternal(HttpServletRequest request) {
43+
String lookupPath = initLookupPath(request);
44+
HttpRequestHandler httpRequestHandler = this.handlerMap.get(lookupPath);
45+
return httpRequestHandler != null ? new HandlerExecutionChain(httpRequestHandler) : null;
46+
}
47+
48+
void registerHandler(String path, HttpRequestHandler httpHandler) {
49+
this.handlerMap.put(path, httpHandler);
50+
}
51+
52+
void unregisterHandler(String path) {
53+
this.handlerMap.remove(path);
54+
}
55+
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.websocket.config;
18+
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
import org.springframework.beans.BeansException;
24+
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
25+
import org.springframework.context.ApplicationContext;
26+
import org.springframework.context.ApplicationContextAware;
27+
import org.springframework.integration.websocket.ServerWebSocketContainer;
28+
import org.springframework.scheduling.TaskScheduler;
29+
import org.springframework.util.CollectionUtils;
30+
import org.springframework.util.MultiValueMap;
31+
import org.springframework.web.HttpRequestHandler;
32+
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
33+
import org.springframework.web.socket.WebSocketHandler;
34+
import org.springframework.web.socket.config.annotation.ServletWebSocketHandlerRegistration;
35+
import org.springframework.web.socket.config.annotation.ServletWebSocketHandlerRegistry;
36+
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistration;
37+
38+
/**
39+
* The {@link ServletWebSocketHandlerRegistry} extension for Spring Integration purpose, especially
40+
* a dynamic WebSocket endpoint registrations.
41+
*
42+
* @author Artem Bilan
43+
*
44+
* @since 5.5
45+
*/
46+
class IntegrationServletWebSocketHandlerRegistry extends ServletWebSocketHandlerRegistry
47+
implements ApplicationContextAware, DestructionAwareBeanPostProcessor {
48+
49+
private final Map<WebSocketHandler, List<String>> dynamicRegistrations = new HashMap<>();
50+
51+
private ApplicationContext applicationContext;
52+
53+
private volatile IntegrationDynamicWebSocketHandlerMapping dynamicHandlerMapping;
54+
55+
IntegrationServletWebSocketHandlerRegistry() {
56+
}
57+
58+
@Override
59+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
60+
this.applicationContext = applicationContext;
61+
}
62+
63+
64+
@Override
65+
protected boolean requiresTaskScheduler() { // NOSONAR visibility
66+
return super.requiresTaskScheduler();
67+
}
68+
69+
@Override
70+
protected void setTaskScheduler(TaskScheduler scheduler) { // NOSONAR visibility
71+
super.setTaskScheduler(scheduler);
72+
}
73+
74+
@Override
75+
public AbstractHandlerMapping getHandlerMapping() {
76+
AbstractHandlerMapping originHandlerMapping = super.getHandlerMapping();
77+
originHandlerMapping.setApplicationContext(this.applicationContext);
78+
this.dynamicHandlerMapping = this.applicationContext.getBean(IntegrationDynamicWebSocketHandlerMapping.class);
79+
return originHandlerMapping;
80+
}
81+
82+
@Override
83+
public WebSocketHandlerRegistration addHandler(WebSocketHandler handler, String... paths) {
84+
if (this.dynamicHandlerMapping != null) {
85+
IntegrationDynamicWebSocketHandlerRegistration registration =
86+
new IntegrationDynamicWebSocketHandlerRegistration();
87+
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+
}
97+
return registration;
98+
}
99+
else {
100+
return super.addHandler(handler, paths);
101+
}
102+
}
103+
104+
@Override
105+
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
106+
if (this.dynamicHandlerMapping != null && bean instanceof ServerWebSocketContainer) {
107+
((ServerWebSocketContainer) bean).registerWebSocketHandlers(this);
108+
}
109+
return bean;
110+
}
111+
112+
@Override
113+
public boolean requiresDestruction(Object bean) {
114+
return bean instanceof ServerWebSocketContainer;
115+
}
116+
117+
@Override
118+
public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
119+
if (requiresDestruction(bean)) {
120+
removeRegistration((ServerWebSocketContainer) bean);
121+
}
122+
}
123+
124+
void removeRegistration(ServerWebSocketContainer serverWebSocketContainer) {
125+
List<String> patterns = this.dynamicRegistrations.remove(serverWebSocketContainer.getWebSocketHandler());
126+
if (this.dynamicHandlerMapping != null && !CollectionUtils.isEmpty(patterns)) {
127+
for (String pattern : patterns) {
128+
this.dynamicHandlerMapping.unregisterHandler(pattern);
129+
}
130+
}
131+
}
132+
133+
private static final class IntegrationDynamicWebSocketHandlerRegistration
134+
extends ServletWebSocketHandlerRegistration {
135+
136+
MultiValueMap<HttpRequestHandler, String> getMapping() {
137+
return getMappings();
138+
}
139+
140+
}
141+
142+
}

0 commit comments

Comments
 (0)